skiima 0.1.000
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/.travis.yml +8 -0
- data/CHANGELOG +8 -0
- data/Gemfile +30 -0
- data/Guardfile +14 -0
- data/README.md +87 -0
- data/Rakefile +56 -0
- data/lib/skiima/db_adapters/base_mysql_adapter.rb +308 -0
- data/lib/skiima/db_adapters/mysql2_adapter.rb +114 -0
- data/lib/skiima/db_adapters/mysql_adapter.rb +287 -0
- data/lib/skiima/db_adapters/postgresql_adapter.rb +509 -0
- data/lib/skiima/db_adapters.rb +187 -0
- data/lib/skiima/dependency.rb +84 -0
- data/lib/skiima/locales/en.yml +20 -0
- data/lib/skiima/locales/fr.yml +2 -0
- data/lib/skiima/version.rb +4 -0
- data/lib/skiima.rb +270 -0
- data/lib/skiima_helpers.rb +49 -0
- data/skiima.gemspec +53 -0
- data/spec/config/database.yml +56 -0
- data/spec/db/skiima/depends.yml +61 -0
- data/spec/db/skiima/empty_depends.yml +0 -0
- data/spec/db/skiima/init_test_db/database.skiima_test.mysql.current.sql +2 -0
- data/spec/db/skiima/init_test_db/database.skiima_test.postgresql.current.sql +2 -0
- data/spec/db/skiima/test_column_names/table.test_column_names.mysql.current.sql +8 -0
- data/spec/db/skiima/test_column_names/table.test_column_names.postgresql.current.sql +18 -0
- data/spec/db/skiima/test_index/index.test_index.mysql.current.sql +2 -0
- data/spec/db/skiima/test_index/index.test_index.postgresql.current.sql +2 -0
- data/spec/db/skiima/test_proc/proc.test_proc.mysql.current.sql +8 -0
- data/spec/db/skiima/test_proc/proc.test_proc_drop.mysql.current.drop.sql +1 -0
- data/spec/db/skiima/test_proc/proc.test_proc_drop.mysql.current.sql +8 -0
- data/spec/db/skiima/test_rule/rule.test_rule.postgresql.current.sql +6 -0
- data/spec/db/skiima/test_schema/schema.test_schema.postgresql.current.sql +2 -0
- data/spec/db/skiima/test_table/table.test_table.mysql.current.sql +7 -0
- data/spec/db/skiima/test_table/table.test_table.postgresql.current.sql +4 -0
- data/spec/db/skiima/test_view/view.test_view.mysql.current.sql +5 -0
- data/spec/db/skiima/test_view/view.test_view.postgresql.current.sql +6 -0
- data/spec/helpers/mysql_spec_helper.rb +0 -0
- data/spec/helpers/postgresql_spec_helper.rb +11 -0
- data/spec/mysql2_spec.rb +57 -0
- data/spec/mysql_spec.rb +100 -0
- data/spec/postgresql_spec.rb +138 -0
- data/spec/skiima/db_adapters/mysql_adapter_spec.rb +38 -0
- data/spec/skiima/db_adapters/postgresql_adapter_spec.rb +20 -0
- data/spec/skiima/db_adapters_spec.rb +31 -0
- data/spec/skiima/dependency_spec.rb +94 -0
- data/spec/skiima_spec.rb +97 -0
- data/spec/spec_helper.rb +35 -0
- metadata +195 -0
@@ -0,0 +1,509 @@
|
|
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
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Skiima
|
3
|
+
module DbAdapters
|
4
|
+
class Base
|
5
|
+
attr_accessor :version
|
6
|
+
|
7
|
+
def initialize(connection, logger = nil) #:nodoc:
|
8
|
+
super()
|
9
|
+
|
10
|
+
@active = nil
|
11
|
+
@connection = connection
|
12
|
+
@in_use = false
|
13
|
+
@last_use = false
|
14
|
+
@logger = logger
|
15
|
+
@visitor = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def adapter_name
|
19
|
+
'Base'
|
20
|
+
end
|
21
|
+
|
22
|
+
def supports_ddl_transactions?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def supported_objects
|
27
|
+
[] # this should be overridden by concrete adapters
|
28
|
+
end
|
29
|
+
|
30
|
+
def drop(type, name, opts = {})
|
31
|
+
send("drop_#{type}", name, opts) if supported_objects.include? type.to_sym
|
32
|
+
end
|
33
|
+
|
34
|
+
def object_exists?(type, name, opts = {})
|
35
|
+
send("#{type}_exists?", name, opts) if supported_objects.include? type.to_sym
|
36
|
+
end
|
37
|
+
|
38
|
+
# Does this adapter support savepoints? PostgreSQL and MySQL do,
|
39
|
+
# SQLite < 3.6.8 does not.
|
40
|
+
def supports_savepoints?
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def active?
|
45
|
+
@active != false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Disconnects from the database if already connected, and establishes a
|
49
|
+
# new connection with the database.
|
50
|
+
def reconnect!
|
51
|
+
@active = true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Disconnects from the database if already connected. Otherwise, this
|
55
|
+
# method does nothing.
|
56
|
+
def disconnect!
|
57
|
+
@active = false
|
58
|
+
end
|
59
|
+
|
60
|
+
# Reset the state of this connection, directing the DBMS to clear
|
61
|
+
# transactions and other connection-related server-side state. Usually a
|
62
|
+
# database-dependent operation.
|
63
|
+
#
|
64
|
+
# The default implementation does nothing; the implementation should be
|
65
|
+
# overridden by concrete adapters.
|
66
|
+
def reset!
|
67
|
+
# this should be overridden by concrete adapters
|
68
|
+
end
|
69
|
+
|
70
|
+
###
|
71
|
+
# Clear any caching the database adapter may be doing, for example
|
72
|
+
# clearing the prepared statement cache. This is database specific.
|
73
|
+
def clear_cache!
|
74
|
+
# this should be overridden by concrete adapters
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns true if its required to reload the connection between requests for development mode.
|
78
|
+
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
|
79
|
+
def requires_reloading?
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks whether the connection to the database is still active (i.e. not stale).
|
84
|
+
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
85
|
+
# is no longer active, then this method will reconnect to the database.
|
86
|
+
def verify!(*ignored)
|
87
|
+
reconnect! unless active?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Provides access to the underlying database driver for this adapter. For
|
91
|
+
# example, this method returns a Mysql object in case of MysqlAdapter,
|
92
|
+
# and a PGconn object in case of PostgreSQLAdapter.
|
93
|
+
#
|
94
|
+
# This is useful for when you need to call a proprietary method such as
|
95
|
+
# PostgreSQL's lo_* methods.
|
96
|
+
def raw_connection
|
97
|
+
@connection
|
98
|
+
end
|
99
|
+
|
100
|
+
attr_reader :open_transactions
|
101
|
+
|
102
|
+
def increment_open_transactions
|
103
|
+
@open_transactions += 1
|
104
|
+
end
|
105
|
+
|
106
|
+
def decrement_open_transactions
|
107
|
+
@open_transactions -= 1
|
108
|
+
end
|
109
|
+
|
110
|
+
def transaction_joinable=(joinable)
|
111
|
+
@transaction_joinable = joinable
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_savepoint
|
115
|
+
end
|
116
|
+
|
117
|
+
def rollback_to_savepoint
|
118
|
+
end
|
119
|
+
|
120
|
+
def release_savepoint
|
121
|
+
end
|
122
|
+
|
123
|
+
def current_savepoint_name
|
124
|
+
"active_record_#{open_transactions}"
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check the connection back in to the connection pool
|
128
|
+
def close
|
129
|
+
disconnect!
|
130
|
+
end
|
131
|
+
|
132
|
+
# Disconnects from the database if already connected. Otherwise, this
|
133
|
+
# method does nothing.
|
134
|
+
def disconnect!
|
135
|
+
clear_cache!
|
136
|
+
@connection.close rescue nil
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
140
|
+
|
141
|
+
def log(sql, name = "SQL", binds = [])
|
142
|
+
@logger.debug("Executing SQL Statement: #{name}")
|
143
|
+
@logger.debug(sql)
|
144
|
+
result = yield
|
145
|
+
@logger.debug("SUCCESS!")
|
146
|
+
result
|
147
|
+
rescue Exception => e
|
148
|
+
message = "#{e.class.name}: #{e.message}: #{sql}"
|
149
|
+
@logger.debug message if @logger
|
150
|
+
exception = translate_exception(e, message)
|
151
|
+
exception.set_backtrace e.backtrace
|
152
|
+
raise exception
|
153
|
+
end
|
154
|
+
|
155
|
+
def translate_exception(e, message)
|
156
|
+
# override in derived class
|
157
|
+
raise "override in derived class"
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
class Resolver
|
163
|
+
attr_accessor :db, :adapter_method
|
164
|
+
|
165
|
+
def initialize(db_config)
|
166
|
+
@db = Skiima.symbolize_keys(db_config)
|
167
|
+
adapter_specified?
|
168
|
+
load_adapter
|
169
|
+
@adapter_method = "#{db[:adapter]}_connection"
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def adapter_specified?
|
175
|
+
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless db.key?(:adapter)
|
176
|
+
end
|
177
|
+
|
178
|
+
def load_adapter
|
179
|
+
begin
|
180
|
+
require "skiima/db_adapters/#{db[:adapter]}_adapter"
|
181
|
+
rescue => e
|
182
|
+
raise LoadError, "Adapter does not exist: #{db[:adapter]} - (#{e.message})", e.backtrace
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|