activerecord 1.0.0 → 2.0.0
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 +4928 -3
- data/README +45 -46
- data/RUNNING_UNIT_TESTS +8 -11
- data/Rakefile +247 -0
- data/install.rb +8 -38
- data/lib/active_record/aggregations.rb +64 -49
- data/lib/active_record/associations/association_collection.rb +217 -47
- data/lib/active_record/associations/association_proxy.rb +159 -0
- data/lib/active_record/associations/belongs_to_association.rb +56 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +155 -37
- data/lib/active_record/associations/has_many_association.rb +145 -75
- data/lib/active_record/associations/has_many_through_association.rb +283 -0
- data/lib/active_record/associations/has_one_association.rb +96 -0
- data/lib/active_record/associations.rb +1537 -304
- data/lib/active_record/attribute_methods.rb +328 -0
- data/lib/active_record/base.rb +2001 -588
- data/lib/active_record/calculations.rb +269 -0
- data/lib/active_record/callbacks.rb +169 -165
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +308 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +472 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +306 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +125 -279
- data/lib/active_record/connection_adapters/mysql_adapter.rb +442 -77
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +805 -135
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +353 -69
- data/lib/active_record/fixtures.rb +946 -100
- data/lib/active_record/locking/optimistic.rb +144 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/migration.rb +417 -0
- data/lib/active_record/observer.rb +142 -32
- data/lib/active_record/query_cache.rb +23 -0
- data/lib/active_record/reflection.rb +163 -70
- data/lib/active_record/schema.rb +58 -0
- data/lib/active_record/schema_dumper.rb +171 -0
- data/lib/active_record/serialization.rb +98 -0
- data/lib/active_record/serializers/json_serializer.rb +71 -0
- data/lib/active_record/serializers/xml_serializer.rb +315 -0
- data/lib/active_record/timestamp.rb +41 -0
- data/lib/active_record/transactions.rb +87 -57
- data/lib/active_record/validations.rb +909 -122
- data/lib/active_record/vendor/db2.rb +362 -0
- data/lib/active_record/vendor/mysql.rb +126 -29
- data/lib/active_record/version.rb +9 -0
- data/lib/active_record.rb +35 -7
- data/lib/activerecord.rb +1 -0
- data/test/aaa_create_tables_test.rb +72 -0
- data/test/abstract_unit.rb +73 -5
- data/test/active_schema_test_mysql.rb +43 -0
- data/test/adapter_test.rb +105 -0
- data/test/adapter_test_sqlserver.rb +95 -0
- data/test/aggregations_test.rb +110 -16
- data/test/all.sh +2 -2
- data/test/ar_schema_test.rb +33 -0
- data/test/association_inheritance_reload.rb +14 -0
- data/test/associations/ar_joins_test.rb +0 -0
- data/test/associations/callbacks_test.rb +147 -0
- data/test/associations/cascaded_eager_loading_test.rb +110 -0
- data/test/associations/eager_singularization_test.rb +145 -0
- data/test/associations/eager_test.rb +442 -0
- data/test/associations/extension_test.rb +47 -0
- data/test/associations/inner_join_association_test.rb +88 -0
- data/test/associations/join_model_test.rb +553 -0
- data/test/associations_test.rb +1930 -267
- data/test/attribute_methods_test.rb +146 -0
- data/test/base_test.rb +1316 -84
- data/test/binary_test.rb +32 -0
- data/test/calculations_test.rb +251 -0
- data/test/callbacks_test.rb +400 -0
- data/test/class_inheritable_attributes_test.rb +3 -4
- data/test/column_alias_test.rb +17 -0
- data/test/connection_test_firebird.rb +8 -0
- data/test/connection_test_mysql.rb +30 -0
- data/test/connections/native_db2/connection.rb +25 -0
- data/test/connections/native_firebird/connection.rb +26 -0
- data/test/connections/native_frontbase/connection.rb +27 -0
- data/test/connections/native_mysql/connection.rb +21 -18
- data/test/connections/native_openbase/connection.rb +21 -0
- data/test/connections/native_oracle/connection.rb +27 -0
- data/test/connections/native_postgresql/connection.rb +17 -18
- data/test/connections/native_sqlite/connection.rb +17 -16
- data/test/connections/native_sqlite3/connection.rb +25 -0
- data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
- data/test/connections/native_sybase/connection.rb +23 -0
- data/test/copy_table_test_sqlite.rb +69 -0
- data/test/datatype_test_postgresql.rb +203 -0
- data/test/date_time_test.rb +37 -0
- data/test/default_test_firebird.rb +16 -0
- data/test/defaults_test.rb +67 -0
- data/test/deprecated_finder_test.rb +30 -0
- data/test/finder_test.rb +607 -32
- data/test/fixtures/accounts.yml +28 -0
- data/test/fixtures/all/developers.yml +0 -0
- data/test/fixtures/all/people.csv +0 -0
- data/test/fixtures/all/tasks.yml +0 -0
- data/test/fixtures/author.rb +107 -0
- data/test/fixtures/author_favorites.yml +4 -0
- data/test/fixtures/authors.yml +7 -0
- data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
- data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
- data/test/fixtures/bad_fixtures/blank_line +3 -0
- data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
- data/test/fixtures/bad_fixtures/missing_value +1 -0
- data/test/fixtures/binaries.yml +132 -0
- data/test/fixtures/binary.rb +2 -0
- data/test/fixtures/book.rb +4 -0
- data/test/fixtures/books.yml +7 -0
- data/test/fixtures/categories/special_categories.yml +9 -0
- data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
- data/test/fixtures/categories.yml +14 -0
- data/test/fixtures/categories_ordered.yml +7 -0
- data/test/fixtures/categories_posts.yml +23 -0
- data/test/fixtures/categorization.rb +5 -0
- data/test/fixtures/categorizations.yml +17 -0
- data/test/fixtures/category.rb +26 -0
- data/test/fixtures/citation.rb +6 -0
- data/test/fixtures/comment.rb +23 -0
- data/test/fixtures/comments.yml +59 -0
- data/test/fixtures/companies.yml +55 -0
- data/test/fixtures/company.rb +81 -4
- data/test/fixtures/company_in_module.rb +32 -6
- data/test/fixtures/computer.rb +4 -0
- data/test/fixtures/computers.yml +4 -0
- data/test/fixtures/contact.rb +16 -0
- data/test/fixtures/courses.yml +7 -0
- data/test/fixtures/customer.rb +28 -3
- data/test/fixtures/customers.yml +17 -0
- data/test/fixtures/db_definitions/db2.drop.sql +33 -0
- data/test/fixtures/db_definitions/db2.sql +235 -0
- data/test/fixtures/db_definitions/db22.drop.sql +2 -0
- data/test/fixtures/db_definitions/db22.sql +5 -0
- data/test/fixtures/db_definitions/firebird.drop.sql +65 -0
- data/test/fixtures/db_definitions/firebird.sql +310 -0
- data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
- data/test/fixtures/db_definitions/firebird2.sql +6 -0
- data/test/fixtures/db_definitions/frontbase.drop.sql +33 -0
- data/test/fixtures/db_definitions/frontbase.sql +273 -0
- data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
- data/test/fixtures/db_definitions/frontbase2.sql +4 -0
- data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase.sql +318 -0
- data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
- data/test/fixtures/db_definitions/openbase2.sql +7 -0
- data/test/fixtures/db_definitions/oracle.drop.sql +67 -0
- data/test/fixtures/db_definitions/oracle.sql +330 -0
- data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
- data/test/fixtures/db_definitions/oracle2.sql +6 -0
- data/test/fixtures/db_definitions/postgresql.drop.sql +44 -0
- data/test/fixtures/db_definitions/postgresql.sql +217 -38
- data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
- data/test/fixtures/db_definitions/postgresql2.sql +2 -2
- data/test/fixtures/db_definitions/schema.rb +354 -0
- data/test/fixtures/db_definitions/schema2.rb +11 -0
- data/test/fixtures/db_definitions/sqlite.drop.sql +33 -0
- data/test/fixtures/db_definitions/sqlite.sql +139 -5
- data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
- data/test/fixtures/db_definitions/sqlite2.sql +1 -0
- data/test/fixtures/db_definitions/sybase.drop.sql +35 -0
- data/test/fixtures/db_definitions/sybase.sql +222 -0
- data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
- data/test/fixtures/db_definitions/sybase2.sql +5 -0
- data/test/fixtures/developer.rb +70 -6
- data/test/fixtures/developers.yml +21 -0
- data/test/fixtures/developers_projects/david_action_controller +2 -1
- data/test/fixtures/developers_projects/david_active_record +2 -1
- data/test/fixtures/developers_projects.yml +17 -0
- data/test/fixtures/edge.rb +5 -0
- data/test/fixtures/edges.yml +6 -0
- data/test/fixtures/entrants.yml +14 -0
- data/test/fixtures/example.log +1 -0
- data/test/fixtures/fk_test_has_fk.yml +3 -0
- data/test/fixtures/fk_test_has_pk.yml +2 -0
- data/test/fixtures/flowers.jpg +0 -0
- data/test/fixtures/funny_jokes.yml +10 -0
- data/test/fixtures/item.rb +7 -0
- data/test/fixtures/items.yml +4 -0
- data/test/fixtures/joke.rb +3 -0
- data/test/fixtures/keyboard.rb +3 -0
- data/test/fixtures/legacy_thing.rb +3 -0
- data/test/fixtures/legacy_things.yml +3 -0
- data/test/fixtures/matey.rb +4 -0
- data/test/fixtures/mateys.yml +4 -0
- data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
- data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
- data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
- data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
- data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
- data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
- data/test/fixtures/minimalistic.rb +2 -0
- data/test/fixtures/minimalistics.yml +2 -0
- data/test/fixtures/mixed_case_monkey.rb +3 -0
- data/test/fixtures/mixed_case_monkeys.yml +6 -0
- data/test/fixtures/mixins.yml +29 -0
- data/test/fixtures/movies.yml +7 -0
- data/test/fixtures/naked/csv/accounts.csv +1 -0
- data/test/fixtures/naked/yml/accounts.yml +1 -0
- data/test/fixtures/naked/yml/companies.yml +1 -0
- data/test/fixtures/naked/yml/courses.yml +1 -0
- data/test/fixtures/order.rb +4 -0
- data/test/fixtures/parrot.rb +13 -0
- data/test/fixtures/parrots.yml +27 -0
- data/test/fixtures/parrots_pirates.yml +7 -0
- data/test/fixtures/people.yml +3 -0
- data/test/fixtures/person.rb +4 -0
- data/test/fixtures/pirate.rb +5 -0
- data/test/fixtures/pirates.yml +9 -0
- data/test/fixtures/post.rb +59 -0
- data/test/fixtures/posts.yml +48 -0
- data/test/fixtures/project.rb +27 -2
- data/test/fixtures/projects.yml +7 -0
- data/test/fixtures/reader.rb +4 -0
- data/test/fixtures/readers.yml +4 -0
- data/test/fixtures/reply.rb +18 -2
- data/test/fixtures/reserved_words/distinct.yml +5 -0
- data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
- data/test/fixtures/reserved_words/group.yml +14 -0
- data/test/fixtures/reserved_words/select.yml +8 -0
- data/test/fixtures/reserved_words/values.yml +7 -0
- data/test/fixtures/ship.rb +3 -0
- data/test/fixtures/ships.yml +5 -0
- data/test/fixtures/subject.rb +4 -0
- data/test/fixtures/subscriber.rb +4 -3
- data/test/fixtures/tag.rb +7 -0
- data/test/fixtures/tagging.rb +10 -0
- data/test/fixtures/taggings.yml +25 -0
- data/test/fixtures/tags.yml +7 -0
- data/test/fixtures/task.rb +3 -0
- data/test/fixtures/tasks.yml +7 -0
- data/test/fixtures/topic.rb +20 -3
- data/test/fixtures/topics.yml +22 -0
- data/test/fixtures/treasure.rb +4 -0
- data/test/fixtures/treasures.yml +10 -0
- data/test/fixtures/vertex.rb +9 -0
- data/test/fixtures/vertices.yml +4 -0
- data/test/fixtures_test.rb +574 -8
- data/test/inheritance_test.rb +113 -27
- data/test/json_serialization_test.rb +180 -0
- data/test/lifecycle_test.rb +56 -29
- data/test/locking_test.rb +273 -0
- data/test/method_scoping_test.rb +416 -0
- data/test/migration_test.rb +933 -0
- data/test/migration_test_firebird.rb +124 -0
- data/test/mixin_test.rb +95 -0
- data/test/modules_test.rb +23 -10
- data/test/multiple_db_test.rb +17 -3
- data/test/pk_test.rb +59 -15
- data/test/query_cache_test.rb +104 -0
- data/test/readonly_test.rb +107 -0
- data/test/reflection_test.rb +124 -27
- data/test/reserved_word_test_mysql.rb +177 -0
- data/test/schema_authorization_test_postgresql.rb +75 -0
- data/test/schema_dumper_test.rb +131 -0
- data/test/schema_test_postgresql.rb +64 -0
- data/test/serialization_test.rb +47 -0
- data/test/synonym_test_oracle.rb +17 -0
- data/test/table_name_test_sqlserver.rb +23 -0
- data/test/threaded_connections_test.rb +48 -0
- data/test/transactions_test.rb +227 -29
- data/test/unconnected_test.rb +14 -6
- data/test/validations_test.rb +1293 -32
- data/test/xml_serialization_test.rb +202 -0
- metadata +347 -143
- data/dev-utils/eval_debugger.rb +0 -9
- data/examples/associations.rb +0 -87
- data/examples/shared_setup.rb +0 -15
- data/examples/validation.rb +0 -88
- data/lib/active_record/deprecated_associations.rb +0 -70
- data/lib/active_record/support/class_attribute_accessors.rb +0 -43
- data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
- data/lib/active_record/support/clean_logger.rb +0 -10
- data/lib/active_record/support/inflector.rb +0 -70
- data/lib/active_record/vendor/simple.rb +0 -702
- data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
- data/lib/active_record/wrappings.rb +0 -59
- data/rakefile +0 -122
- data/test/deprecated_associations_test.rb +0 -336
- data/test/fixtures/accounts/signals37 +0 -3
- data/test/fixtures/accounts/unknown +0 -2
- data/test/fixtures/companies/first_client +0 -6
- data/test/fixtures/companies/first_firm +0 -4
- data/test/fixtures/companies/second_client +0 -6
- data/test/fixtures/courses/java +0 -2
- data/test/fixtures/courses/ruby +0 -2
- data/test/fixtures/customers/david +0 -6
- data/test/fixtures/db_definitions/mysql.sql +0 -96
- data/test/fixtures/db_definitions/mysql2.sql +0 -4
- data/test/fixtures/developers/david +0 -2
- data/test/fixtures/developers/jamis +0 -2
- data/test/fixtures/entrants/first +0 -3
- data/test/fixtures/entrants/second +0 -3
- data/test/fixtures/entrants/third +0 -3
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/movies/first +0 -2
- data/test/fixtures/movies/second +0 -2
- data/test/fixtures/projects/action_controller +0 -2
- data/test/fixtures/projects/active_record +0 -2
- data/test/fixtures/topics/first +0 -9
- data/test/fixtures/topics/second +0 -8
- data/test/inflector_test.rb +0 -104
- data/test/thread_safety_test.rb +0 -33
@@ -1,177 +1,847 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
1
2
|
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
# This is due to the fact that, in postgresql you can not have a
|
9
|
-
# totally zero timestamp. Instead null/nil should be used to
|
10
|
-
# represent no value.
|
11
|
-
#
|
3
|
+
module ActiveRecord
|
4
|
+
class Base
|
5
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
6
|
+
def self.postgresql_connection(config) # :nodoc:
|
7
|
+
require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
|
12
8
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
9
|
+
config = config.symbolize_keys
|
10
|
+
host = config[:host]
|
11
|
+
port = config[:port] || 5432
|
12
|
+
username = config[:username].to_s
|
13
|
+
password = config[:password].to_s
|
14
|
+
|
15
|
+
if config.has_key?(:database)
|
16
|
+
database = config[:database]
|
17
|
+
else
|
18
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
19
|
+
end
|
20
|
+
|
21
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
22
|
+
# so just pass a nil connection object for the time being.
|
23
|
+
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ConnectionAdapters
|
28
|
+
# PostgreSQL-specific extensions to column definitions in a table.
|
29
|
+
class PostgreSQLColumn < Column #:nodoc:
|
30
|
+
# Instantiates a new PostgreSQL column definition in a table.
|
31
|
+
def initialize(name, default, sql_type = nil, null = true)
|
32
|
+
super(name, self.class.extract_value_from_default(default), sql_type, null)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
# Extracts the scale from PostgreSQL-specific data types.
|
37
|
+
def extract_scale(sql_type)
|
38
|
+
# Money type has a fixed scale of 2.
|
39
|
+
sql_type =~ /^money/ ? 2 : super
|
40
|
+
end
|
41
|
+
|
42
|
+
# Extracts the precision from PostgreSQL-specific data types.
|
43
|
+
def extract_precision(sql_type)
|
44
|
+
# Actual code is defined dynamically in PostgreSQLAdapter.connect
|
45
|
+
# depending on the server specifics
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
# Escapes binary strings for bytea input to the database.
|
50
|
+
def self.string_to_binary(value)
|
51
|
+
if PGconn.respond_to?(:escape_bytea)
|
52
|
+
self.class.module_eval do
|
53
|
+
define_method(:string_to_binary) do |value|
|
54
|
+
PGconn.escape_bytea(value) if value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
else
|
58
|
+
self.class.module_eval do
|
59
|
+
define_method(:string_to_binary) do |value|
|
60
|
+
if value
|
61
|
+
result = ''
|
62
|
+
value.each_byte { |c| result << sprintf('\\\\%03o', c) }
|
63
|
+
result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
self.class.string_to_binary(value)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Unescapes bytea output from a database to the binary string it represents.
|
72
|
+
def self.binary_to_string(value)
|
73
|
+
# In each case, check if the value actually is escaped PostgreSQL bytea output
|
74
|
+
# or an unescaped Active Record attribute that was just written.
|
75
|
+
if PGconn.respond_to?(:unescape_bytea)
|
76
|
+
self.class.module_eval do
|
77
|
+
define_method(:binary_to_string) do |value|
|
78
|
+
if value =~ /\\\d{3}/
|
79
|
+
PGconn.unescape_bytea(value)
|
80
|
+
else
|
81
|
+
value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
else
|
86
|
+
self.class.module_eval do
|
87
|
+
define_method(:binary_to_string) do |value|
|
88
|
+
if value =~ /\\\d{3}/
|
89
|
+
result = ''
|
90
|
+
i, max = 0, value.size
|
91
|
+
while i < max
|
92
|
+
char = value[i]
|
93
|
+
if char == ?\\
|
94
|
+
if value[i+1] == ?\\
|
95
|
+
char = ?\\
|
96
|
+
i += 1
|
97
|
+
else
|
98
|
+
char = value[i+1..i+3].oct
|
99
|
+
i += 3
|
100
|
+
end
|
101
|
+
end
|
102
|
+
result << char
|
103
|
+
i += 1
|
104
|
+
end
|
105
|
+
result
|
106
|
+
else
|
107
|
+
value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
self.class.binary_to_string(value)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Maps PostgreSQL-specific data types to logical Rails types.
|
116
|
+
def simplified_type(field_type)
|
117
|
+
case field_type
|
118
|
+
# Numeric and monetary types
|
119
|
+
when /^(?:real|double precision)$/
|
120
|
+
:float
|
121
|
+
# Monetary types
|
122
|
+
when /^money$/
|
123
|
+
:decimal
|
124
|
+
# Character types
|
125
|
+
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
|
126
|
+
:string
|
127
|
+
# Binary data types
|
128
|
+
when /^bytea$/
|
129
|
+
:binary
|
130
|
+
# Date/time types
|
131
|
+
when /^timestamp with(?:out)? time zone$/
|
132
|
+
:datetime
|
133
|
+
when /^interval$/
|
134
|
+
:string
|
135
|
+
# Geometric types
|
136
|
+
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
|
137
|
+
:string
|
138
|
+
# Network address types
|
139
|
+
when /^(?:cidr|inet|macaddr)$/
|
140
|
+
:string
|
141
|
+
# Bit strings
|
142
|
+
when /^bit(?: varying)?(?:\(\d+\))?$/
|
143
|
+
:string
|
144
|
+
# XML type
|
145
|
+
when /^xml$/
|
146
|
+
:string
|
147
|
+
# Arrays
|
148
|
+
when /^\D+\[\]$/
|
149
|
+
:string
|
150
|
+
# Object identifier types
|
151
|
+
when /^oid$/
|
152
|
+
:integer
|
153
|
+
# Pass through all types that are not specific to PostgreSQL.
|
154
|
+
else
|
155
|
+
super
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Extracts the value from a PostgreSQL column default definition.
|
160
|
+
def self.extract_value_from_default(default)
|
161
|
+
case default
|
162
|
+
# Numeric types
|
163
|
+
when /\A-?\d+(\.\d*)?\z/
|
164
|
+
default
|
165
|
+
# Character types
|
166
|
+
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
|
167
|
+
$1
|
168
|
+
# Character types (8.1 formatting)
|
169
|
+
when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m
|
170
|
+
$1.gsub(/\\(\d\d\d)/) { $1.oct.chr }
|
171
|
+
# Binary data types
|
172
|
+
when /\A'(.*)'::bytea\z/m
|
173
|
+
$1
|
174
|
+
# Date/time types
|
175
|
+
when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
|
176
|
+
$1
|
177
|
+
when /\A'(.*)'::interval\z/
|
178
|
+
$1
|
179
|
+
# Boolean type
|
180
|
+
when 'true'
|
181
|
+
true
|
182
|
+
when 'false'
|
183
|
+
false
|
184
|
+
# Geometric types
|
185
|
+
when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
|
186
|
+
$1
|
187
|
+
# Network address types
|
188
|
+
when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
|
189
|
+
$1
|
190
|
+
# Bit string types
|
191
|
+
when /\AB'(.*)'::"?bit(?: varying)?"?\z/
|
192
|
+
$1
|
193
|
+
# XML type
|
194
|
+
when /\A'(.*)'::xml\z/m
|
195
|
+
$1
|
196
|
+
# Arrays
|
197
|
+
when /\A'(.*)'::"?\D+"?\[\]\z/
|
198
|
+
$1
|
199
|
+
# Object identifier types
|
200
|
+
when /\A-?\d+\z/
|
201
|
+
$1
|
202
|
+
else
|
203
|
+
# Anything else is blank, some user type, or some function
|
204
|
+
# and we can't know the value of that, so return nil.
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
module ConnectionAdapters
|
212
|
+
# The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
|
213
|
+
# Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
|
214
|
+
#
|
215
|
+
# Options:
|
216
|
+
#
|
217
|
+
# * <tt>:host</tt> -- Defaults to localhost
|
218
|
+
# * <tt>:port</tt> -- Defaults to 5432
|
219
|
+
# * <tt>:username</tt> -- Defaults to nothing
|
220
|
+
# * <tt>:password</tt> -- Defaults to nothing
|
221
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
222
|
+
# * <tt>:schema_search_path</tt> -- An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the :schema_order option.
|
223
|
+
# * <tt>:encoding</tt> -- An optional client encoding that is used in a SET client_encoding TO <encoding> call on the connection.
|
224
|
+
# * <tt>:min_messages</tt> -- An optional client min messages that is used in a SET client_min_messages TO <min_messages> call on the connection.
|
225
|
+
# * <tt>:allow_concurrency</tt> -- If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
|
226
|
+
class PostgreSQLAdapter < AbstractAdapter
|
227
|
+
# Returns 'PostgreSQL' as adapter name for identification purposes.
|
228
|
+
def adapter_name
|
229
|
+
'PostgreSQL'
|
230
|
+
end
|
231
|
+
|
232
|
+
# Initializes and connects a PostgreSQL adapter.
|
233
|
+
def initialize(connection, logger, connection_parameters, config)
|
234
|
+
super(connection, logger)
|
235
|
+
@connection_parameters, @config = connection_parameters, config
|
236
|
+
|
237
|
+
connect
|
238
|
+
end
|
239
|
+
|
240
|
+
# Is this connection alive and ready for queries?
|
241
|
+
def active?
|
242
|
+
if @connection.respond_to?(:status)
|
243
|
+
@connection.status == PGconn::CONNECTION_OK
|
32
244
|
else
|
33
|
-
|
245
|
+
# We're asking the driver, not ActiveRecord, so use @connection.query instead of #query
|
246
|
+
@connection.query 'SELECT 1'
|
247
|
+
true
|
34
248
|
end
|
249
|
+
# postgres-pr raises a NoMethodError when querying if no connection is available.
|
250
|
+
rescue PGError, NoMethodError
|
251
|
+
false
|
252
|
+
end
|
35
253
|
|
36
|
-
|
37
|
-
|
38
|
-
)
|
254
|
+
# Close then reopen the connection.
|
255
|
+
def reconnect!
|
256
|
+
if @connection.respond_to?(:reset)
|
257
|
+
@connection.reset
|
258
|
+
configure_connection
|
259
|
+
else
|
260
|
+
disconnect!
|
261
|
+
connect
|
262
|
+
end
|
39
263
|
end
|
40
|
-
end
|
41
264
|
|
42
|
-
|
43
|
-
|
265
|
+
# Close the connection.
|
266
|
+
def disconnect!
|
267
|
+
@connection.close rescue nil
|
268
|
+
end
|
269
|
+
|
270
|
+
def native_database_types #:nodoc:
|
271
|
+
{
|
272
|
+
:primary_key => "serial primary key",
|
273
|
+
:string => { :name => "character varying", :limit => 255 },
|
274
|
+
:text => { :name => "text" },
|
275
|
+
:integer => { :name => "integer" },
|
276
|
+
:float => { :name => "float" },
|
277
|
+
:decimal => { :name => "decimal" },
|
278
|
+
:datetime => { :name => "timestamp" },
|
279
|
+
:timestamp => { :name => "timestamp" },
|
280
|
+
:time => { :name => "time" },
|
281
|
+
:date => { :name => "date" },
|
282
|
+
:binary => { :name => "bytea" },
|
283
|
+
:boolean => { :name => "boolean" }
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
# Does PostgreSQL support migrations?
|
288
|
+
def supports_migrations?
|
289
|
+
true
|
290
|
+
end
|
291
|
+
|
292
|
+
# Does PostgreSQL support standard conforming strings?
|
293
|
+
def supports_standard_conforming_strings?
|
294
|
+
# Temporarily set the client message level above error to prevent unintentional
|
295
|
+
# error messages in the logs when working on a PostgreSQL database server that
|
296
|
+
# does not support standard conforming strings.
|
297
|
+
client_min_messages_old = client_min_messages
|
298
|
+
self.client_min_messages = 'panic'
|
299
|
+
|
300
|
+
# postgres-pr does not raise an exception when client_min_messages is set higher
|
301
|
+
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
|
302
|
+
# PGresult instead.
|
303
|
+
has_support = execute('SHOW standard_conforming_strings')[0][0] rescue false
|
304
|
+
self.client_min_messages = client_min_messages_old
|
305
|
+
has_support
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns the configured supported identifier length supported by PostgreSQL,
|
309
|
+
# or report the default of 63 on PostgreSQL 7.x.
|
310
|
+
def table_alias_length
|
311
|
+
@table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63)
|
312
|
+
end
|
44
313
|
|
45
|
-
|
46
|
-
|
314
|
+
# QUOTING ==================================================
|
315
|
+
|
316
|
+
# Quotes PostgreSQL-specific data types for SQL input.
|
317
|
+
def quote(value, column = nil) #:nodoc:
|
318
|
+
if value.kind_of?(String) && column && column.type == :binary
|
319
|
+
"#{quoted_string_prefix}'#{column.class.string_to_binary(value)}'"
|
320
|
+
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
|
321
|
+
"xml '#{quote_string(value)}'"
|
322
|
+
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
|
323
|
+
# Not truly string input, so doesn't require (or allow) escape string syntax.
|
324
|
+
"'#{value.to_s}'"
|
325
|
+
elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/
|
326
|
+
case value
|
327
|
+
when /^[01]*$/
|
328
|
+
"B'#{value}'" # Bit-string notation
|
329
|
+
when /^[0-9A-F]*$/i
|
330
|
+
"X'#{value}'" # Hexadecimal notation
|
331
|
+
end
|
332
|
+
else
|
333
|
+
super
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Quotes strings for use in SQL input in the postgres driver for better performance.
|
338
|
+
def quote_string(s) #:nodoc:
|
339
|
+
if PGconn.respond_to?(:escape)
|
340
|
+
self.class.instance_eval do
|
341
|
+
define_method(:quote_string) do |s|
|
342
|
+
PGconn.escape(s)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
else
|
346
|
+
# There are some incorrectly compiled postgres drivers out there
|
347
|
+
# that don't define PGconn.escape.
|
348
|
+
self.class.instance_eval do
|
349
|
+
undef_method(:quote_string)
|
350
|
+
end
|
47
351
|
end
|
352
|
+
quote_string(s)
|
353
|
+
end
|
354
|
+
|
355
|
+
# Quotes column names for use in SQL queries.
|
356
|
+
def quote_column_name(name) #:nodoc:
|
357
|
+
%("#{name}")
|
358
|
+
end
|
48
359
|
|
49
|
-
|
50
|
-
|
51
|
-
|
360
|
+
# Quote date/time values for use in SQL input. Includes microseconds
|
361
|
+
# if the value is a Time responding to usec.
|
362
|
+
def quoted_date(value) #:nodoc:
|
363
|
+
if value.acts_like?(:time) && value.respond_to?(:usec)
|
364
|
+
"#{super}.#{sprintf("%06d", value.usec)}"
|
365
|
+
else
|
366
|
+
super
|
52
367
|
end
|
368
|
+
end
|
369
|
+
|
370
|
+
# REFERENTIAL INTEGRITY ====================================
|
371
|
+
|
372
|
+
def disable_referential_integrity(&block) #:nodoc:
|
373
|
+
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
|
374
|
+
yield
|
375
|
+
ensure
|
376
|
+
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
|
377
|
+
end
|
378
|
+
|
379
|
+
# DATABASE STATEMENTS ======================================
|
380
|
+
|
381
|
+
# Executes a SELECT query and returns an array of rows. Each row is an
|
382
|
+
# array of field values.
|
383
|
+
def select_rows(sql, name = nil)
|
384
|
+
select_raw(sql, name).last
|
385
|
+
end
|
386
|
+
|
387
|
+
# Executes an INSERT query and returns the new record's ID
|
388
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
389
|
+
table = sql.split(" ", 4)[2]
|
390
|
+
super || last_insert_id(table, sequence_name || default_sequence_name(table, pk))
|
391
|
+
end
|
53
392
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
393
|
+
# Queries the database and returns the results in an Array or nil otherwise.
|
394
|
+
def query(sql, name = nil) #:nodoc:
|
395
|
+
log(sql, name) do
|
396
|
+
if @async
|
397
|
+
@connection.async_query(sql)
|
398
|
+
else
|
399
|
+
@connection.query(sql)
|
58
400
|
end
|
59
401
|
end
|
402
|
+
end
|
60
403
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
404
|
+
# Executes an SQL statement, returning a PGresult object on success
|
405
|
+
# or raising a PGError exception otherwise.
|
406
|
+
def execute(sql, name = nil)
|
407
|
+
log(sql, name) do
|
408
|
+
if @async
|
409
|
+
@connection.async_exec(sql)
|
410
|
+
else
|
411
|
+
@connection.exec(sql)
|
412
|
+
end
|
65
413
|
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# Executes an UPDATE query and returns the number of affected tuples.
|
417
|
+
def update_sql(sql, name = nil)
|
418
|
+
super.cmdtuples
|
419
|
+
end
|
420
|
+
|
421
|
+
# Begins a transaction.
|
422
|
+
def begin_db_transaction
|
423
|
+
execute "BEGIN"
|
424
|
+
end
|
66
425
|
|
67
|
-
|
68
|
-
|
426
|
+
# Commits a transaction.
|
427
|
+
def commit_db_transaction
|
428
|
+
execute "COMMIT"
|
429
|
+
end
|
430
|
+
|
431
|
+
# Aborts a transaction.
|
432
|
+
def rollback_db_transaction
|
433
|
+
execute "ROLLBACK"
|
434
|
+
end
|
435
|
+
|
436
|
+
# SCHEMA STATEMENTS ========================================
|
437
|
+
|
438
|
+
# Returns the list of all tables in the schema search path or a specified schema.
|
439
|
+
def tables(name = nil)
|
440
|
+
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
441
|
+
query(<<-SQL, name).map { |row| row[0] }
|
442
|
+
SELECT tablename
|
443
|
+
FROM pg_tables
|
444
|
+
WHERE schemaname IN (#{schemas})
|
445
|
+
SQL
|
446
|
+
end
|
447
|
+
|
448
|
+
# Returns the list of all indexes for a table.
|
449
|
+
def indexes(table_name, name = nil)
|
450
|
+
result = query(<<-SQL, name)
|
451
|
+
SELECT i.relname, d.indisunique, a.attname
|
452
|
+
FROM pg_class t, pg_class i, pg_index d, pg_attribute a
|
453
|
+
WHERE i.relkind = 'i'
|
454
|
+
AND d.indexrelid = i.oid
|
455
|
+
AND d.indisprimary = 'f'
|
456
|
+
AND t.oid = d.indrelid
|
457
|
+
AND t.relname = '#{table_name}'
|
458
|
+
AND a.attrelid = t.oid
|
459
|
+
AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
|
460
|
+
OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
|
461
|
+
OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
|
462
|
+
OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
|
463
|
+
OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
|
464
|
+
ORDER BY i.relname
|
465
|
+
SQL
|
466
|
+
|
467
|
+
current_index = nil
|
468
|
+
indexes = []
|
469
|
+
|
470
|
+
result.each do |row|
|
471
|
+
if current_index != row[0]
|
472
|
+
indexes << IndexDefinition.new(table_name, row[0], row[1] == "t", [])
|
473
|
+
current_index = row[0]
|
474
|
+
end
|
475
|
+
|
476
|
+
indexes.last.columns << row[2]
|
69
477
|
end
|
70
478
|
|
71
|
-
|
72
|
-
|
479
|
+
indexes
|
480
|
+
end
|
73
481
|
|
74
|
-
|
75
|
-
|
76
|
-
|
482
|
+
# Returns the list of all column definitions for a table.
|
483
|
+
def columns(table_name, name = nil)
|
484
|
+
# Limit, precision, and scale are all handled by the superclass.
|
485
|
+
column_definitions(table_name).collect do |name, type, default, notnull|
|
486
|
+
PostgreSQLColumn.new(name, default, type, notnull == 'f')
|
487
|
+
end
|
488
|
+
end
|
77
489
|
|
78
|
-
|
79
|
-
|
490
|
+
# Sets the schema search path to a string of comma-separated schema names.
|
491
|
+
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
492
|
+
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
|
493
|
+
#
|
494
|
+
# This should be not be called manually but set in database.yml.
|
495
|
+
def schema_search_path=(schema_csv)
|
496
|
+
if schema_csv
|
497
|
+
execute "SET search_path TO #{schema_csv}"
|
498
|
+
@schema_search_path = schema_csv
|
80
499
|
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Returns the active schema search path.
|
503
|
+
def schema_search_path
|
504
|
+
@schema_search_path ||= query('SHOW search_path')[0][0]
|
505
|
+
end
|
506
|
+
|
507
|
+
# Returns the current client message level.
|
508
|
+
def client_min_messages
|
509
|
+
query('SHOW client_min_messages')[0][0]
|
510
|
+
end
|
511
|
+
|
512
|
+
# Set the client message level.
|
513
|
+
def client_min_messages=(level)
|
514
|
+
execute("SET client_min_messages TO '#{level}'")
|
515
|
+
end
|
81
516
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
517
|
+
# Returns the sequence name for a table's primary key or some other specified key.
|
518
|
+
def default_sequence_name(table_name, pk = nil) #:nodoc:
|
519
|
+
default_pk, default_seq = pk_and_sequence_for(table_name)
|
520
|
+
default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq"
|
521
|
+
end
|
522
|
+
|
523
|
+
# Resets the sequence of a table's primary key to the maximum value.
|
524
|
+
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
525
|
+
unless pk and sequence
|
526
|
+
default_pk, default_sequence = pk_and_sequence_for(table)
|
527
|
+
pk ||= default_pk
|
528
|
+
sequence ||= default_sequence
|
529
|
+
end
|
530
|
+
if pk
|
531
|
+
if sequence
|
532
|
+
select_value <<-end_sql, 'Reset sequence'
|
533
|
+
SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false)
|
534
|
+
end_sql
|
535
|
+
else
|
536
|
+
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
|
86
537
|
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
# Returns a table's primary key and belonging sequence.
|
542
|
+
def pk_and_sequence_for(table) #:nodoc:
|
543
|
+
# First try looking for a sequence with a dependency on the
|
544
|
+
# given table's primary key.
|
545
|
+
result = query(<<-end_sql, 'PK and serial sequence')[0]
|
546
|
+
SELECT attr.attname, seq.relname
|
547
|
+
FROM pg_class seq,
|
548
|
+
pg_attribute attr,
|
549
|
+
pg_depend dep,
|
550
|
+
pg_namespace name,
|
551
|
+
pg_constraint cons
|
552
|
+
WHERE seq.oid = dep.objid
|
553
|
+
AND seq.relkind = 'S'
|
554
|
+
AND attr.attrelid = dep.refobjid
|
555
|
+
AND attr.attnum = dep.refobjsubid
|
556
|
+
AND attr.attrelid = cons.conrelid
|
557
|
+
AND attr.attnum = cons.conkey[1]
|
558
|
+
AND cons.contype = 'p'
|
559
|
+
AND dep.refobjid = '#{table}'::regclass
|
560
|
+
end_sql
|
561
|
+
|
562
|
+
if result.nil? or result.empty?
|
563
|
+
# If that fails, try parsing the primary key's default value.
|
564
|
+
# Support the 7.x and 8.0 nextval('foo'::text) as well as
|
565
|
+
# the 8.1+ nextval('foo'::regclass).
|
566
|
+
result = query(<<-end_sql, 'PK and custom sequence')[0]
|
567
|
+
SELECT attr.attname, split_part(def.adsrc, '''', 2)
|
568
|
+
FROM pg_class t
|
569
|
+
JOIN pg_attribute attr ON (t.oid = attrelid)
|
570
|
+
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
|
571
|
+
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
|
572
|
+
WHERE t.oid = '#{table}'::regclass
|
573
|
+
AND cons.contype = 'p'
|
574
|
+
AND def.adsrc ~* 'nextval'
|
575
|
+
end_sql
|
576
|
+
end
|
577
|
+
# [primary_key, sequence]
|
578
|
+
[result.first, result.last]
|
579
|
+
rescue
|
580
|
+
nil
|
581
|
+
end
|
582
|
+
|
583
|
+
# Renames a table.
|
584
|
+
def rename_table(name, new_name)
|
585
|
+
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
586
|
+
end
|
587
|
+
|
588
|
+
# Adds a column to a table.
|
589
|
+
def add_column(table_name, column_name, type, options = {})
|
590
|
+
default = options[:default]
|
591
|
+
notnull = options[:null] == false
|
592
|
+
|
593
|
+
# Add the column.
|
594
|
+
execute("ALTER TABLE #{table_name} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit])}")
|
595
|
+
|
596
|
+
change_column_default(table_name, column_name, default) if options_include_default?(options)
|
597
|
+
change_column_null(table_name, column_name, false, default) if notnull
|
598
|
+
end
|
87
599
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
600
|
+
# Changes the column of a table.
|
601
|
+
def change_column(table_name, column_name, type, options = {})
|
602
|
+
begin
|
603
|
+
execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
604
|
+
rescue ActiveRecord::StatementInvalid
|
605
|
+
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
|
606
|
+
begin_db_transaction
|
607
|
+
tmp_column_name = "#{column_name}_ar_tmp"
|
608
|
+
add_column(table_name, tmp_column_name, type, options)
|
609
|
+
execute "UPDATE #{table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
|
610
|
+
remove_column(table_name, column_name)
|
611
|
+
rename_column(table_name, tmp_column_name, column_name)
|
612
|
+
commit_db_transaction
|
613
|
+
end
|
614
|
+
|
615
|
+
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
|
616
|
+
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
|
617
|
+
end
|
618
|
+
|
619
|
+
# Changes the default value of a table column.
|
620
|
+
def change_column_default(table_name, column_name, default)
|
621
|
+
execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
|
622
|
+
end
|
623
|
+
|
624
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
625
|
+
unless null || default.nil?
|
626
|
+
execute("UPDATE #{table_name} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
627
|
+
end
|
628
|
+
execute("ALTER TABLE #{table_name} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
|
629
|
+
end
|
630
|
+
|
631
|
+
# Renames a column in a table.
|
632
|
+
def rename_column(table_name, column_name, new_column_name)
|
633
|
+
execute "ALTER TABLE #{table_name} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
634
|
+
end
|
635
|
+
|
636
|
+
# Drops an index from a table.
|
637
|
+
def remove_index(table_name, options = {})
|
638
|
+
execute "DROP INDEX #{index_name(table_name, options)}"
|
639
|
+
end
|
640
|
+
|
641
|
+
# Maps logical Rails types to PostgreSQL-specific data types.
|
642
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
643
|
+
return super unless type.to_s == 'integer'
|
644
|
+
|
645
|
+
if limit.nil? || limit == 4
|
646
|
+
'integer'
|
647
|
+
elsif limit < 4
|
648
|
+
'smallint'
|
649
|
+
else
|
650
|
+
'bigint'
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
655
|
+
#
|
656
|
+
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
|
657
|
+
# requires that the ORDER BY include the distinct column.
|
658
|
+
#
|
659
|
+
# distinct("posts.id", "posts.created_at desc")
|
660
|
+
def distinct(columns, order_by) #:nodoc:
|
661
|
+
return "DISTINCT #{columns}" if order_by.blank?
|
662
|
+
|
663
|
+
# Construct a clean list of column names from the ORDER BY clause, removing
|
664
|
+
# any ASC/DESC modifiers
|
665
|
+
order_columns = order_by.split(',').collect { |s| s.split.first }
|
666
|
+
order_columns.delete_if &:blank?
|
667
|
+
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
|
668
|
+
|
669
|
+
# Return a DISTINCT ON() clause that's distinct on the columns we want but includes
|
670
|
+
# all the required columns for the ORDER BY to work properly.
|
671
|
+
sql = "DISTINCT ON (#{columns}) #{columns}, "
|
672
|
+
sql << order_columns * ', '
|
673
|
+
end
|
674
|
+
|
675
|
+
# Returns an ORDER BY clause for the passed order option.
|
676
|
+
#
|
677
|
+
# PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
|
678
|
+
# by wrapping the sql as a sub-select and ordering in that query.
|
679
|
+
def add_order_by_for_association_limiting!(sql, options) #:nodoc:
|
680
|
+
return sql if options[:order].blank?
|
681
|
+
|
682
|
+
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
|
683
|
+
order.map! { |s| 'DESC' if s =~ /\bdesc$/i }
|
684
|
+
order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ')
|
685
|
+
|
686
|
+
sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}"
|
687
|
+
end
|
688
|
+
|
689
|
+
protected
|
690
|
+
# Returns the version of the connected PostgreSQL version.
|
691
|
+
def postgresql_version
|
692
|
+
@postgresql_version ||=
|
693
|
+
if @connection.respond_to?(:server_version)
|
694
|
+
@connection.server_version
|
695
|
+
else
|
696
|
+
# Mimic PGconn.server_version behavior
|
697
|
+
begin
|
698
|
+
query('SELECT version()')[0][0] =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/
|
699
|
+
($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i
|
700
|
+
rescue
|
701
|
+
0
|
100
702
|
end
|
101
703
|
end
|
102
|
-
|
103
|
-
|
704
|
+
end
|
705
|
+
|
706
|
+
private
|
707
|
+
# The internal PostgreSQL identifer of the money data type.
|
708
|
+
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
|
709
|
+
|
710
|
+
# Connects to a PostgreSQL server and sets up the adapter depending on the
|
711
|
+
# connected server's characteristics.
|
712
|
+
def connect
|
713
|
+
@connection = PGconn.connect(*@connection_parameters)
|
714
|
+
PGconn.translate_results = false if PGconn.respond_to?(:translate_results=)
|
104
715
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
716
|
+
# Ignore async_exec and async_query when using postgres-pr.
|
717
|
+
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
|
718
|
+
|
719
|
+
# Use escape string syntax if available. We cannot do this lazily when encountering
|
720
|
+
# the first string, because that could then break any transactions in progress.
|
721
|
+
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
|
722
|
+
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
|
723
|
+
# support escape string syntax. Don't override the inherited quoted_string_prefix.
|
724
|
+
if supports_standard_conforming_strings?
|
725
|
+
self.class.instance_eval do
|
726
|
+
define_method(:quoted_string_prefix) { 'E' }
|
111
727
|
end
|
112
|
-
return [schema_name, table_name]
|
113
728
|
end
|
114
729
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
column_defaults = nil
|
127
|
-
log(sql, nil, @connection) { |connection| column_defaults = connection.query(sql) }
|
128
|
-
column_defaults.collect do |row|
|
129
|
-
field = row[0]
|
130
|
-
type = type_as_string(row[3], row[2])
|
131
|
-
default = default_value(row[1])
|
132
|
-
length = row[2]
|
133
|
-
|
134
|
-
[field, type, default, length]
|
730
|
+
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
731
|
+
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
732
|
+
# should know about this but can't detect it there, so deal with it here.
|
733
|
+
money_precision = (postgresql_version >= 80300) ? 19 : 10
|
734
|
+
PostgreSQLColumn.module_eval(<<-end_eval)
|
735
|
+
def extract_precision(sql_type)
|
736
|
+
if sql_type =~ /^money$/
|
737
|
+
#{money_precision}
|
738
|
+
else
|
739
|
+
super
|
740
|
+
end
|
135
741
|
end
|
136
|
-
|
742
|
+
end_eval
|
743
|
+
|
744
|
+
configure_connection
|
745
|
+
end
|
137
746
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
747
|
+
# Configures the encoding, verbosity, and schema search path of the connection.
|
748
|
+
# This is called by #connect and should not be called manually.
|
749
|
+
def configure_connection
|
750
|
+
if @config[:encoding]
|
751
|
+
if @connection.respond_to?(:set_client_encoding)
|
752
|
+
@connection.set_client_encoding(@config[:encoding])
|
753
|
+
else
|
754
|
+
execute("SET client_encoding TO '#{@config[:encoding]}'")
|
144
755
|
end
|
756
|
+
end
|
757
|
+
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
758
|
+
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
759
|
+
end
|
145
760
|
|
146
|
-
|
761
|
+
# Returns the current ID of a table's sequence.
|
762
|
+
def last_insert_id(table, sequence_name) #:nodoc:
|
763
|
+
Integer(select_value("SELECT currval('#{sequence_name}')"))
|
764
|
+
end
|
147
765
|
|
148
|
-
|
766
|
+
# Executes a SELECT query and returns the results, performing any data type
|
767
|
+
# conversions that are required to be performed here instead of in PostgreSQLColumn.
|
768
|
+
def select(sql, name = nil)
|
769
|
+
fields, rows = select_raw(sql, name)
|
770
|
+
result = []
|
771
|
+
for row in rows
|
772
|
+
row_hash = {}
|
773
|
+
fields.each_with_index do |f, i|
|
774
|
+
row_hash[f] = row[i]
|
775
|
+
end
|
776
|
+
result << row_hash
|
149
777
|
end
|
778
|
+
result
|
779
|
+
end
|
150
780
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
781
|
+
def select_raw(sql, name = nil)
|
782
|
+
res = execute(sql, name)
|
783
|
+
results = res.result
|
784
|
+
fields = []
|
785
|
+
rows = []
|
786
|
+
if results.length > 0
|
787
|
+
fields = res.fields
|
788
|
+
results.each do |row|
|
789
|
+
hashed_row = {}
|
790
|
+
row.each_index do |cell_index|
|
791
|
+
# If this is a money type column and there are any currency symbols,
|
792
|
+
# then strip them off. Indeed it would be prettier to do this in
|
793
|
+
# PostgreSQLColumn.string_to_decimal but would break form input
|
794
|
+
# fields that call value_before_type_cast.
|
795
|
+
if res.type(cell_index) == MONEY_COLUMN_TYPE_OID
|
796
|
+
# Because money output is formatted according to the locale, there are two
|
797
|
+
# cases to consider (note the decimal separators):
|
798
|
+
# (1) $12,345,678.12
|
799
|
+
# (2) $12.345.678,12
|
800
|
+
case column = row[cell_index]
|
801
|
+
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
|
802
|
+
row[cell_index] = column.gsub(/[^-\d\.]/, '')
|
803
|
+
when /^-?\D+[\d\.]+,\d{2}$/ # (2)
|
804
|
+
row[cell_index] = column.gsub(/[^-\d,]/, '').sub(/,/, '.')
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
hashed_row[fields[cell_index]] = column
|
809
|
+
end
|
810
|
+
rows << row
|
811
|
+
end
|
171
812
|
end
|
172
|
-
|
813
|
+
res.clear
|
814
|
+
return fields, rows
|
815
|
+
end
|
816
|
+
|
817
|
+
# Returns the list of a table's column names, data types, and default values.
|
818
|
+
#
|
819
|
+
# The underlying query is roughly:
|
820
|
+
# SELECT column.name, column.type, default.value
|
821
|
+
# FROM column LEFT JOIN default
|
822
|
+
# ON column.table_id = default.table_id
|
823
|
+
# AND column.num = default.column_num
|
824
|
+
# WHERE column.table_id = get_table_id('table_name')
|
825
|
+
# AND column.num > 0
|
826
|
+
# AND NOT column.is_dropped
|
827
|
+
# ORDER BY column.num
|
828
|
+
#
|
829
|
+
# If the table name is not prefixed with a schema, the database will
|
830
|
+
# take the first match from the schema search path.
|
831
|
+
#
|
832
|
+
# Query implementation notes:
|
833
|
+
# - format_type includes the column size constraint, e.g. varchar(50)
|
834
|
+
# - ::regclass is a function that gives the id for a table name
|
835
|
+
def column_definitions(table_name) #:nodoc:
|
836
|
+
query <<-end_sql
|
837
|
+
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
838
|
+
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
839
|
+
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
840
|
+
WHERE a.attrelid = '#{table_name}'::regclass
|
841
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
842
|
+
ORDER BY a.attnum
|
843
|
+
end_sql
|
844
|
+
end
|
173
845
|
end
|
174
846
|
end
|
175
|
-
rescue LoadError
|
176
|
-
# PostgreSQL driver is not availible
|
177
847
|
end
|