skiima 0.1.000 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +11 -3
  4. data/Gemfile +12 -6
  5. data/Guardfile +13 -11
  6. data/LICENSE +20 -0
  7. data/Procfile.example +2 -0
  8. data/README.md +170 -23
  9. data/Rakefile +26 -22
  10. data/lib/skiima.rb +61 -240
  11. data/lib/skiima/config.rb +60 -0
  12. data/lib/skiima/config/struct.rb +87 -0
  13. data/lib/skiima/db/connector.rb +195 -0
  14. data/lib/skiima/db/connector/active_record.rb +11 -0
  15. data/lib/skiima/db/connector/active_record/base_connector.rb +34 -0
  16. data/lib/skiima/db/connector/active_record/mysql2_connector.rb +147 -0
  17. data/lib/skiima/db/connector/active_record/mysql_connector.rb +177 -0
  18. data/lib/skiima/db/connector/active_record/postgresql_connector.rb +39 -0
  19. data/lib/skiima/db/helpers/mysql.rb +230 -0
  20. data/lib/skiima/db/helpers/postgresql.rb +221 -0
  21. data/lib/skiima/db/resolver.rb +62 -0
  22. data/lib/skiima/dependency/reader.rb +55 -0
  23. data/lib/skiima/dependency/script.rb +63 -0
  24. data/lib/skiima/i18n.rb +24 -0
  25. data/lib/skiima/loader.rb +108 -0
  26. data/lib/skiima/locales/en.yml +2 -2
  27. data/lib/skiima/logger.rb +54 -0
  28. data/lib/skiima/railtie.rb +10 -0
  29. data/lib/skiima/railties/skiima.rake +31 -0
  30. data/lib/skiima/version.rb +2 -2
  31. data/skiima.gemspec +5 -5
  32. data/spec/config/{database.yml → database.yml.example} +16 -0
  33. data/spec/config/database.yml.travis +69 -0
  34. data/spec/db/skiima/{depends.yml → dependencies.yml} +7 -2
  35. data/spec/db/skiima/{empty_depends.yml → empty_dependencies.yml} +0 -0
  36. data/spec/db/skiima/init_test_db/database.skiima_test.mysql.current.sql +7 -0
  37. data/spec/db/skiima/init_test_db/database.skiima_test.postgresql.current.sql +7 -0
  38. data/spec/mysql2_spec.rb +61 -12
  39. data/spec/mysql_spec.rb +66 -27
  40. data/spec/postgresql_spec.rb +55 -34
  41. data/spec/shared_examples/config_shared_example.rb +40 -0
  42. data/spec/skiima/config/struct_spec.rb +78 -0
  43. data/spec/skiima/config_spec.rb +6 -0
  44. data/spec/skiima/db/connector/active_record/base_connector_spec.rb +0 -0
  45. data/spec/skiima/db/connector/active_record/mysql2_connector_spec.rb +3 -0
  46. data/spec/skiima/db/connector/active_record/mysql_connector_spec.rb +3 -0
  47. data/spec/skiima/db/connector/active_record/postgresql_connector_spec.rb +7 -0
  48. data/spec/skiima/db/connector_spec.rb +6 -0
  49. data/spec/skiima/db/resolver_spec.rb +54 -0
  50. data/spec/skiima/dependency/reader_spec.rb +52 -0
  51. data/spec/skiima/{dependency_spec.rb → dependency/script_spec.rb} +3 -41
  52. data/spec/skiima/i18n_spec.rb +29 -0
  53. data/spec/skiima/loader_spec.rb +102 -0
  54. data/spec/skiima/logger_spec.rb +0 -0
  55. data/spec/skiima_spec.rb +43 -64
  56. data/spec/spec_helper.rb +38 -4
  57. metadata +144 -100
  58. data/lib/skiima/db_adapters.rb +0 -187
  59. data/lib/skiima/db_adapters/base_mysql_adapter.rb +0 -308
  60. data/lib/skiima/db_adapters/mysql2_adapter.rb +0 -114
  61. data/lib/skiima/db_adapters/mysql_adapter.rb +0 -287
  62. data/lib/skiima/db_adapters/postgresql_adapter.rb +0 -509
  63. data/lib/skiima/dependency.rb +0 -84
  64. data/lib/skiima_helpers.rb +0 -49
  65. data/spec/skiima/db_adapters/mysql_adapter_spec.rb +0 -38
  66. data/spec/skiima/db_adapters/postgresql_adapter_spec.rb +0 -20
  67. data/spec/skiima/db_adapters_spec.rb +0 -31
