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