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.
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