skiima 0.1.000 → 0.2.2

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