skiima 0.1.000
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.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
|