@@ -1,114 +0,0 @@
1
- # encoding: utf-8
2
- require 'skiima/db_adapters/base_mysql_adapter'
3
-
4
- gem 'mysql2', '~> 0.3.10'
5
- require 'mysql2'
6
-
7
- module Skiima
8
- def self.mysql2_connection(logger, config)
9
- config[:username] = 'root' if config[:username].nil?
10
-
11
- if Mysql2::Client.const_defined? :FOUND_ROWS
12
- config[:flags] = Mysql2::Client::FOUND_ROWS
13
- end
14
-
15
- client = Mysql2::Client.new(Skiima.symbolize_keys(config))
16
- options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
17
- Skiima::DbAdapters::Mysql2Adapter.new(client, logger, options, config)
18
- end
19
-
20
- module DbAdapters
21
- class Mysql2Adapter < BaseMysqlAdapter
22
- ADAPTER_NAME = 'Mysql2'
23
-
24
- def initialize(connection, logger, connection_options, config)
25
- super
26
- configure_connection
27
- end
28
-
29
- def supports_explain?
30
- true
31
- end
32
-
33
- def error_number(exception)
34
- exception.error_number if exception.respond_to?(:error_number)
35
- end
36
-
37
- def active?
38
- return false unless @connection
39
- @connection.ping
40
- end
41
-
42
- def reconnect!
43
- disconnect!
44
- connect
45
- end
46
-
47
- # Disconnects from the database if already connected.
48
- # Otherwise, this method does nothing.
49
- def disconnect!
50
- unless @connection.nil?
51
- @connection.close
52
- @connection = nil
53
- end
54
- end
55
-
56
- def reset!
57
- disconnect!
58
- connect
59
- end
60
-
61
- def execute(sql, name = nil)
62
- # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
63
- # made since we established the connection
64
- # @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
65
-
66
- # relying on formatting inside the file is precisely what i wanted to avoid...
67
- results = sql.split(/^--={4,}/).map do |spider_monkey|
68
- super(spider_monkey)
69
- end
70
-
71
- results.first
72
- end
73
-
74
- def exec_query(sql, name = 'SQL', binds = [])
75
- result = execute(sql, name)
76
- ActiveRecord::Result.new(result.fields, result.to_a)
77
- end
78
-
79
- alias exec_without_stmt exec_query
80
-
81
- private
82
-
83
- def connect
84
- @connection = Mysql2::Client.new(@config)
85
- configure_connection
86
- end
87
-
88
- def configure_connection
89
- @connection.query_options.merge!(:as => :array)
90
-
91
- variable_assignments = get_var_assignments
92
- execute("SET #{variable_assignments.join(', ')}", :skip_logging)
93
-
94
- version
95
- end
96
-
97
- def get_var_assignments
98
- # By default, MySQL 'where id is null' selects the last inserted id.
99
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
100
- variable_assignments = ['SQL_AUTO_IS_NULL=0']
101
- encoding = @config[:encoding]
102
-
103
- # make sure we set the encoding
104
- variable_assignments << "NAMES '#{encoding}'" if encoding
105
-
106
- # increase timeout so mysql server doesn't disconnect us
107
- wait_timeout = @config[:wait_timeout]
108
- wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
109
- variable_assignments << "@@wait_timeout = #{wait_timeout}"
110
- end
111
-
112
- end
113
- end
114
- end
@@ -1,287 +0,0 @@
1
- # encoding: utf-8
2
- require 'skiima/db_adapters/base_mysql_adapter'
3
-
4
- gem 'mysql', '~> 2.8.1'
5
- require 'mysql'
6
-
7
- class Mysql
8
- class Time
9
- ###
10
- # This monkey patch is for test_additional_columns_from_join_table
11
- def to_date
12
- Date.new(year, month, day)
13
- end
14
- end
15
- class Stmt; include Enumerable end
16
- class Result; include Enumerable end
17
- end
18
-
19
- module Skiima
20
- # Establishes a connection to the database that's used by all Active Record objects.
21
- def self.mysql_connection(logger, config) # :nodoc:
22
- config = Skiima.symbolize_keys(config)
23
- host = config[:host]
24
- port = config[:port]
25
- socket = config[:socket]
26
- username = config[:username] ? config[:username].to_s : 'root'
27
- password = config[:password].to_s
28
- database = config[:database]
29
-
30
- mysql = Mysql.init
31
- mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
32
-
33
- default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
34
- default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
35
-
36
- options = [host, username, password, database, port, socket, default_flags]
37
- Skiima::DbAdapters::MysqlAdapter.new(mysql, logger, options, config)
38
- end
39
-
40
- module DbAdapters
41
- class MysqlAdapter < BaseMysqlAdapter
42
- attr_accessor :client_encoding
43
-
44
- ADAPTER_NAME = 'MySQL'
45
-
46
- def initialize(connection, logger, connection_options, config)
47
- super
48
- @client_encoding = nil
49
- connect
50
- end
51
-
52
- # # Returns the version of the connected MySQL server.
53
- def version
54
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
55
- end
56
-
57
- # # Returns true, since this connection adapter supports prepared statement
58
- # # caching.
59
- def supports_statement_cache?
60
- true
61
- end
62
-
63
- def error_number(exception) # :nodoc:
64
- exception.errno if exception.respond_to?(:errno)
65
- end
66
-
67
- def type_cast(value, column)
68
- return super unless value == true || value == false
69
-
70
- value ? 1 : 0
71
- end
72
-
73
- def quote_string(string) #:nodoc:
74
- @connection.quote(string)
75
- end
76
-
77
- def active?
78
- if @connection.respond_to?(:stat)
79
- @connection.stat
80
- else
81
- @connection.query 'select 1'
82
- end
83
-
84
- # mysql-ruby doesn't raise an exception when stat fails.
85
- if @connection.respond_to?(:errno)
86
- @connection.errno.zero?
87
- else
88
- true
89
- end
90
- rescue Mysql::Error
91
- false
92
- end
93
-
94
- def reconnect!
95
- disconnect!
96
- clear_cache!
97
- connect
98
- end
99
-
100
- # Disconnects from the database if already connected. Otherwise, this
101
- # method does nothing.
102
- def disconnect!
103
- @connection.close rescue nil
104
- end
105
-
106
- def reset!
107
- if @connection.respond_to?(:change_user)
108
- # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
109
- # reset the connection is to change the user to the same user.
110
- @connection.change_user(@config[:username], @config[:password], @config[:database])
111
- configure_connection
112
- end
113
- end
114
-
115
- if "<3".respond_to?(:encode)
116
- # Taken from here:
117
- # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
118
- # Author: TOMITA Masahiro <tommy@tmtm.org>
119
- ENCODINGS = {
120
- "armscii8" => nil,
121
- "ascii" => Encoding::US_ASCII,
122
- "big5" => Encoding::Big5,
123
- "binary" => Encoding::ASCII_8BIT,
124
- "cp1250" => Encoding::Windows_1250,
125
- "cp1251" => Encoding::Windows_1251,
126
- "cp1256" => Encoding::Windows_1256,
127
- "cp1257" => Encoding::Windows_1257,
128
- "cp850" => Encoding::CP850,
129
- "cp852" => Encoding::CP852,
130
- "cp866" => Encoding::IBM866,
131
- "cp932" => Encoding::Windows_31J,
132
- "dec8" => nil,
133
- "eucjpms" => Encoding::EucJP_ms,
134
- "euckr" => Encoding::EUC_KR,
135
- "gb2312" => Encoding::EUC_CN,
136
- "gbk" => Encoding::GBK,
137
- "geostd8" => nil,
138
- "greek" => Encoding::ISO_8859_7,
139
- "hebrew" => Encoding::ISO_8859_8,
140
- "hp8" => nil,
141
- "keybcs2" => nil,
142
- "koi8r" => Encoding::KOI8_R,
143
- "koi8u" => Encoding::KOI8_U,
144
- "latin1" => Encoding::ISO_8859_1,
145
- "latin2" => Encoding::ISO_8859_2,
146
- "latin5" => Encoding::ISO_8859_9,
147
- "latin7" => Encoding::ISO_8859_13,
148
- "macce" => Encoding::MacCentEuro,
149
- "macroman" => Encoding::MacRoman,
150
- "sjis" => Encoding::SHIFT_JIS,
151
- "swe7" => nil,
152
- "tis620" => Encoding::TIS_620,
153
- "ucs2" => Encoding::UTF_16BE,
154
- "ujis" => Encoding::EucJP_ms,
155
- "utf8" => Encoding::UTF_8,
156
- "utf8mb4" => Encoding::UTF_8,
157
- }
158
- else
159
- ENCODINGS = Hash.new { |h,k| h[k] = k }
160
- end
161
-
162
- # Get the client encoding for this database
163
- def client_encoding
164
- return @client_encoding if @client_encoding
165
-
166
- result = exec_query(
167
- "SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
168
- 'SCHEMA')
169
- @client_encoding = ENCODINGS[result.last.last]
170
- end
171
-
172
- def execute(sql, name = nil)
173
- # this is nauseating. i can't believe it's an issue to run multiple statements
174
- # tried using OPTION_MULTI_STATEMENTS_ON, but ruby segfaulted. womp womp.
175
- # my workaround is messy for now
176
-
177
- # relying on formatting inside the file is precisely what i wanted to avoid...
178
- results = sql.split(/^--={4,}/).map do |spider_monkey|
179
- super(spider_monkey)
180
- end
181
-
182
- results.first
183
- end
184
-
185
- def exec_query(sql, name = 'SQL')
186
- log(sql, name) do
187
- exec_stmt(sql, name) do |cols, stmt|
188
- stmt.to_a
189
- end
190
- end
191
- end
192
-
193
- # def exec_without_stmt(sql, name = 'SQL') # :nodoc:
194
- # # Some queries, like SHOW CREATE TABLE don't work through the prepared
195
- # # statement API. For those queries, we need to use this method. :'(
196
- # log(sql, name) do
197
- # result = @connection.query(sql)
198
- # cols = []
199
- # rows = []
200
-
201
- # if result
202
- # cols = result.fetch_fields.map { |field| field.name }
203
- # rows = result.to_a
204
- # result.free
205
- # end
206
- # ActiveRecord::Result.new(cols, rows)
207
- # end
208
- # end
209
-
210
- def execute_and_free(sql, name = nil)
211
- result = execute(sql, name)
212
- ret = yield result
213
- result.free
214
- ret
215
- end
216
-
217
- def begin_db_transaction #:nodoc:
218
- exec_without_stmt "BEGIN"
219
- rescue Mysql::Error
220
- # Transactions aren't supported
221
- end
222
-
223
- private
224
-
225
- def exec_stmt(sql, name)
226
- stmt = @connection.prepare(sql)
227
-
228
- begin
229
- stmt.execute
230
- rescue Mysql::Error => e
231
- # Older versions of MySQL leave the prepared statement in a bad
232
- # place when an error occurs. To support older mysql versions, we
233
- # need to close the statement and delete the statement from the
234
- # cache.
235
- stmt.close
236
- raise e
237
- end
238
-
239
- cols = nil
240
- if metadata = stmt.result_metadata
241
- cols = metadata.fetch_fields.map { |field| field.name }
242
- end
243
-
244
- result = yield [cols, stmt]
245
-
246
- stmt.result_metadata.free if cols
247
- stmt.free_result
248
- stmt.close
249
-
250
- result
251
- end
252
-
253
- def connect
254
- encoding = @config[:encoding]
255
- if encoding
256
- @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
257
- end
258
-
259
- if @config[:sslca] || @config[:sslkey]
260
- @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
261
- end
262
-
263
- @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
264
- @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
265
- @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
266
-
267
- @connection.real_connect(*@connection_options)
268
-
269
- # reconnect must be set after real_connect is called, because real_connect sets it to false internally
270
- @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
271
-
272
- version #gets version
273
-
274
- configure_connection
275
- end
276
-
277
- def configure_connection
278
- encoding = @config[:encoding]
279
- execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
280
-
281
- # By default, MySQL 'where id is null' selects the last inserted id.
282
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
283
- execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
284
- end
285
- end
286
- end
287
- end
@@ -1,509 +0,0 @@
1
- # encoding: utf-8
2
- gem 'pg', '~> 0.11'
3
- require 'pg'
4
-
5
- module Skiima
6
- def self.postgresql_connection(logger, config) # :nodoc:
7
- config = Skiima.symbolize_keys(config)
8
- host = config[:host]
9
- port = config[:port] || 5432
10
- username = config[:username].to_s if config[:username]
11
- password = config[:password].to_s if config[:password]
12
-
13
- if config.key?(:database)
14
- database = config[:database]
15
- else
16
- raise ArgumentError, "No database specified. Missing argument: database."
17
- end
18
-
19
- # The postgres drivers don't allow the creation of an unconnected PGconn object,
20
- # so just pass a nil connection object for the time being.
21
- Skiima::DbAdapters::PostgresqlAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
22
- end
23
-
24
- module DbAdapters
25
-
26
- class PostgresqlAdapter < Base
27
- attr_accessor :version, :local_tz
28
-
29
- ADAPTER_NAME = 'PostgreSQL'
30
-
31
- NATIVE_DATABASE_TYPES = {
32
- :primary_key => "serial primary key",
33
- :string => { :name => "character varying", :limit => 255 },
34
- :text => { :name => "text" },
35
- :integer => { :name => "integer" },
36
- :float => { :name => "float" },
37
- :decimal => { :name => "decimal" },
38
- :datetime => { :name => "timestamp" },
39
- :timestamp => { :name => "timestamp" },
40
- :time => { :name => "time" },
41
- :date => { :name => "date" },
42
- :binary => { :name => "bytea" },
43
- :boolean => { :name => "boolean" },
44
- :xml => { :name => "xml" },
45
- :tsvector => { :name => "tsvector" }
46
- }
47
-
48
- MONEY_COLUMN_TYPE_OID = 790 # The internal PostgreSQL identifier of the money data type.
49
- BYTEA_COLUMN_TYPE_OID = 17 # The internal PostgreSQL identifier of the BYTEA data type.
50
-
51
- def adapter_name
52
- ADAPTER_NAME
53
- end
54
-
55
- # Initializes and connects a PostgreSQL adapter.
56
- def initialize(connection, logger, connection_parameters, config)
57
- super(connection, logger)
58
- @connection_parameters, @config = connection_parameters, config
59
- # @visitor = Arel::Visitors::PostgreSQL.new self
60
-
61
- # @local_tz is initialized as nil to avoid warnings when connect tries to use it
62
- @local_tz = nil
63
- @table_alias_length = nil
64
- @version = nil
65
-
66
- connect
67
- check_psql_version
68
- @local_tz = get_timezone
69
- end
70
-
71
- # Is this connection alive and ready for queries?
72
- def active?
73
- @connection.status == PGconn::CONNECTION_OK
74
- rescue PGError
75
- false
76
- end
77
-
78
- # Close then reopen the connection.
79
- def reconnect!
80
- clear_cache!
81
- @connection.reset
82
- configure_connection
83
- end
84
-
85
- def reset!
86
- clear_cache!
87
- super
88
- end
89
-
90
- # Disconnects from the database if already connected. Otherwise, this
91
- # method does nothing.
92
- def disconnect!
93
- clear_cache!
94
- @connection.close rescue nil
95
- end
96
-
97
- # Enable standard-conforming strings if available.
98
- def set_standard_conforming_strings
99
- old, self.client_min_messages = client_min_messages, 'panic'
100
- execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
101
- ensure
102
- self.client_min_messages = old
103
- end
104
-
105
- def supported_objects
106
- [:database, :schema, :table, :view, :rule, :index]
107
- end
108
-
109
- def database_exists?(name, opts = {})
110
- query(Skiima.interpolate_sql('&', <<-SQL, { :database => name }))[0][0].to_i > 0
111
- SELECT COUNT(*)
112
- FROM pg_databases pdb
113
- WHERE pdb.datname = '&database'
114
- SQL
115
- end
116
-
117
- def schema_exists?(name, opts = {})
118
- query(Skiima.interpolate_sql('&', <<-SQL, { :schema => name }))[0][0].to_i > 0
119
- SELECT COUNT(*)
120
- FROM pg_namespace
121
- WHERE nspname = '&schema'
122
- SQL
123
- end
124
-
125
- def table_exists?(name, opts = {})
126
- schema, table = Utils.extract_schema_and_table(name.to_s)
127
- vars = { :table => table,
128
- :schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
129
-
130
- vars.inspect
131
-
132
- query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
133
- SELECT COUNT(*)
134
- FROM pg_class c
135
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
136
- WHERE c.relkind in ('r')
137
- AND c.relname = '&table'
138
- AND n.nspname = &schema
139
- SQL
140
- end
141
-
142
- def view_exists?(name, opts = {})
143
- schema, view = Utils.extract_schema_and_table(name.to_s)
144
- vars = { :view => view,
145
- :schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
146
-
147
- query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
148
- SELECT COUNT(*)
149
- FROM pg_class c
150
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
151
- WHERE c.relkind in ('v')
152
- AND c.relname = '&view'
153
- AND n.nspname = &schema
154
- SQL
155
- end
156
-
157
- def rule_exists?(name, opts = {})
158
- target = opts[:attr] ? opts[:attr][0] : nil
159
- raise "requires target object" unless target
160
- schema, rule = Utils.extract_schema_and_table(name.to_s)
161
- vars = { :rule => rule,
162
- :target => target,
163
- :schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
164
-
165
- query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
166
- SELECT COUNT(*)
167
- FROM pg_rules pgr
168
- WHERE pgr.rulename = '&rule'
169
- AND pgr.tablename = '&target'
170
- AND pgr.schemaname = &schema
171
- SQL
172
- end
173
-
174
- def index_exists?(name, opts = {})
175
- target = opts[:attr] ? opts[:attr][0] : nil
176
- raise "requires target object" unless target
177
- schema, index = Utils.extract_schema_and_table(name.to_s)
178
- vars = { :index => index,
179
- :target => target,
180
- :schema => ((schema && !schema.empty?) ? "'#{schema}'" : "ANY (current_schemas(false))") }
181
-
182
- query(Skiima.interpolate_sql('&', <<-SQL, vars))[0][0].to_i > 0
183
- SELECT COUNT(*)
184
- FROM pg_indexes pgr
185
- WHERE pgr.indexname = '&index'
186
- AND pgr.tablename = '&target'
187
- AND pgr.schemaname = &schema
188
- SQL
189
- end
190
-
191
- def drop_database(name, opts = {})
192
- "DROP DATABASE IF EXISTS #{name}"
193
- end
194
-
195
- def drop_schema(name, opts = {})
196
- "DROP SCHEMA IF EXISTS #{name}"
197
- end
198
-
199
- def drop_table(name, opts = {})
200
- "DROP TABLE IF EXISTS #{name}"
201
- end
202
-
203
- def drop_view(name, opts = {})
204
- "DROP VIEW IF EXISTS #{name}"
205
- end
206
-
207
- def drop_rule(name, opts = {})
208
- target = opts[:attr].first if opts[:attr]
209
- raise "requires target object" unless target
210
-
211
- "DROP RULE IF EXISTS #{name} ON #{target}"
212
- end
213
-
214
- def drop_index(name, opts = {})
215
- "DROP INDEX IF EXISTS #{name}"
216
- end
217
-
218
- class PgColumn
219
- attr_accessor :name, :deafult, :type, :null
220
- def initialize(name, default = nil, type = nil, null = true)
221
- @name, @default, @type, @null = name, default, type, null
222
- end
223
-
224
- def to_s
225
- # to be implemented
226
- end
227
- end
228
-
229
- # Returns the list of a table's column names, data types, and default values.
230
- #
231
- # The underlying query is roughly:
232
- # SELECT column.name, column.type, default.value
233
- # FROM column LEFT JOIN default
234
- # ON column.table_id = default.table_id
235
- # AND column.num = default.column_num
236
- # WHERE column.table_id = get_table_id('table_name')
237
- # AND column.num > 0
238
- # AND NOT column.is_dropped
239
- # ORDER BY column.num
240
- #
241
- # If the table name is not prefixed with a schema, the database will
242
- # take the first match from the schema search path.
243
- #
244
- # Query implementation notes:
245
- # - format_type includes the column size constraint, e.g. varchar(50)
246
- # - ::regclass is a function that gives the id for a table name
247
- def column_definitions(table_name) #:nodoc:
248
- query(<<-SQL, 'SCHEMA')
249
- SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
250
- FROM pg_attribute a LEFT JOIN pg_attrdef d
251
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
252
- WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
253
- AND a.attnum > 0 AND NOT a.attisdropped
254
- ORDER BY a.attnum
255
- SQL
256
- end
257
-
258
- def column_names(table_name)
259
- cols = column_definitions(table_name).map do |c|
260
- PgColumn.new(c[0], c[1], c[2], c[3])
261
- end
262
- cols.map(&:name)
263
- end
264
-
265
- # Checks the following cases:
266
- #
267
- # - table_name
268
- # - "table.name"
269
- # - schema_name.table_name
270
- # - schema_name."table.name"
271
- # - "schema.name".table_name
272
- # - "schema.name"."table.name"
273
- def quote_table_name(name)
274
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
275
-
276
- unless name_part
277
- quote_column_name(schema)
278
- else
279
- table_name, name_part = extract_pg_identifier_from_name(name_part)
280
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
281
- end
282
- end
283
-
284
- # Quotes column names for use in SQL queries.
285
- def quote_column_name(name) #:nodoc:
286
- PGconn.quote_ident(name.to_s)
287
- end
288
-
289
- def extract_pg_identifier_from_name(name)
290
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
291
-
292
- if match_data
293
- rest = name[match_data[0].length, name.length]
294
- rest = rest[1, rest.length] if rest.start_with? "."
295
- [match_data[1], (rest.length > 0 ? rest : nil)]
296
- end
297
- end
298
-
299
- # Executes an SQL statement, returning a PGresult object on success
300
- # or raising a PGError exception otherwise.
301
- def execute(sql, name = nil)
302
- log(sql, name) do
303
- @connection.async_exec(sql)
304
- end
305
- end
306
-
307
- # Queries the database and returns the results in an Array-like object
308
- def query(sql, name = nil) #:nodoc:
309
- log(sql, name) do
310
- result_as_array @connection.async_exec(sql)
311
- end
312
- end
313
-
314
- # create a 2D array representing the result set
315
- def result_as_array(res) #:nodoc:
316
- # check if we have any binary column and if they need escaping
317
- ftypes = Array.new(res.nfields) do |i|
318
- [i, res.ftype(i)]
319
- end
320
-
321
- rows = res.values
322
- return rows unless ftypes.any? { |_, x|
323
- x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
324
- }
325
-
326
- typehash = ftypes.group_by { |_, type| type }
327
- binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
328
- monies = typehash[MONEY_COLUMN_TYPE_OID] || []
329
-
330
- rows.each do |row|
331
- # unescape string passed BYTEA field (OID == 17)
332
- binaries.each do |index, _|
333
- row[index] = unescape_bytea(row[index])
334
- end
335
-
336
- # If this is a money type column and there are any currency symbols,
337
- # then strip them off. Indeed it would be prettier to do this in
338
- # PostgreSQLColumn.string_to_decimal but would break form input
339
- # fields that call value_before_type_cast.
340
- monies.each do |index, _|
341
- data = row[index]
342
- # Because money output is formatted according to the locale, there are two
343
- # cases to consider (note the decimal separators):
344
- # (1) $12,345,678.12
345
- # (2) $12.345.678,12
346
- case data
347
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
348
- data.gsub!(/[^-\d.]/, '')
349
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
350
- data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
351
- end
352
- end
353
- end
354
- end
355
-
356
- # Set the authorized user for this session
357
- # def session_auth=(user)
358
- # clear_cache!
359
- # exec_query "SET SESSION AUTHORIZATION #{user}"
360
- # end
361
-
362
- # Begins a transaction.
363
- def begin_db_transaction
364
- execute "BEGIN"
365
- end
366
-
367
- # # Commits a transaction.
368
- def commit_db_transaction
369
- execute "COMMIT"
370
- end
371
-
372
- # # Aborts a transaction.
373
- def rollback_db_transaction
374
- execute "ROLLBACK"
375
- end
376
-
377
- def outside_transaction?
378
- @connection.transaction_status == PGconn::PQTRANS_IDLE
379
- end
380
-
381
- def create_savepoint
382
- execute("SAVEPOINT #{current_savepoint_name}")
383
- end
384
-
385
- def rollback_to_savepoint
386
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
387
- end
388
-
389
- def release_savepoint
390
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
391
- end
392
-
393
- # Returns the current database name.
394
- def current_database
395
- query('select current_database()')[0][0]
396
- end
397
-
398
- # # Returns the current schema name.
399
- def current_schema
400
- query('SELECT current_schema', 'SCHEMA')[0][0]
401
- end
402
-
403
- # Returns the current client message level.
404
- def client_min_messages
405
- query('SHOW client_min_messages', 'SCHEMA')[0][0]
406
- end
407
-
408
- # Set the client message level.
409
- def client_min_messages=(level)
410
- execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
411
- end
412
-
413
- # def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
414
- # need to be able to reset sequences?
415
-
416
- # Sets the schema search path to a string of comma-separated schema names.
417
- # Names beginning with $ have to be quoted (e.g. $user => '$user').
418
- # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
419
- #
420
- # This should be not be called manually but set in database.yml.
421
- def schema_search_path=(schema_csv)
422
- if schema_csv
423
- execute "SET search_path TO #{schema_csv}"
424
- @schema_search_path = schema_csv
425
- end
426
- end
427
-
428
- module Utils
429
- extend self
430
-
431
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
432
- # +schema_name+ is nil if not specified in +name+.
433
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
434
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
435
- #
436
- # * <tt>table_name</tt>
437
- # * <tt>"table.name"</tt>
438
- # * <tt>schema_name.table_name</tt>
439
- # * <tt>schema_name."table.name"</tt>
440
- # * <tt>"schema.name"."table name"</tt>
441
- def extract_schema_and_table(name)
442
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
443
- [schema, table]
444
- end
445
- end
446
-
447
- protected
448
-
449
- # Returns the version of the connected PostgreSQL server.
450
- def postgresql_version
451
- @connection.server_version
452
- end
453
-
454
- def check_psql_version
455
- @version = postgresql_version
456
- if @version < 80200
457
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
458
- end
459
- end
460
-
461
- def get_timezone
462
- execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
463
- end
464
-
465
- def translate_exception(e, message)
466
- e
467
- # case exception.message
468
- # when /duplicate key value violates unique constraint/
469
- # RecordNotUnique.new(message, exception)
470
- # when /violates foreign key constraint/
471
- # InvalidForeignKey.new(message, exception)
472
- # else
473
- # super
474
- # end
475
- end
476
-
477
- private
478
-
479
- # Connects to a PostgreSQL server and sets up the adapter depending on the
480
- # connected server's characteristics.
481
- def connect
482
- @connection = PGconn.connect(*@connection_parameters)
483
-
484
- # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
485
- # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
486
- # should know about this but can't detect it there, so deal with it here.
487
- # PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
488
-
489
- configure_connection
490
- end
491
-
492
- # Configures the encoding, verbosity, schema search path, and time zone of the connection.
493
- # This is called by #connect and should not be called manually.
494
- def configure_connection
495
- if @config[:encoding]
496
- @connection.set_client_encoding(@config[:encoding])
497
- end
498
- self.client_min_messages = @config[:min_messages] if @config[:min_messages]
499
- self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
500
-
501
- # Use standard-conforming strings if available so we don't have to do the E'...' dance.
502
- set_standard_conforming_strings
503
-
504
- #configure the connection to return TIMESTAMP WITH ZONE types in UTC.
505
- execute("SET time zone '#{@local_tz}'", 'SCHEMA') if @local_tz
506
- end
507
- end
508
- end
509
- end