skiima 0.1.000

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +20 -0
  2. data/.travis.yml +8 -0
  3. data/CHANGELOG +8 -0
  4. data/Gemfile +30 -0
  5. data/Guardfile +14 -0
  6. data/README.md +87 -0
  7. data/Rakefile +56 -0
  8. data/lib/skiima/db_adapters/base_mysql_adapter.rb +308 -0
  9. data/lib/skiima/db_adapters/mysql2_adapter.rb +114 -0
  10. data/lib/skiima/db_adapters/mysql_adapter.rb +287 -0
  11. data/lib/skiima/db_adapters/postgresql_adapter.rb +509 -0
  12. data/lib/skiima/db_adapters.rb +187 -0
  13. data/lib/skiima/dependency.rb +84 -0
  14. data/lib/skiima/locales/en.yml +20 -0
  15. data/lib/skiima/locales/fr.yml +2 -0
  16. data/lib/skiima/version.rb +4 -0
  17. data/lib/skiima.rb +270 -0
  18. data/lib/skiima_helpers.rb +49 -0
  19. data/skiima.gemspec +53 -0
  20. data/spec/config/database.yml +56 -0
  21. data/spec/db/skiima/depends.yml +61 -0
  22. data/spec/db/skiima/empty_depends.yml +0 -0
  23. data/spec/db/skiima/init_test_db/database.skiima_test.mysql.current.sql +2 -0
  24. data/spec/db/skiima/init_test_db/database.skiima_test.postgresql.current.sql +2 -0
  25. data/spec/db/skiima/test_column_names/table.test_column_names.mysql.current.sql +8 -0
  26. data/spec/db/skiima/test_column_names/table.test_column_names.postgresql.current.sql +18 -0
  27. data/spec/db/skiima/test_index/index.test_index.mysql.current.sql +2 -0
  28. data/spec/db/skiima/test_index/index.test_index.postgresql.current.sql +2 -0
  29. data/spec/db/skiima/test_proc/proc.test_proc.mysql.current.sql +8 -0
  30. data/spec/db/skiima/test_proc/proc.test_proc_drop.mysql.current.drop.sql +1 -0
  31. data/spec/db/skiima/test_proc/proc.test_proc_drop.mysql.current.sql +8 -0
  32. data/spec/db/skiima/test_rule/rule.test_rule.postgresql.current.sql +6 -0
  33. data/spec/db/skiima/test_schema/schema.test_schema.postgresql.current.sql +2 -0
  34. data/spec/db/skiima/test_table/table.test_table.mysql.current.sql +7 -0
  35. data/spec/db/skiima/test_table/table.test_table.postgresql.current.sql +4 -0
  36. data/spec/db/skiima/test_view/view.test_view.mysql.current.sql +5 -0
  37. data/spec/db/skiima/test_view/view.test_view.postgresql.current.sql +6 -0
  38. data/spec/helpers/mysql_spec_helper.rb +0 -0
  39. data/spec/helpers/postgresql_spec_helper.rb +11 -0
  40. data/spec/mysql2_spec.rb +57 -0
  41. data/spec/mysql_spec.rb +100 -0
  42. data/spec/postgresql_spec.rb +138 -0
  43. data/spec/skiima/db_adapters/mysql_adapter_spec.rb +38 -0
  44. data/spec/skiima/db_adapters/postgresql_adapter_spec.rb +20 -0
  45. data/spec/skiima/db_adapters_spec.rb +31 -0
  46. data/spec/skiima/dependency_spec.rb +94 -0
  47. data/spec/skiima_spec.rb +97 -0
  48. data/spec/spec_helper.rb +35 -0
  49. 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