activerecord 3.2.13 → 3.2.14.rc1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG.md +148 -2
- data/lib/active_record/associations/association.rb +9 -3
- data/lib/active_record/associations/belongs_to_association.rb +1 -1
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
- data/lib/active_record/associations/builder/belongs_to.rb +4 -1
- data/lib/active_record/associations/builder/belongs_to.rb.orig +95 -0
- data/lib/active_record/associations/collection_association.rb +1 -1
- data/lib/active_record/associations/has_many_association.rb +1 -2
- data/lib/active_record/associations/has_many_association.rb.orig +116 -0
- data/lib/active_record/associations/join_dependency.rb +1 -1
- data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
- data/lib/active_record/associations/preloader/through_association.rb +1 -2
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/autosave_association.rb +7 -12
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb.orig +619 -0
- data/lib/active_record/connection_adapters/connection_specification.rb.orig +124 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -3
- data/lib/active_record/connection_adapters/postgresql/cast.rb.orig +136 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb.orig +485 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +9 -3
- data/lib/active_record/core.rb.orig +452 -0
- data/lib/active_record/explain_subscriber.rb +1 -1
- data/lib/active_record/model_schema.rb +4 -2
- data/lib/active_record/nested_attributes.rb +41 -17
- data/lib/active_record/railtie.rb +6 -7
- data/lib/active_record/railties/databases.rake +2 -1
- data/lib/active_record/relation/calculations.rb +5 -6
- data/lib/active_record/relation/calculations.rb.orig +378 -0
- data/lib/active_record/relation/finder_methods.rb +1 -0
- data/lib/active_record/relation/finder_methods.rb.orig +405 -0
- data/lib/active_record/relation/spawn_methods.rb +34 -3
- data/lib/active_record/store.rb +1 -1
- data/lib/active_record/version.rb +2 -2
- data/lib/rails/generators/active_record/observer/observer_generator.rb.orig +15 -0
- metadata +117 -70
- checksums.yaml +0 -7
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
class ConnectionSpecification #:nodoc:
|
6
|
+
attr_reader :config, :adapter_method
|
7
|
+
|
8
|
+
def initialize(config, adapter_method)
|
9
|
+
@config, @adapter_method = config, adapter_method
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize_dup(original)
|
13
|
+
@config = original.config.dup
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Builds a ConnectionSpecification from user input
|
18
|
+
class Resolver # :nodoc:
|
19
|
+
attr_reader :config, :klass, :configurations
|
20
|
+
|
21
|
+
def initialize(config, configurations)
|
22
|
+
@config = config
|
23
|
+
@configurations = configurations
|
24
|
+
end
|
25
|
+
|
26
|
+
def spec
|
27
|
+
case config
|
28
|
+
when nil
|
29
|
+
raise AdapterNotSpecified unless defined?(Rails.env)
|
30
|
+
resolve_string_connection Rails.env
|
31
|
+
when Symbol, String
|
32
|
+
resolve_string_connection config.to_s
|
33
|
+
when Hash
|
34
|
+
resolve_hash_connection config
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def resolve_string_connection(spec) # :nodoc:
|
40
|
+
hash = configurations.fetch(spec) do |k|
|
41
|
+
self.class.connection_url_to_hash(k)
|
42
|
+
end
|
43
|
+
|
44
|
+
raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
|
45
|
+
|
46
|
+
resolve_hash_connection hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve_hash_connection(spec) # :nodoc:
|
50
|
+
spec = spec.symbolize_keys
|
51
|
+
|
52
|
+
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
|
53
|
+
|
54
|
+
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
|
55
|
+
begin
|
56
|
+
require path_to_adapter
|
57
|
+
rescue Gem::LoadError => e
|
58
|
+
raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile."
|
59
|
+
rescue LoadError => e
|
60
|
+
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
|
61
|
+
end
|
62
|
+
|
63
|
+
adapter_method = "#{spec[:adapter]}_connection"
|
64
|
+
|
65
|
+
ConnectionSpecification.new(spec, adapter_method)
|
66
|
+
end
|
67
|
+
|
68
|
+
<<<<<<< HEAD
|
69
|
+
# For DATABASE_URL, accept a limited concept of ints and floats
|
70
|
+
SIMPLE_INT = /\A\d+\z/
|
71
|
+
SIMPLE_FLOAT = /\A\d+\.\d+\z/
|
72
|
+
|
73
|
+
def self.connection_url_to_hash(url) # :nodoc:
|
74
|
+
=======
|
75
|
+
def connection_url_to_hash(url) # :nodoc:
|
76
|
+
>>>>>>> parent of 4b005fb... DATABASE_URL parsing should turn numeric strings into numeric types, and
|
77
|
+
config = URI.parse url
|
78
|
+
adapter = config.scheme
|
79
|
+
adapter = "postgresql" if adapter == "postgres"
|
80
|
+
spec = { :adapter => adapter,
|
81
|
+
:username => config.user,
|
82
|
+
:password => config.password,
|
83
|
+
:port => config.port,
|
84
|
+
:database => config.path.sub(%r{^/},""),
|
85
|
+
:host => config.host }
|
86
|
+
|
87
|
+
spec.reject!{ |_,value| value.blank? }
|
88
|
+
|
89
|
+
uri_parser = URI::Parser.new
|
90
|
+
|
91
|
+
spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
|
92
|
+
|
93
|
+
if config.query
|
94
|
+
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
|
95
|
+
<<<<<<< HEAD
|
96
|
+
|
97
|
+
options.each { |key, value| options[key] = type_cast_value(value) }
|
98
|
+
|
99
|
+
=======
|
100
|
+
>>>>>>> parent of 4b005fb... DATABASE_URL parsing should turn numeric strings into numeric types, and
|
101
|
+
spec.merge!(options)
|
102
|
+
end
|
103
|
+
|
104
|
+
spec
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.type_cast_value(value)
|
108
|
+
case value
|
109
|
+
when SIMPLE_INT
|
110
|
+
value.to_i
|
111
|
+
when SIMPLE_FLOAT
|
112
|
+
value.to_f
|
113
|
+
when 'true'
|
114
|
+
true
|
115
|
+
when 'false'
|
116
|
+
false
|
117
|
+
else
|
118
|
+
value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -204,9 +204,11 @@ module ActiveRecord
|
|
204
204
|
|
205
205
|
# Executes the SQL statement in the context of this connection.
|
206
206
|
def execute(sql, name = nil)
|
207
|
-
|
208
|
-
|
209
|
-
|
207
|
+
if @connection
|
208
|
+
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
|
209
|
+
# made since we established the connection
|
210
|
+
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
|
211
|
+
end
|
210
212
|
|
211
213
|
super
|
212
214
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class PostgreSQLColumn < Column
|
4
|
+
module Cast
|
5
|
+
def string_to_time(string)
|
6
|
+
return string unless String === string
|
7
|
+
|
8
|
+
case string
|
9
|
+
when 'infinity'; 1.0 / 0.0
|
10
|
+
when '-infinity'; -1.0 / 0.0
|
11
|
+
when / BC$/
|
12
|
+
super("-" + string.sub(/ BC$/, ""))
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def hstore_to_string(object)
|
19
|
+
if Hash === object
|
20
|
+
object.map { |k,v|
|
21
|
+
"#{escape_hstore(k)}=>#{escape_hstore(v)}"
|
22
|
+
}.join ','
|
23
|
+
else
|
24
|
+
object
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def string_to_hstore(string)
|
29
|
+
if string.nil?
|
30
|
+
nil
|
31
|
+
elsif String === string
|
32
|
+
Hash[string.scan(HstorePair).map { |k,v|
|
33
|
+
v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
|
34
|
+
<<<<<<< HEAD
|
35
|
+
k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
|
36
|
+
=======
|
37
|
+
k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
|
38
|
+
>>>>>>> 33231b5... Fix regex to strip quotations from hstore values
|
39
|
+
[k,v]
|
40
|
+
}]
|
41
|
+
else
|
42
|
+
string
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def json_to_string(object)
|
47
|
+
if Hash === object
|
48
|
+
ActiveSupport::JSON.encode(object)
|
49
|
+
else
|
50
|
+
object
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def array_to_string(value, column, adapter, should_be_quoted = false)
|
55
|
+
casted_values = value.map do |val|
|
56
|
+
if String === val
|
57
|
+
if val == "NULL"
|
58
|
+
"\"#{val}\""
|
59
|
+
else
|
60
|
+
quote_and_escape(adapter.type_cast(val, column, true))
|
61
|
+
end
|
62
|
+
else
|
63
|
+
adapter.type_cast(val, column, true)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
"{#{casted_values.join(',')}}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def range_to_string(object)
|
70
|
+
from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
|
71
|
+
to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
|
72
|
+
"[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def string_to_json(string)
|
76
|
+
if String === string
|
77
|
+
ActiveSupport::JSON.decode(string)
|
78
|
+
else
|
79
|
+
string
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def string_to_cidr(string)
|
84
|
+
if string.nil?
|
85
|
+
nil
|
86
|
+
elsif String === string
|
87
|
+
IPAddr.new(string)
|
88
|
+
else
|
89
|
+
string
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def cidr_to_string(object)
|
94
|
+
if IPAddr === object
|
95
|
+
"#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
|
96
|
+
else
|
97
|
+
object
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def string_to_array(string, oid)
|
102
|
+
parse_pg_array(string).map{|val| oid.type_cast val}
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
HstorePair = begin
|
108
|
+
quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
|
109
|
+
unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
|
110
|
+
/(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
|
111
|
+
end
|
112
|
+
|
113
|
+
def escape_hstore(value)
|
114
|
+
if value.nil?
|
115
|
+
'NULL'
|
116
|
+
else
|
117
|
+
if value == ""
|
118
|
+
'""'
|
119
|
+
else
|
120
|
+
'"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def quote_and_escape(value)
|
126
|
+
case value
|
127
|
+
when "NULL"
|
128
|
+
value
|
129
|
+
else
|
130
|
+
"\"#{value.gsub(/"/,"\\\"")}\""
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,485 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class PostgreSQLAdapter < AbstractAdapter
|
4
|
+
class SchemaCreation < AbstractAdapter::SchemaCreation
|
5
|
+
private
|
6
|
+
|
7
|
+
def visit_AddColumn(o)
|
8
|
+
sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
|
9
|
+
sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}"
|
10
|
+
add_column_options!(sql, column_options(o))
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit_ColumnDefinition(o)
|
14
|
+
sql = super
|
15
|
+
if o.primary_key? && o.type == :uuid
|
16
|
+
sql << " PRIMARY KEY "
|
17
|
+
add_column_options!(sql, column_options(o))
|
18
|
+
end
|
19
|
+
sql
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_column_options!(sql, options)
|
23
|
+
if options[:array] || options[:column].try(:array)
|
24
|
+
sql << '[]'
|
25
|
+
end
|
26
|
+
|
27
|
+
column = options.fetch(:column) { return super }
|
28
|
+
if column.type == :uuid && options[:default] =~ /\(\)/
|
29
|
+
sql << " DEFAULT #{options[:default]}"
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def schema_creation
|
37
|
+
SchemaCreation.new self
|
38
|
+
end
|
39
|
+
|
40
|
+
module SchemaStatements
|
41
|
+
# Drops the database specified on the +name+ attribute
|
42
|
+
# and creates it again using the provided +options+.
|
43
|
+
def recreate_database(name, options = {}) #:nodoc:
|
44
|
+
drop_database(name)
|
45
|
+
create_database(name, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
|
49
|
+
# <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
|
50
|
+
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
|
51
|
+
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
|
52
|
+
#
|
53
|
+
# Example:
|
54
|
+
# create_database config[:database], config
|
55
|
+
# create_database 'foo_development', encoding: 'unicode'
|
56
|
+
def create_database(name, options = {})
|
57
|
+
options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
|
58
|
+
|
59
|
+
option_string = options.sum do |key, value|
|
60
|
+
case key
|
61
|
+
when :owner
|
62
|
+
" OWNER = \"#{value}\""
|
63
|
+
when :template
|
64
|
+
" TEMPLATE = \"#{value}\""
|
65
|
+
when :encoding
|
66
|
+
" ENCODING = '#{value}'"
|
67
|
+
when :collation
|
68
|
+
" LC_COLLATE = '#{value}'"
|
69
|
+
when :ctype
|
70
|
+
" LC_CTYPE = '#{value}'"
|
71
|
+
when :tablespace
|
72
|
+
" TABLESPACE = \"#{value}\""
|
73
|
+
when :connection_limit
|
74
|
+
" CONNECTION LIMIT = #{value}"
|
75
|
+
else
|
76
|
+
""
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Drops a PostgreSQL database.
|
84
|
+
#
|
85
|
+
# Example:
|
86
|
+
# drop_database 'matt_development'
|
87
|
+
def drop_database(name) #:nodoc:
|
88
|
+
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the list of all tables in the schema search path or a specified schema.
|
92
|
+
def tables(name = nil)
|
93
|
+
query(<<-SQL, 'SCHEMA').map { |row| row[0] }
|
94
|
+
SELECT tablename
|
95
|
+
FROM pg_tables
|
96
|
+
WHERE schemaname = ANY (current_schemas(false))
|
97
|
+
SQL
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if table exists.
|
101
|
+
# If the schema is not specified as part of +name+ then it will only find tables within
|
102
|
+
# the current schema search path (regardless of permissions to access tables in other schemas)
|
103
|
+
def table_exists?(name)
|
104
|
+
schema, table = Utils.extract_schema_and_table(name.to_s)
|
105
|
+
return false unless table
|
106
|
+
|
107
|
+
binds = [[nil, table]]
|
108
|
+
binds << [nil, schema] if schema
|
109
|
+
|
110
|
+
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
|
111
|
+
SELECT COUNT(*)
|
112
|
+
FROM pg_class c
|
113
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
114
|
+
WHERE c.relkind in ('v','r')
|
115
|
+
AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
|
116
|
+
AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
|
117
|
+
SQL
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns true if schema exists.
|
121
|
+
def schema_exists?(name)
|
122
|
+
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
|
123
|
+
SELECT COUNT(*)
|
124
|
+
FROM pg_namespace
|
125
|
+
WHERE nspname = '#{name}'
|
126
|
+
SQL
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns an array of indexes for the given table.
|
130
|
+
def indexes(table_name, name = nil)
|
131
|
+
result = query(<<-SQL, 'SCHEMA')
|
132
|
+
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
|
133
|
+
FROM pg_class t
|
134
|
+
INNER JOIN pg_index d ON t.oid = d.indrelid
|
135
|
+
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
136
|
+
WHERE i.relkind = 'i'
|
137
|
+
AND d.indisprimary = 'f'
|
138
|
+
AND t.relname = '#{table_name}'
|
139
|
+
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
|
140
|
+
ORDER BY i.relname
|
141
|
+
SQL
|
142
|
+
|
143
|
+
result.map do |row|
|
144
|
+
index_name = row[0]
|
145
|
+
unique = row[1] == 't'
|
146
|
+
indkey = row[2].split(" ")
|
147
|
+
inddef = row[3]
|
148
|
+
oid = row[4]
|
149
|
+
|
150
|
+
columns = Hash[query(<<-SQL, "SCHEMA")]
|
151
|
+
SELECT a.attnum, a.attname
|
152
|
+
FROM pg_attribute a
|
153
|
+
WHERE a.attrelid = #{oid}
|
154
|
+
AND a.attnum IN (#{indkey.join(",")})
|
155
|
+
SQL
|
156
|
+
|
157
|
+
column_names = columns.values_at(*indkey).compact
|
158
|
+
|
159
|
+
unless column_names.empty?
|
160
|
+
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
|
161
|
+
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
|
162
|
+
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
|
163
|
+
where = inddef.scan(/WHERE (.+)$/).flatten[0]
|
164
|
+
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
|
165
|
+
|
166
|
+
IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
|
167
|
+
end
|
168
|
+
end.compact
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns the list of all column definitions for a table.
|
172
|
+
def columns(table_name)
|
173
|
+
# Limit, precision, and scale are all handled by the superclass.
|
174
|
+
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
|
175
|
+
oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
|
176
|
+
OID::Identity.new
|
177
|
+
}
|
178
|
+
PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Returns the current database name.
|
183
|
+
def current_database
|
184
|
+
query('select current_database()', 'SCHEMA')[0][0]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns the current schema name.
|
188
|
+
def current_schema
|
189
|
+
query('SELECT current_schema', 'SCHEMA')[0][0]
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns the current database encoding format.
|
193
|
+
def encoding
|
194
|
+
query(<<-end_sql, 'SCHEMA')[0][0]
|
195
|
+
SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
|
196
|
+
WHERE pg_database.datname LIKE '#{current_database}'
|
197
|
+
end_sql
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns the current database collation.
|
201
|
+
def collation
|
202
|
+
query(<<-end_sql, 'SCHEMA')[0][0]
|
203
|
+
SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
|
204
|
+
end_sql
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the current database ctype.
|
208
|
+
def ctype
|
209
|
+
query(<<-end_sql, 'SCHEMA')[0][0]
|
210
|
+
SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
|
211
|
+
end_sql
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns an array of schema names.
|
215
|
+
def schema_names
|
216
|
+
query(<<-SQL, 'SCHEMA').flatten
|
217
|
+
SELECT nspname
|
218
|
+
FROM pg_namespace
|
219
|
+
WHERE nspname !~ '^pg_.*'
|
220
|
+
AND nspname NOT IN ('information_schema')
|
221
|
+
ORDER by nspname;
|
222
|
+
SQL
|
223
|
+
end
|
224
|
+
|
225
|
+
# Creates a schema for the given schema name.
|
226
|
+
def create_schema schema_name
|
227
|
+
execute "CREATE SCHEMA #{schema_name}"
|
228
|
+
end
|
229
|
+
|
230
|
+
# Drops the schema for the given schema name.
|
231
|
+
def drop_schema schema_name
|
232
|
+
execute "DROP SCHEMA #{schema_name} CASCADE"
|
233
|
+
end
|
234
|
+
|
235
|
+
# Sets the schema search path to a string of comma-separated schema names.
|
236
|
+
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
237
|
+
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
|
238
|
+
#
|
239
|
+
# This should be not be called manually but set in database.yml.
|
240
|
+
def schema_search_path=(schema_csv)
|
241
|
+
if schema_csv
|
242
|
+
execute("SET search_path TO #{schema_csv}", 'SCHEMA')
|
243
|
+
@schema_search_path = schema_csv
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns the active schema search path.
|
248
|
+
def schema_search_path
|
249
|
+
@schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns the current client message level.
|
253
|
+
def client_min_messages
|
254
|
+
query('SHOW client_min_messages', 'SCHEMA')[0][0]
|
255
|
+
end
|
256
|
+
|
257
|
+
# Set the client message level.
|
258
|
+
def client_min_messages=(level)
|
259
|
+
execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
|
260
|
+
end
|
261
|
+
|
262
|
+
# Returns the sequence name for a table's primary key or some other specified key.
|
263
|
+
def default_sequence_name(table_name, pk = nil) #:nodoc:
|
264
|
+
result = serial_sequence(table_name, pk || 'id')
|
265
|
+
return nil unless result
|
266
|
+
result.split('.').last
|
267
|
+
rescue ActiveRecord::StatementInvalid
|
268
|
+
"#{table_name}_#{pk || 'id'}_seq"
|
269
|
+
end
|
270
|
+
|
271
|
+
def serial_sequence(table, column)
|
272
|
+
result = exec_query(<<-eosql, 'SCHEMA')
|
273
|
+
SELECT pg_get_serial_sequence('#{table}', '#{column}')
|
274
|
+
eosql
|
275
|
+
result.rows.first.first
|
276
|
+
end
|
277
|
+
|
278
|
+
# Resets the sequence of a table's primary key to the maximum value.
|
279
|
+
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
280
|
+
unless pk and sequence
|
281
|
+
default_pk, default_sequence = pk_and_sequence_for(table)
|
282
|
+
|
283
|
+
pk ||= default_pk
|
284
|
+
sequence ||= default_sequence
|
285
|
+
end
|
286
|
+
|
287
|
+
if @logger && pk && !sequence
|
288
|
+
@logger.warn "#{table} has primary key #{pk} with no default sequence"
|
289
|
+
end
|
290
|
+
|
291
|
+
if pk && sequence
|
292
|
+
quoted_sequence = quote_table_name(sequence)
|
293
|
+
|
294
|
+
select_value <<-end_sql, 'SCHEMA'
|
295
|
+
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
|
296
|
+
end_sql
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns a table's primary key and belonging sequence.
|
301
|
+
def pk_and_sequence_for(table) #:nodoc:
|
302
|
+
# First try looking for a sequence with a dependency on the
|
303
|
+
# given table's primary key.
|
304
|
+
result = query(<<-end_sql, 'SCHEMA')[0]
|
305
|
+
SELECT attr.attname, seq.relname
|
306
|
+
FROM pg_class seq,
|
307
|
+
pg_attribute attr,
|
308
|
+
pg_depend dep,
|
309
|
+
pg_constraint cons
|
310
|
+
WHERE seq.oid = dep.objid
|
311
|
+
AND seq.relkind = 'S'
|
312
|
+
AND attr.attrelid = dep.refobjid
|
313
|
+
AND attr.attnum = dep.refobjsubid
|
314
|
+
AND attr.attrelid = cons.conrelid
|
315
|
+
AND attr.attnum = cons.conkey[1]
|
316
|
+
AND cons.contype = 'p'
|
317
|
+
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
|
318
|
+
end_sql
|
319
|
+
|
320
|
+
if result.nil? or result.empty?
|
321
|
+
result = query(<<-end_sql, 'SCHEMA')[0]
|
322
|
+
SELECT attr.attname,
|
323
|
+
CASE
|
324
|
+
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
|
325
|
+
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
|
326
|
+
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
|
327
|
+
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
|
328
|
+
ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
|
329
|
+
END
|
330
|
+
FROM pg_class t
|
331
|
+
JOIN pg_attribute attr ON (t.oid = attrelid)
|
332
|
+
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
|
333
|
+
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
|
334
|
+
WHERE t.oid = '#{quote_table_name(table)}'::regclass
|
335
|
+
AND cons.contype = 'p'
|
336
|
+
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
|
337
|
+
end_sql
|
338
|
+
end
|
339
|
+
|
340
|
+
[result.first, result.last]
|
341
|
+
rescue
|
342
|
+
nil
|
343
|
+
end
|
344
|
+
|
345
|
+
# Returns just a table's primary key
|
346
|
+
def primary_key(table)
|
347
|
+
row = exec_query(<<-end_sql, 'SCHEMA').rows.first
|
348
|
+
SELECT attr.attname
|
349
|
+
FROM pg_attribute attr
|
350
|
+
INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
|
351
|
+
WHERE cons.contype = 'p'
|
352
|
+
AND cons.conrelid = '#{quote_table_name(table)}'::regclass
|
353
|
+
end_sql
|
354
|
+
|
355
|
+
row && row.first
|
356
|
+
end
|
357
|
+
|
358
|
+
# Renames a table.
|
359
|
+
# Also renames a table's primary key sequence if the sequence name matches the
|
360
|
+
# Active Record default.
|
361
|
+
#
|
362
|
+
# Example:
|
363
|
+
# rename_table('octopuses', 'octopi')
|
364
|
+
def rename_table(table_name, new_name)
|
365
|
+
clear_cache!
|
366
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
|
367
|
+
pk, seq = pk_and_sequence_for(new_name)
|
368
|
+
if seq == "#{table_name}_#{pk}_seq"
|
369
|
+
new_seq = "#{new_name}_#{pk}_seq"
|
370
|
+
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
|
371
|
+
end
|
372
|
+
|
373
|
+
rename_table_indexes(table_name, new_name)
|
374
|
+
end
|
375
|
+
|
376
|
+
# Adds a new column to the named table.
|
377
|
+
# See TableDefinition#column for details of the options you can use.
|
378
|
+
def add_column(table_name, column_name, type, options = {})
|
379
|
+
clear_cache!
|
380
|
+
super
|
381
|
+
end
|
382
|
+
|
383
|
+
# Changes the column of a table.
|
384
|
+
def change_column(table_name, column_name, type, options = {})
|
385
|
+
clear_cache!
|
386
|
+
quoted_table_name = quote_table_name(table_name)
|
387
|
+
|
388
|
+
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
389
|
+
|
390
|
+
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
|
391
|
+
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
|
392
|
+
end
|
393
|
+
|
394
|
+
# Changes the default value of a table column.
|
395
|
+
def change_column_default(table_name, column_name, default)
|
396
|
+
clear_cache!
|
397
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
|
398
|
+
end
|
399
|
+
|
400
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
401
|
+
clear_cache!
|
402
|
+
unless null || default.nil?
|
403
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
404
|
+
end
|
405
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
|
406
|
+
end
|
407
|
+
|
408
|
+
# Renames a column in a table.
|
409
|
+
def rename_column(table_name, column_name, new_column_name)
|
410
|
+
clear_cache!
|
411
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
412
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
413
|
+
end
|
414
|
+
|
415
|
+
def add_index(table_name, column_name, options = {}) #:nodoc:
|
416
|
+
index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
|
417
|
+
execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
|
418
|
+
end
|
419
|
+
|
420
|
+
def remove_index!(table_name, index_name) #:nodoc:
|
421
|
+
execute "DROP INDEX #{quote_table_name(index_name)}"
|
422
|
+
end
|
423
|
+
|
424
|
+
def rename_index(table_name, old_name, new_name)
|
425
|
+
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
|
426
|
+
end
|
427
|
+
|
428
|
+
def index_name_length
|
429
|
+
63
|
430
|
+
end
|
431
|
+
|
432
|
+
# Maps logical Rails types to PostgreSQL-specific data types.
|
433
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
434
|
+
case type.to_s
|
435
|
+
when 'binary'
|
436
|
+
# PostgreSQL doesn't support limits on binary (bytea) columns.
|
437
|
+
# The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
|
438
|
+
case limit
|
439
|
+
when nil, 0..0x3fffffff; super(type)
|
440
|
+
else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
|
441
|
+
end
|
442
|
+
when 'text'
|
443
|
+
# PostgreSQL doesn't support limits on text columns.
|
444
|
+
# The hard limit is 1Gb, according to section 8.3 in the manual.
|
445
|
+
case limit
|
446
|
+
when nil, 0..0x3fffffff; super(type)
|
447
|
+
else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
|
448
|
+
end
|
449
|
+
when 'integer'
|
450
|
+
return 'integer' unless limit
|
451
|
+
|
452
|
+
case limit
|
453
|
+
when 1, 2; 'smallint'
|
454
|
+
when 3, 4; 'integer'
|
455
|
+
when 5..8; 'bigint'
|
456
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
457
|
+
end
|
458
|
+
when 'datetime'
|
459
|
+
return super unless precision
|
460
|
+
|
461
|
+
case precision
|
462
|
+
when 0..6; "timestamp(#{precision})"
|
463
|
+
else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
|
464
|
+
end
|
465
|
+
else
|
466
|
+
super
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
|
471
|
+
# requires that the ORDER BY include the distinct column.
|
472
|
+
def columns_for_distinct(columns, orders) #:nodoc:
|
473
|
+
order_columns = orders.map{ |s|
|
474
|
+
# Convert Arel node to string
|
475
|
+
s = s.to_sql unless s.is_a?(String)
|
476
|
+
# Remove any ASC/DESC modifiers
|
477
|
+
s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
|
478
|
+
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
|
479
|
+
|
480
|
+
[super, *order_columns].join(', ')
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|