skiima 0.1.000 → 0.2.2
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.
- checksums.yaml +15 -0
- data/.gitignore +9 -0
- data/.travis.yml +11 -3
- data/Gemfile +12 -6
- data/Guardfile +13 -11
- data/LICENSE +20 -0
- data/Procfile.example +2 -0
- data/README.md +170 -23
- data/Rakefile +26 -22
- data/lib/skiima.rb +61 -240
- data/lib/skiima/config.rb +60 -0
- data/lib/skiima/config/struct.rb +87 -0
- data/lib/skiima/db/connector.rb +195 -0
- data/lib/skiima/db/connector/active_record.rb +11 -0
- data/lib/skiima/db/connector/active_record/base_connector.rb +34 -0
- data/lib/skiima/db/connector/active_record/mysql2_connector.rb +147 -0
- data/lib/skiima/db/connector/active_record/mysql_connector.rb +177 -0
- data/lib/skiima/db/connector/active_record/postgresql_connector.rb +39 -0
- data/lib/skiima/db/helpers/mysql.rb +230 -0
- data/lib/skiima/db/helpers/postgresql.rb +221 -0
- data/lib/skiima/db/resolver.rb +62 -0
- data/lib/skiima/dependency/reader.rb +55 -0
- data/lib/skiima/dependency/script.rb +63 -0
- data/lib/skiima/i18n.rb +24 -0
- data/lib/skiima/loader.rb +108 -0
- data/lib/skiima/locales/en.yml +2 -2
- data/lib/skiima/logger.rb +54 -0
- data/lib/skiima/railtie.rb +10 -0
- data/lib/skiima/railties/skiima.rake +31 -0
- data/lib/skiima/version.rb +2 -2
- data/skiima.gemspec +5 -5
- data/spec/config/{database.yml → database.yml.example} +16 -0
- data/spec/config/database.yml.travis +69 -0
- data/spec/db/skiima/{depends.yml → dependencies.yml} +7 -2
- data/spec/db/skiima/{empty_depends.yml → empty_dependencies.yml} +0 -0
- data/spec/db/skiima/init_test_db/database.skiima_test.mysql.current.sql +7 -0
- data/spec/db/skiima/init_test_db/database.skiima_test.postgresql.current.sql +7 -0
- data/spec/mysql2_spec.rb +61 -12
- data/spec/mysql_spec.rb +66 -27
- data/spec/postgresql_spec.rb +55 -34
- data/spec/shared_examples/config_shared_example.rb +40 -0
- data/spec/skiima/config/struct_spec.rb +78 -0
- data/spec/skiima/config_spec.rb +6 -0
- data/spec/skiima/db/connector/active_record/base_connector_spec.rb +0 -0
- data/spec/skiima/db/connector/active_record/mysql2_connector_spec.rb +3 -0
- data/spec/skiima/db/connector/active_record/mysql_connector_spec.rb +3 -0
- data/spec/skiima/db/connector/active_record/postgresql_connector_spec.rb +7 -0
- data/spec/skiima/db/connector_spec.rb +6 -0
- data/spec/skiima/db/resolver_spec.rb +54 -0
- data/spec/skiima/dependency/reader_spec.rb +52 -0
- data/spec/skiima/{dependency_spec.rb → dependency/script_spec.rb} +3 -41
- data/spec/skiima/i18n_spec.rb +29 -0
- data/spec/skiima/loader_spec.rb +102 -0
- data/spec/skiima/logger_spec.rb +0 -0
- data/spec/skiima_spec.rb +43 -64
- data/spec/spec_helper.rb +38 -4
- metadata +144 -100
- data/lib/skiima/db_adapters.rb +0 -187
- data/lib/skiima/db_adapters/base_mysql_adapter.rb +0 -308
- data/lib/skiima/db_adapters/mysql2_adapter.rb +0 -114
- data/lib/skiima/db_adapters/mysql_adapter.rb +0 -287
- data/lib/skiima/db_adapters/postgresql_adapter.rb +0 -509
- data/lib/skiima/dependency.rb +0 -84
- data/lib/skiima_helpers.rb +0 -49
- data/spec/skiima/db_adapters/mysql_adapter_spec.rb +0 -38
- data/spec/skiima/db_adapters/postgresql_adapter_spec.rb +0 -20
- data/spec/skiima/db_adapters_spec.rb +0 -31
@@ -1,114 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
require 'skiima/db_adapters/base_mysql_adapter'
|
3
|
-
|
4
|
-
gem 'mysql2', '~> 0.3.10'
|
5
|
-
require 'mysql2'
|
6
|
-
|
7
|
-
module Skiima
|
8
|
-
def self.mysql2_connection(logger, config)
|
9
|
-
config[:username] = 'root' if config[:username].nil?
|
10
|
-
|
11
|
-
if Mysql2::Client.const_defined? :FOUND_ROWS
|
12
|
-
config[:flags] = Mysql2::Client::FOUND_ROWS
|
13
|
-
end
|
14
|
-
|
15
|
-
client = Mysql2::Client.new(Skiima.symbolize_keys(config))
|
16
|
-
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
|
17
|
-
Skiima::DbAdapters::Mysql2Adapter.new(client, logger, options, config)
|
18
|
-
end
|
19
|
-
|
20
|
-
module DbAdapters
|
21
|
-
class Mysql2Adapter < BaseMysqlAdapter
|
22
|
-
ADAPTER_NAME = 'Mysql2'
|
23
|
-
|
24
|
-
def initialize(connection, logger, connection_options, config)
|
25
|
-
super
|
26
|
-
configure_connection
|
27
|
-
end
|
28
|
-
|
29
|
-
def supports_explain?
|
30
|
-
true
|
31
|
-
end
|
32
|
-
|
33
|
-
def error_number(exception)
|
34
|
-
exception.error_number if exception.respond_to?(:error_number)
|
35
|
-
end
|
36
|
-
|
37
|
-
def active?
|
38
|
-
return false unless @connection
|
39
|
-
@connection.ping
|
40
|
-
end
|
41
|
-
|
42
|
-
def reconnect!
|
43
|
-
disconnect!
|
44
|
-
connect
|
45
|
-
end
|
46
|
-
|
47
|
-
# Disconnects from the database if already connected.
|
48
|
-
# Otherwise, this method does nothing.
|
49
|
-
def disconnect!
|
50
|
-
unless @connection.nil?
|
51
|
-
@connection.close
|
52
|
-
@connection = nil
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def reset!
|
57
|
-
disconnect!
|
58
|
-
connect
|
59
|
-
end
|
60
|
-
|
61
|
-
def execute(sql, name = nil)
|
62
|
-
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
|
63
|
-
# made since we established the connection
|
64
|
-
# @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
|
65
|
-
|
66
|
-
# relying on formatting inside the file is precisely what i wanted to avoid...
|
67
|
-
results = sql.split(/^--={4,}/).map do |spider_monkey|
|
68
|
-
super(spider_monkey)
|
69
|
-
end
|
70
|
-
|
71
|
-
results.first
|
72
|
-
end
|
73
|
-
|
74
|
-
def exec_query(sql, name = 'SQL', binds = [])
|
75
|
-
result = execute(sql, name)
|
76
|
-
ActiveRecord::Result.new(result.fields, result.to_a)
|
77
|
-
end
|
78
|
-
|
79
|
-
alias exec_without_stmt exec_query
|
80
|
-
|
81
|
-
private
|
82
|
-
|
83
|
-
def connect
|
84
|
-
@connection = Mysql2::Client.new(@config)
|
85
|
-
configure_connection
|
86
|
-
end
|
87
|
-
|
88
|
-
def configure_connection
|
89
|
-
@connection.query_options.merge!(:as => :array)
|
90
|
-
|
91
|
-
variable_assignments = get_var_assignments
|
92
|
-
execute("SET #{variable_assignments.join(', ')}", :skip_logging)
|
93
|
-
|
94
|
-
version
|
95
|
-
end
|
96
|
-
|
97
|
-
def get_var_assignments
|
98
|
-
# By default, MySQL 'where id is null' selects the last inserted id.
|
99
|
-
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
100
|
-
variable_assignments = ['SQL_AUTO_IS_NULL=0']
|
101
|
-
encoding = @config[:encoding]
|
102
|
-
|
103
|
-
# make sure we set the encoding
|
104
|
-
variable_assignments << "NAMES '#{encoding}'" if encoding
|
105
|
-
|
106
|
-
# increase timeout so mysql server doesn't disconnect us
|
107
|
-
wait_timeout = @config[:wait_timeout]
|
108
|
-
wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
|
109
|
-
variable_assignments << "@@wait_timeout = #{wait_timeout}"
|
110
|
-
end
|
111
|
-
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
@@ -1,287 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
require 'skiima/db_adapters/base_mysql_adapter'
|
3
|
-
|
4
|
-
gem 'mysql', '~> 2.8.1'
|
5
|
-
require 'mysql'
|
6
|
-
|
7
|
-
class Mysql
|
8
|
-
class Time
|
9
|
-
###
|
10
|
-
# This monkey patch is for test_additional_columns_from_join_table
|
11
|
-
def to_date
|
12
|
-
Date.new(year, month, day)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
class Stmt; include Enumerable end
|
16
|
-
class Result; include Enumerable end
|
17
|
-
end
|
18
|
-
|
19
|
-
module Skiima
|
20
|
-
# Establishes a connection to the database that's used by all Active Record objects.
|
21
|
-
def self.mysql_connection(logger, config) # :nodoc:
|
22
|
-
config = Skiima.symbolize_keys(config)
|
23
|
-
host = config[:host]
|
24
|
-
port = config[:port]
|
25
|
-
socket = config[:socket]
|
26
|
-
username = config[:username] ? config[:username].to_s : 'root'
|
27
|
-
password = config[:password].to_s
|
28
|
-
database = config[:database]
|
29
|
-
|
30
|
-
mysql = Mysql.init
|
31
|
-
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
32
|
-
|
33
|
-
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
|
34
|
-
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
|
35
|
-
|
36
|
-
options = [host, username, password, database, port, socket, default_flags]
|
37
|
-
Skiima::DbAdapters::MysqlAdapter.new(mysql, logger, options, config)
|
38
|
-
end
|
39
|
-
|
40
|
-
module DbAdapters
|
41
|
-
class MysqlAdapter < BaseMysqlAdapter
|
42
|
-
attr_accessor :client_encoding
|
43
|
-
|
44
|
-
ADAPTER_NAME = 'MySQL'
|
45
|
-
|
46
|
-
def initialize(connection, logger, connection_options, config)
|
47
|
-
super
|
48
|
-
@client_encoding = nil
|
49
|
-
connect
|
50
|
-
end
|
51
|
-
|
52
|
-
# # Returns the version of the connected MySQL server.
|
53
|
-
def version
|
54
|
-
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
55
|
-
end
|
56
|
-
|
57
|
-
# # Returns true, since this connection adapter supports prepared statement
|
58
|
-
# # caching.
|
59
|
-
def supports_statement_cache?
|
60
|
-
true
|
61
|
-
end
|
62
|
-
|
63
|
-
def error_number(exception) # :nodoc:
|
64
|
-
exception.errno if exception.respond_to?(:errno)
|
65
|
-
end
|
66
|
-
|
67
|
-
def type_cast(value, column)
|
68
|
-
return super unless value == true || value == false
|
69
|
-
|
70
|
-
value ? 1 : 0
|
71
|
-
end
|
72
|
-
|
73
|
-
def quote_string(string) #:nodoc:
|
74
|
-
@connection.quote(string)
|
75
|
-
end
|
76
|
-
|
77
|
-
def active?
|
78
|
-
if @connection.respond_to?(:stat)
|
79
|
-
@connection.stat
|
80
|
-
else
|
81
|
-
@connection.query 'select 1'
|
82
|
-
end
|
83
|
-
|
84
|
-
# mysql-ruby doesn't raise an exception when stat fails.
|
85
|
-
if @connection.respond_to?(:errno)
|
86
|
-
@connection.errno.zero?
|
87
|
-
else
|
88
|
-
true
|
89
|
-
end
|
90
|
-
rescue Mysql::Error
|
91
|
-
false
|
92
|
-
end
|
93
|
-
|
94
|
-
def reconnect!
|
95
|
-
disconnect!
|
96
|
-
clear_cache!
|
97
|
-
connect
|
98
|
-
end
|
99
|
-
|
100
|
-
# Disconnects from the database if already connected. Otherwise, this
|
101
|
-
# method does nothing.
|
102
|
-
def disconnect!
|
103
|
-
@connection.close rescue nil
|
104
|
-
end
|
105
|
-
|
106
|
-
def reset!
|
107
|
-
if @connection.respond_to?(:change_user)
|
108
|
-
# See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
|
109
|
-
# reset the connection is to change the user to the same user.
|
110
|
-
@connection.change_user(@config[:username], @config[:password], @config[:database])
|
111
|
-
configure_connection
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
if "<3".respond_to?(:encode)
|
116
|
-
# Taken from here:
|
117
|
-
# https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
|
118
|
-
# Author: TOMITA Masahiro <tommy@tmtm.org>
|
119
|
-
ENCODINGS = {
|
120
|
-
"armscii8" => nil,
|
121
|
-
"ascii" => Encoding::US_ASCII,
|
122
|
-
"big5" => Encoding::Big5,
|
123
|
-
"binary" => Encoding::ASCII_8BIT,
|
124
|
-
"cp1250" => Encoding::Windows_1250,
|
125
|
-
"cp1251" => Encoding::Windows_1251,
|
126
|
-
"cp1256" => Encoding::Windows_1256,
|
127
|
-
"cp1257" => Encoding::Windows_1257,
|
128
|
-
"cp850" => Encoding::CP850,
|
129
|
-
"cp852" => Encoding::CP852,
|
130
|
-
"cp866" => Encoding::IBM866,
|
131
|
-
"cp932" => Encoding::Windows_31J,
|
132
|
-
"dec8" => nil,
|
133
|
-
"eucjpms" => Encoding::EucJP_ms,
|
134
|
-
"euckr" => Encoding::EUC_KR,
|
135
|
-
"gb2312" => Encoding::EUC_CN,
|
136
|
-
"gbk" => Encoding::GBK,
|
137
|
-
"geostd8" => nil,
|
138
|
-
"greek" => Encoding::ISO_8859_7,
|
139
|
-
"hebrew" => Encoding::ISO_8859_8,
|
140
|
-
"hp8" => nil,
|
141
|
-
"keybcs2" => nil,
|
142
|
-
"koi8r" => Encoding::KOI8_R,
|
143
|
-
"koi8u" => Encoding::KOI8_U,
|
144
|
-
"latin1" => Encoding::ISO_8859_1,
|
145
|
-
"latin2" => Encoding::ISO_8859_2,
|
146
|
-
"latin5" => Encoding::ISO_8859_9,
|
147
|
-
"latin7" => Encoding::ISO_8859_13,
|
148
|
-
"macce" => Encoding::MacCentEuro,
|
149
|
-
"macroman" => Encoding::MacRoman,
|
150
|
-
"sjis" => Encoding::SHIFT_JIS,
|
151
|
-
"swe7" => nil,
|
152
|
-
"tis620" => Encoding::TIS_620,
|
153
|
-
"ucs2" => Encoding::UTF_16BE,
|
154
|
-
"ujis" => Encoding::EucJP_ms,
|
155
|
-
"utf8" => Encoding::UTF_8,
|
156
|
-
"utf8mb4" => Encoding::UTF_8,
|
157
|
-
}
|
158
|
-
else
|
159
|
-
ENCODINGS = Hash.new { |h,k| h[k] = k }
|
160
|
-
end
|
161
|
-
|
162
|
-
# Get the client encoding for this database
|
163
|
-
def client_encoding
|
164
|
-
return @client_encoding if @client_encoding
|
165
|
-
|
166
|
-
result = exec_query(
|
167
|
-
"SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
|
168
|
-
'SCHEMA')
|
169
|
-
@client_encoding = ENCODINGS[result.last.last]
|
170
|
-
end
|
171
|
-
|
172
|
-
def execute(sql, name = nil)
|
173
|
-
# this is nauseating. i can't believe it's an issue to run multiple statements
|
174
|
-
# tried using OPTION_MULTI_STATEMENTS_ON, but ruby segfaulted. womp womp.
|
175
|
-
# my workaround is messy for now
|
176
|
-
|
177
|
-
# relying on formatting inside the file is precisely what i wanted to avoid...
|
178
|
-
results = sql.split(/^--={4,}/).map do |spider_monkey|
|
179
|
-
super(spider_monkey)
|
180
|
-
end
|
181
|
-
|
182
|
-
results.first
|
183
|
-
end
|
184
|
-
|
185
|
-
def exec_query(sql, name = 'SQL')
|
186
|
-
log(sql, name) do
|
187
|
-
exec_stmt(sql, name) do |cols, stmt|
|
188
|
-
stmt.to_a
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
# def exec_without_stmt(sql, name = 'SQL') # :nodoc:
|
194
|
-
# # Some queries, like SHOW CREATE TABLE don't work through the prepared
|
195
|
-
# # statement API. For those queries, we need to use this method. :'(
|
196
|
-
# log(sql, name) do
|
197
|
-
# result = @connection.query(sql)
|
198
|
-
# cols = []
|
199
|
-
# rows = []
|
200
|
-
|
201
|
-
# if result
|
202
|
-
# cols = result.fetch_fields.map { |field| field.name }
|
203
|
-
# rows = result.to_a
|
204
|
-
# result.free
|
205
|
-
# end
|
206
|
-
# ActiveRecord::Result.new(cols, rows)
|
207
|
-
# end
|
208
|
-
# end
|
209
|
-
|
210
|
-
def execute_and_free(sql, name = nil)
|
211
|
-
result = execute(sql, name)
|
212
|
-
ret = yield result
|
213
|
-
result.free
|
214
|
-
ret
|
215
|
-
end
|
216
|
-
|
217
|
-
def begin_db_transaction #:nodoc:
|
218
|
-
exec_without_stmt "BEGIN"
|
219
|
-
rescue Mysql::Error
|
220
|
-
# Transactions aren't supported
|
221
|
-
end
|
222
|
-
|
223
|
-
private
|
224
|
-
|
225
|
-
def exec_stmt(sql, name)
|
226
|
-
stmt = @connection.prepare(sql)
|
227
|
-
|
228
|
-
begin
|
229
|
-
stmt.execute
|
230
|
-
rescue Mysql::Error => e
|
231
|
-
# Older versions of MySQL leave the prepared statement in a bad
|
232
|
-
# place when an error occurs. To support older mysql versions, we
|
233
|
-
# need to close the statement and delete the statement from the
|
234
|
-
# cache.
|
235
|
-
stmt.close
|
236
|
-
raise e
|
237
|
-
end
|
238
|
-
|
239
|
-
cols = nil
|
240
|
-
if metadata = stmt.result_metadata
|
241
|
-
cols = metadata.fetch_fields.map { |field| field.name }
|
242
|
-
end
|
243
|
-
|
244
|
-
result = yield [cols, stmt]
|
245
|
-
|
246
|
-
stmt.result_metadata.free if cols
|
247
|
-
stmt.free_result
|
248
|
-
stmt.close
|
249
|
-
|
250
|
-
result
|
251
|
-
end
|
252
|
-
|
253
|
-
def connect
|
254
|
-
encoding = @config[:encoding]
|
255
|
-
if encoding
|
256
|
-
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
257
|
-
end
|
258
|
-
|
259
|
-
if @config[:sslca] || @config[:sslkey]
|
260
|
-
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
|
261
|
-
end
|
262
|
-
|
263
|
-
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
|
264
|
-
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
|
265
|
-
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
|
266
|
-
|
267
|
-
@connection.real_connect(*@connection_options)
|
268
|
-
|
269
|
-
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
|
270
|
-
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
|
271
|
-
|
272
|
-
version #gets version
|
273
|
-
|
274
|
-
configure_connection
|
275
|
-
end
|
276
|
-
|
277
|
-
def configure_connection
|
278
|
-
encoding = @config[:encoding]
|
279
|
-
execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
|
280
|
-
|
281
|
-
# By default, MySQL 'where id is null' selects the last inserted id.
|
282
|
-
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
283
|
-
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
|
284
|
-
end
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|
@@ -1,509 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
gem 'pg', '~> 0.11'
|
3
|
-
require 'pg'
|
4
|
-
|
5
|
-
module Skiima
|
6
|
-
def self.postgresql_connection(logger, config) # :nodoc:
|
7
|
-
config = Skiima.symbolize_keys(config)
|
8
|
-
host = config[:host]
|
9
|
-
port = config[:port] || 5432
|
10
|
-
username = config[:username].to_s if config[:username]
|
11
|
-
password = config[:password].to_s if config[:password]
|
12
|
-
|
13
|
-
if config.key?(:database)
|
14
|
-
database = config[:database]
|
15
|
-
else
|
16
|
-
raise ArgumentError, "No database specified. Missing argument: database."
|
17
|
-
end
|
18
|
-
|
19
|
-
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
20
|
-
# so just pass a nil connection object for the time being.
|
21
|
-
Skiima::DbAdapters::PostgresqlAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
|
22
|
-
end
|
23
|
-
|
24
|
-
module DbAdapters
|
25
|
-
|
26
|
-
class PostgresqlAdapter < Base
|
27
|
-
attr_accessor :version, :local_tz
|
28
|
-
|
29
|
-
ADAPTER_NAME = 'PostgreSQL'
|
30
|
-
|
31
|
-
NATIVE_DATABASE_TYPES = {
|
32
|
-
:primary_key => "serial primary key",
|
33
|
-
:string => { :name => "character varying", :limit => 255 },
|
34
|
-
:text => { :name => "text" },
|
35
|
-
:integer => { :name => "integer" },
|
36
|
-
:float => { :name => "float" },
|
37
|
-
:decimal => { :name => "decimal" },
|
38
|
-
:datetime => { :name => "timestamp" },
|
39
|
-
:timestamp => { :name => "timestamp" },
|
40
|
-
:time => { :name => "time" },
|
41
|
-
:date => { :name => "date" },
|
42
|
-
:binary => { :name => "bytea" },
|
43
|
-
:boolean => { :name => "boolean" },
|
44
|
-
:xml => { :name => "xml" },
|
45
|
-
:tsvector => { :name => "tsvector" }
|
46
|
-
}
|
47
|
-
|
48
|
-
MONEY_COLUMN_TYPE_OID = 790 # The internal PostgreSQL identifier of the money data type.
|
49
|
-
BYTEA_COLUMN_TYPE_OID = 17 # The internal PostgreSQL identifier of the BYTEA data type.
|
50
|
-
|
51
|
-
def adapter_name
|
52
|
-
ADAPTER_NAME
|
53
|
-
end
|
54
|
-
|
55
|
-
# Initializes and connects a PostgreSQL adapter.
|
56
|
-
def initialize(connection, logger, connection_parameters, config)
|
57
|
-
super(connection, logger)
|
58
|
-
@connection_parameters, @config = connection_parameters, config
|
59
|
-
# @visitor = Arel::Visitors::PostgreSQL.new self
|
60
|
-
|
61
|
-
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
|
62
|
-
@local_tz = nil
|
63
|
-
@table_alias_length = nil
|
64
|
-
@version = nil
|
65
|
-
|
66
|
-
connect
|
67
|
-
check_psql_version
|
68
|
-
@local_tz = get_timezone
|
69
|
-
end
|
70
|
-
|
71
|
-
# Is this connection alive and ready for queries?
|
72
|
-
def active?
|
73
|
-
@connection.status == PGconn::CONNECTION_OK
|
74
|
-
rescue PGError
|
75
|
-
false
|
76
|
-
end
|
77
|
-
|
78
|
-
# Close then reopen the connection.
|
79
|
-
def reconnect!
|
80
|
-
clear_cache!
|
81
|
-
@connection.reset
|
82
|
-
configure_connection
|
83
|
-
end
|
84
|
-
|
85
|
-
def reset!
|
86
|
-
clear_cache!
|
87
|
-
super
|
88
|
-
end
|
89
|
-
|
90
|
-
# Disconnects from the database if already connected. Otherwise, this
|
91
|
-
# method does nothing.
|
92
|
-
def disconnect!
|
93
|
-
clear_cache!
|
94
|
-
@connection.close rescue nil
|
95
|
-
end
|
96
|
-
|
97
|
-
# Enable standard-conforming strings if available.
|
98
|
-
def set_standard_conforming_strings
|
99
|
-
old, self.client_min_messages = client_min_messages, 'panic'
|
100
|
-
execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
|
101
|
-
ensure
|
102
|
-
self.client_min_messages = old
|
103
|
-
end
|
104
|
-
|
105
|
-
def supported_objects
|
106
|
-
[:database, :schema, :table, :view, :rule, :index]
|
107
|
-
end
|
108
|
-
|
109
|
-
def database_exists?(name, opts = {})
|
110
|
-
query(Skiima.interpolate_sql('&', <<-SQL, { :database => name }))[0][0].to_i > 0
|
111
|
-
SELECT COUNT(*)
|
112
|
-
FROM pg_databases pdb
|
113
|
-
WHERE pdb.datname = '&database'
|
114
|
-
SQL
|
115
|
-
end
|
116
|
-
|
117
|
-
def schema_exists?(name, opts = {})
|
118
|
-
query(Skiima.interpolate_sql('&', <<-SQL, { :schema => name }))[0][0].to_i > 0
|
119
|
-
SELECT COUNT(*)
|
120
|
-
FROM pg_namespace
|
121
|
-
WHERE nspname = '&schema'
|
122
|
-
SQL
|
123
|
-
end
|
124
|
-
|
125
|
-
def table_exists?(name, opts = {})
|
126
|
-
schema, table = Utils.extract_schema_and_table(name.to_s)
|
127
|
-
vars = { :table => table,
|
128
|
-
:schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
|
129
|
-
|
130
|
-
vars.inspect
|
131
|
-
|
132
|
-
query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
|
133
|
-
SELECT COUNT(*)
|
134
|
-
FROM pg_class c
|
135
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
136
|
-
WHERE c.relkind in ('r')
|
137
|
-
AND c.relname = '&table'
|
138
|
-
AND n.nspname = &schema
|
139
|
-
SQL
|
140
|
-
end
|
141
|
-
|
142
|
-
def view_exists?(name, opts = {})
|
143
|
-
schema, view = Utils.extract_schema_and_table(name.to_s)
|
144
|
-
vars = { :view => view,
|
145
|
-
:schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
|
146
|
-
|
147
|
-
query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
|
148
|
-
SELECT COUNT(*)
|
149
|
-
FROM pg_class c
|
150
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
151
|
-
WHERE c.relkind in ('v')
|
152
|
-
AND c.relname = '&view'
|
153
|
-
AND n.nspname = &schema
|
154
|
-
SQL
|
155
|
-
end
|
156
|
-
|
157
|
-
def rule_exists?(name, opts = {})
|
158
|
-
target = opts[:attr] ? opts[:attr][0] : nil
|
159
|
-
raise "requires target object" unless target
|
160
|
-
schema, rule = Utils.extract_schema_and_table(name.to_s)
|
161
|
-
vars = { :rule => rule,
|
162
|
-
:target => target,
|
163
|
-
:schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
|
164
|
-
|
165
|
-
query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
|
166
|
-
SELECT COUNT(*)
|
167
|
-
FROM pg_rules pgr
|
168
|
-
WHERE pgr.rulename = '&rule'
|
169
|
-
AND pgr.tablename = '&target'
|
170
|
-
AND pgr.schemaname = &schema
|
171
|
-
SQL
|
172
|
-
end
|
173
|
-
|
174
|
-
def index_exists?(name, opts = {})
|
175
|
-
target = opts[:attr] ? opts[:attr][0] : nil
|
176
|
-
raise "requires target object" unless target
|
177
|
-
schema, index = Utils.extract_schema_and_table(name.to_s)
|
178
|
-
vars = { :index => index,
|
179
|
-
:target => target,
|
180
|
-
:schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
|
181
|
-
|
182
|
-
query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
|
183
|
-
SELECT COUNT(*)
|
184
|
-
FROM pg_indexes pgr
|
185
|
-
WHERE pgr.indexname = '&index'
|
186
|
-
AND pgr.tablename = '&target'
|
187
|
-
AND pgr.schemaname = &schema
|
188
|
-
SQL
|
189
|
-
end
|
190
|
-
|
191
|
-
def drop_database(name, opts = {})
|
192
|
-
"DROP DATABASE IF EXISTS #{name}"
|
193
|
-
end
|
194
|
-
|
195
|
-
def drop_schema(name, opts = {})
|
196
|
-
"DROP SCHEMA IF EXISTS #{name}"
|
197
|
-
end
|
198
|
-
|
199
|
-
def drop_table(name, opts = {})
|
200
|
-
"DROP TABLE IF EXISTS #{name}"
|
201
|
-
end
|
202
|
-
|
203
|
-
def drop_view(name, opts = {})
|
204
|
-
"DROP VIEW IF EXISTS #{name}"
|
205
|
-
end
|
206
|
-
|
207
|
-
def drop_rule(name, opts = {})
|
208
|
-
target = opts[:attr].first if opts[:attr]
|
209
|
-
raise "requires target object" unless target
|
210
|
-
|
211
|
-
"DROP RULE IF EXISTS #{name} ON #{target}"
|
212
|
-
end
|
213
|
-
|
214
|
-
def drop_index(name, opts = {})
|
215
|
-
"DROP INDEX IF EXISTS #{name}"
|
216
|
-
end
|
217
|
-
|
218
|
-
class PgColumn
|
219
|
-
attr_accessor :name, :deafult, :type, :null
|
220
|
-
def initialize(name, default = nil, type = nil, null = true)
|
221
|
-
@name, @default, @type, @null = name, default, type, null
|
222
|
-
end
|
223
|
-
|
224
|
-
def to_s
|
225
|
-
# to be implemented
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
# Returns the list of a table's column names, data types, and default values.
|
230
|
-
#
|
231
|
-
# The underlying query is roughly:
|
232
|
-
# SELECT column.name, column.type, default.value
|
233
|
-
# FROM column LEFT JOIN default
|
234
|
-
# ON column.table_id = default.table_id
|
235
|
-
# AND column.num = default.column_num
|
236
|
-
# WHERE column.table_id = get_table_id('table_name')
|
237
|
-
# AND column.num > 0
|
238
|
-
# AND NOT column.is_dropped
|
239
|
-
# ORDER BY column.num
|
240
|
-
#
|
241
|
-
# If the table name is not prefixed with a schema, the database will
|
242
|
-
# take the first match from the schema search path.
|
243
|
-
#
|
244
|
-
# Query implementation notes:
|
245
|
-
# - format_type includes the column size constraint, e.g. varchar(50)
|
246
|
-
# - ::regclass is a function that gives the id for a table name
|
247
|
-
def column_definitions(table_name) #:nodoc:
|
248
|
-
query(<<-SQL, 'SCHEMA')
|
249
|
-
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
250
|
-
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
251
|
-
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
252
|
-
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
|
253
|
-
AND a.attnum > 0 AND NOT a.attisdropped
|
254
|
-
ORDER BY a.attnum
|
255
|
-
SQL
|
256
|
-
end
|
257
|
-
|
258
|
-
def column_names(table_name)
|
259
|
-
cols = column_definitions(table_name).map do |c|
|
260
|
-
PgColumn.new(c[0], c[1], c[2], c[3])
|
261
|
-
end
|
262
|
-
cols.map(&:name)
|
263
|
-
end
|
264
|
-
|
265
|
-
# Checks the following cases:
|
266
|
-
#
|
267
|
-
# - table_name
|
268
|
-
# - "table.name"
|
269
|
-
# - schema_name.table_name
|
270
|
-
# - schema_name."table.name"
|
271
|
-
# - "schema.name".table_name
|
272
|
-
# - "schema.name"."table.name"
|
273
|
-
def quote_table_name(name)
|
274
|
-
schema, name_part = extract_pg_identifier_from_name(name.to_s)
|
275
|
-
|
276
|
-
unless name_part
|
277
|
-
quote_column_name(schema)
|
278
|
-
else
|
279
|
-
table_name, name_part = extract_pg_identifier_from_name(name_part)
|
280
|
-
"#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
# Quotes column names for use in SQL queries.
|
285
|
-
def quote_column_name(name) #:nodoc:
|
286
|
-
PGconn.quote_ident(name.to_s)
|
287
|
-
end
|
288
|
-
|
289
|
-
def extract_pg_identifier_from_name(name)
|
290
|
-
match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
|
291
|
-
|
292
|
-
if match_data
|
293
|
-
rest = name[match_data[0].length, name.length]
|
294
|
-
rest = rest[1, rest.length] if rest.start_with? "."
|
295
|
-
[match_data[1], (rest.length > 0 ? rest : nil)]
|
296
|
-
end
|
297
|
-
end
|
298
|
-
|
299
|
-
# Executes an SQL statement, returning a PGresult object on success
|
300
|
-
# or raising a PGError exception otherwise.
|
301
|
-
def execute(sql, name = nil)
|
302
|
-
log(sql, name) do
|
303
|
-
@connection.async_exec(sql)
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
# Queries the database and returns the results in an Array-like object
|
308
|
-
def query(sql, name = nil) #:nodoc:
|
309
|
-
log(sql, name) do
|
310
|
-
result_as_array @connection.async_exec(sql)
|
311
|
-
end
|
312
|
-
end
|
313
|
-
|
314
|
-
# create a 2D array representing the result set
|
315
|
-
def result_as_array(res) #:nodoc:
|
316
|
-
# check if we have any binary column and if they need escaping
|
317
|
-
ftypes = Array.new(res.nfields) do |i|
|
318
|
-
[i, res.ftype(i)]
|
319
|
-
end
|
320
|
-
|
321
|
-
rows = res.values
|
322
|
-
return rows unless ftypes.any? { |_, x|
|
323
|
-
x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
|
324
|
-
}
|
325
|
-
|
326
|
-
typehash = ftypes.group_by { |_, type| type }
|
327
|
-
binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
|
328
|
-
monies = typehash[MONEY_COLUMN_TYPE_OID] || []
|
329
|
-
|
330
|
-
rows.each do |row|
|
331
|
-
# unescape string passed BYTEA field (OID == 17)
|
332
|
-
binaries.each do |index, _|
|
333
|
-
row[index] = unescape_bytea(row[index])
|
334
|
-
end
|
335
|
-
|
336
|
-
# If this is a money type column and there are any currency symbols,
|
337
|
-
# then strip them off. Indeed it would be prettier to do this in
|
338
|
-
# PostgreSQLColumn.string_to_decimal but would break form input
|
339
|
-
# fields that call value_before_type_cast.
|
340
|
-
monies.each do |index, _|
|
341
|
-
data = row[index]
|
342
|
-
# Because money output is formatted according to the locale, there are two
|
343
|
-
# cases to consider (note the decimal separators):
|
344
|
-
# (1) $12,345,678.12
|
345
|
-
# (2) $12.345.678,12
|
346
|
-
case data
|
347
|
-
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
|
348
|
-
data.gsub!(/[^-\d.]/, '')
|
349
|
-
when /^-?\D+[\d.]+,\d{2}$/ # (2)
|
350
|
-
data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
|
351
|
-
end
|
352
|
-
end
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
# Set the authorized user for this session
|
357
|
-
# def session_auth=(user)
|
358
|
-
# clear_cache!
|
359
|
-
# exec_query "SET SESSION AUTHORIZATION #{user}"
|
360
|
-
# end
|
361
|
-
|
362
|
-
# Begins a transaction.
|
363
|
-
def begin_db_transaction
|
364
|
-
execute "BEGIN"
|
365
|
-
end
|
366
|
-
|
367
|
-
# # Commits a transaction.
|
368
|
-
def commit_db_transaction
|
369
|
-
execute "COMMIT"
|
370
|
-
end
|
371
|
-
|
372
|
-
# # Aborts a transaction.
|
373
|
-
def rollback_db_transaction
|
374
|
-
execute "ROLLBACK"
|
375
|
-
end
|
376
|
-
|
377
|
-
def outside_transaction?
|
378
|
-
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
379
|
-
end
|
380
|
-
|
381
|
-
def create_savepoint
|
382
|
-
execute("SAVEPOINT #{current_savepoint_name}")
|
383
|
-
end
|
384
|
-
|
385
|
-
def rollback_to_savepoint
|
386
|
-
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
387
|
-
end
|
388
|
-
|
389
|
-
def release_savepoint
|
390
|
-
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
391
|
-
end
|
392
|
-
|
393
|
-
# Returns the current database name.
|
394
|
-
def current_database
|
395
|
-
query('select current_database()')[0][0]
|
396
|
-
end
|
397
|
-
|
398
|
-
# # Returns the current schema name.
|
399
|
-
def current_schema
|
400
|
-
query('SELECT current_schema', 'SCHEMA')[0][0]
|
401
|
-
end
|
402
|
-
|
403
|
-
# Returns the current client message level.
|
404
|
-
def client_min_messages
|
405
|
-
query('SHOW client_min_messages', 'SCHEMA')[0][0]
|
406
|
-
end
|
407
|
-
|
408
|
-
# Set the client message level.
|
409
|
-
def client_min_messages=(level)
|
410
|
-
execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
|
411
|
-
end
|
412
|
-
|
413
|
-
# def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
414
|
-
# need to be able to reset sequences?
|
415
|
-
|
416
|
-
# Sets the schema search path to a string of comma-separated schema names.
|
417
|
-
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
418
|
-
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
|
419
|
-
#
|
420
|
-
# This should be not be called manually but set in database.yml.
|
421
|
-
def schema_search_path=(schema_csv)
|
422
|
-
if schema_csv
|
423
|
-
execute "SET search_path TO #{schema_csv}"
|
424
|
-
@schema_search_path = schema_csv
|
425
|
-
end
|
426
|
-
end
|
427
|
-
|
428
|
-
module Utils
|
429
|
-
extend self
|
430
|
-
|
431
|
-
# Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
|
432
|
-
# +schema_name+ is nil if not specified in +name+.
|
433
|
-
# +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
|
434
|
-
# +name+ supports the range of schema/table references understood by PostgreSQL, for example:
|
435
|
-
#
|
436
|
-
# * <tt>table_name</tt>
|
437
|
-
# * <tt>"table.name"</tt>
|
438
|
-
# * <tt>schema_name.table_name</tt>
|
439
|
-
# * <tt>schema_name."table.name"</tt>
|
440
|
-
# * <tt>"schema.name"."table name"</tt>
|
441
|
-
def extract_schema_and_table(name)
|
442
|
-
table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
|
443
|
-
[schema, table]
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
protected
|
448
|
-
|
449
|
-
# Returns the version of the connected PostgreSQL server.
|
450
|
-
def postgresql_version
|
451
|
-
@connection.server_version
|
452
|
-
end
|
453
|
-
|
454
|
-
def check_psql_version
|
455
|
-
@version = postgresql_version
|
456
|
-
if @version < 80200
|
457
|
-
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
|
458
|
-
end
|
459
|
-
end
|
460
|
-
|
461
|
-
def get_timezone
|
462
|
-
execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
|
463
|
-
end
|
464
|
-
|
465
|
-
def translate_exception(e, message)
|
466
|
-
e
|
467
|
-
# case exception.message
|
468
|
-
# when /duplicate key value violates unique constraint/
|
469
|
-
# RecordNotUnique.new(message, exception)
|
470
|
-
# when /violates foreign key constraint/
|
471
|
-
# InvalidForeignKey.new(message, exception)
|
472
|
-
# else
|
473
|
-
# super
|
474
|
-
# end
|
475
|
-
end
|
476
|
-
|
477
|
-
private
|
478
|
-
|
479
|
-
# Connects to a PostgreSQL server and sets up the adapter depending on the
|
480
|
-
# connected server's characteristics.
|
481
|
-
def connect
|
482
|
-
@connection = PGconn.connect(*@connection_parameters)
|
483
|
-
|
484
|
-
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
485
|
-
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
486
|
-
# should know about this but can't detect it there, so deal with it here.
|
487
|
-
# PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
|
488
|
-
|
489
|
-
configure_connection
|
490
|
-
end
|
491
|
-
|
492
|
-
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
|
493
|
-
# This is called by #connect and should not be called manually.
|
494
|
-
def configure_connection
|
495
|
-
if @config[:encoding]
|
496
|
-
@connection.set_client_encoding(@config[:encoding])
|
497
|
-
end
|
498
|
-
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
499
|
-
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
500
|
-
|
501
|
-
# Use standard-conforming strings if available so we don't have to do the E'...' dance.
|
502
|
-
set_standard_conforming_strings
|
503
|
-
|
504
|
-
#configure the connection to return TIMESTAMP WITH ZONE types in UTC.
|
505
|
-
execute("SET time zone '#{@local_tz}'", 'SCHEMA') if @local_tz
|
506
|
-
end
|
507
|
-
end
|
508
|
-
end
|
509
|
-
end
|