r1278@iwill: jeremy | 2005-06-12 05:11:48 -0700

Branch for PostgreSQL schema.  Ticket #827.
 r1281@iwill:  jeremy | 2005-06-12 19:06:43 -0700
 remove search_path from PostgreSQL db definition
 r1282@iwill:  jeremy | 2005-06-12 19:07:50 -0700
 Rakefile support for database-specific tests.
 r1283@iwill:  jeremy | 2005-06-12 19:10:18 -0700
 Add schema_search_path attribute to PostgreSQL adapter.  Replace table_structure with column_definitions which finds the given table_name in the schema search path.
 r1284@iwill:  jeremy | 2005-06-12 19:12:10 -0700
 Unit test PostgreSQL schema search path.
 r1285@iwill:  jeremy | 2005-06-12 19:12:20 -0700
 Changelog entry.
 r1286@iwill:  jeremy | 2005-06-12 20:08:20 -0700
 Don't try to quote schema names.  Include a reference to the PostgreSQL schema docs.
 r1287@iwill:  jeremy | 2005-06-12 20:16:07 -0700
 SchemasTest -> SchemaTest


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1407 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jeremy Kemper
2005-06-12 22:05:30 +00:00
parent c4a3634929
commit 1fde44bfee
5 changed files with 130 additions and 55 deletions

View File

@@ -1,5 +1,7 @@
*SVN*
* Comprehensive PostgreSQL schema support. Use the optional schema_search_path directive in database.yml to give a comma-separated list of schemas to search for your tables. This allows you, for example, to have tables in a shared schema without having to use a custom table name. See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html to learn more. #827 [dave@cherryville.org]
* Corrected @@configurations typo #1410 [david@ruppconsulting.com]
* Return PostgreSQL columns in the order they were declared #1374 [perlguy@gmail.com]

View File

@@ -28,20 +28,20 @@ task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_sqlit
Rake::TestTask.new("test_ruby_mysql") { |t|
t.libs << "test" << "test/connections/native_mysql"
t.pattern = 'test/*_test.rb'
t.pattern = 'test/*_test{,_mysql}.rb'
t.verbose = true
}
Rake::TestTask.new("test_mysql_ruby") { |t|
t.libs << "test" << "test/connections/native_mysql"
t.pattern = 'test/*_test.rb'
t.pattern = 'test/*_test{,_mysql}.rb'
t.verbose = true
}
for adapter in %w( postgresql sqlite sqlite3 sqlserver db2 oci )
Rake::TestTask.new("test_#{adapter}") { |t|
t.libs << "test" << "test/connections/native_#{adapter}"
t.pattern = 'test/*_test.rb'
t.pattern = "test/*_test{,_#{adapter}}.rb"
t.verbose = true
}
end

View File

@@ -24,7 +24,6 @@ module ActiveRecord
username = config[:username].to_s
password = config[:password].to_s
schema_order = config[:schema_order]
encoding = config[:encoding]
min_messages = config[:min_messages]
@@ -38,7 +37,7 @@ module ActiveRecord
PGconn.connect(host, port, "", "", database, username, password), logger
)
pga.execute("SET search_path TO #{schema_order}") if schema_order
pga.schema_search_path = config[:schema_search_path] || config[:schema_order]
pga.execute("SET client_encoding TO '#{encoding}'") if encoding
pga.execute("SET client_min_messages TO '#{min_messages}'") if min_messages
@@ -57,7 +56,7 @@ module ActiveRecord
# * <tt>:username</tt> -- Defaults to nothing
# * <tt>:password</tt> -- Defaults to nothing
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
# * <tt>:schema_order</tt> -- An optional schema order string that is using in a SET search_path TO <schema_order> call on connection.
# * <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.
# * <tt>:encoding</tt> -- An optional client encoding that is using in a SET client_encoding TO <encoding> call on connection.
# * <tt>:min_messages</tt> -- An optional client min messages that is using in a SET client_min_messages TO <min_messages> call on connection.
class PostgreSQLAdapter < AbstractAdapter
@@ -71,9 +70,8 @@ module ActiveRecord
end
def columns(table_name, name = nil)
table_structure(table_name).inject([]) do |columns, field|
columns << Column.new(field[0], field[2], field[1])
columns
column_definitions(table_name).collect do |name, type, default|
Column.new(name, default_value(default), translate_field_type(type))
end
end
@@ -110,14 +108,32 @@ module ActiveRecord
end
def quote_column_name(name)
return "\"#{name}\""
%("#{name}")
end
def adapter_name()
'PostgreSQL'
end
# Set the schema search path to a string of comma-separated schema names.
# Names beginning with $ are quoted (e.g. $user => '$user')
# See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html
def schema_search_path=(schema_csv)
if schema_csv
execute "SET search_path TO #{schema_csv}"
@schema_search_path = nil
end
end
def schema_search_path
@schema_search_path ||= query('SHOW search_path')[0][0]
end
private
BYTEA_COLUMN_TYPE_OID = 17
def last_insert_id(table, column = "id")
sequence_name = "#{table}_#{column || 'id'}_seq"
@connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
@@ -133,7 +149,7 @@ module ActiveRecord
hashed_row = {}
row.each_index do |cel_index|
column = row[cel_index]
if res.type(cel_index) == 17 # type oid for bytea
if res.type(cel_index) == BYTEA_COLUMN_TYPE_OID
column = unescape_bytea(column)
end
hashed_row[fields[cel_index]] = column
@@ -156,53 +172,49 @@ module ActiveRecord
s.gsub(/\\([0-9][0-9][0-9])/) { $1.oct.chr }.gsub(/\\\\/) { '\\' } unless s.nil?
end
def split_table_schema(table_name)
schema_split = table_name.split('.')
schema_name = "public"
if schema_split.length > 1
schema_name = schema_split.first.strip
table_name = schema_split.last.strip
end
return [schema_name, table_name]
# Query a table's column names, default values, and types.
#
# The underlying query is roughly:
# SELECT column.name, column.type, default.value
# FROM column LEFT JOIN default
# ON column.table_id = default.table_id
# AND column.num = default.column_num
# WHERE column.table_id = get_table_id('table_name')
# AND column.num > 0
# AND NOT column.is_dropped
# ORDER BY column.num
#
# If the table name is not prefixed with a schema, the database will
# take the first match from the schema search path.
#
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name)
query <<-end_sql
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{table_name}'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
end_sql
end
def table_structure(table_name)
database_name = @connection.db
schema_name, table_name = split_table_schema(table_name)
# Grab a list of all the default values for the columns.
sql = "SELECT column_name, column_default, character_maximum_length, data_type "
sql << " FROM information_schema.columns "
sql << " WHERE table_catalog = '#{database_name}' "
sql << " AND table_schema = '#{schema_name}' "
sql << " AND table_name = '#{table_name}'"
sql << " ORDER BY ordinal_position"
query(sql).collect do |row|
field = row[0]
type = type_as_string(row[3], row[2])
default = default_value(row[1])
length = row[2]
[field, type, default, length]
# Translate PostgreSQL-specific types into simplified SQL types.
# These are special cases; standard types are handled by
# ConnectionAdapters::Column#simplified_type.
def translate_field_type(field_type)
# Match the beginning of field_type since it may have a size constraint on the end.
case field_type
when /^timestamp/i then 'datetime'
when /^real|^money/i then 'float'
when /^interval/i then 'string'
when /^bytea/i then 'binary'
else field_type # Pass through standard types.
end
end
def type_as_string(field_type, field_length)
type = case field_type
when 'numeric', 'real', 'money' then 'float'
when 'character varying', 'interval' then 'string'
when 'timestamp without time zone' then 'datetime'
when 'timestamp with time zone' then 'datetime'
when 'bytea' then 'binary'
else field_type
end
size = field_length.nil? ? "" : "(#{field_length})"
return type + size
end
def default_value(value)
# Boolean types
return "t" if value =~ /true/i

View File

@@ -1,5 +1,3 @@
SET search_path = public, pg_catalog;
CREATE TABLE accounts (
id serial,
firm_id integer,

View File

@@ -0,0 +1,63 @@
require 'abstract_unit'
class SchemaTest < Test::Unit::TestCase
SCHEMA_NAME = 'test_schema'
TABLE_NAME = 'things'
COLUMNS = [
'id integer',
'name character varying(50)',
'moment timestamp without time zone default now()'
]
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
end
def teardown
@connection.execute "DROP TABLE #{SCHEMA_NAME}.#{TABLE_NAME}"
@connection.execute "DROP SCHEMA #{SCHEMA_NAME}"
end
def test_with_schema_prefixed_table_name
assert_nothing_raised do
assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}")
end
end
def test_with_schema_search_path
assert_nothing_raised do
with_schema_search_path(SCHEMA_NAME) do
assert_equal COLUMNS, columns(TABLE_NAME)
end
end
end
def test_raise_on_unquoted_schema_name
assert_raise(ActiveRecord::StatementInvalid) do
with_schema_search_path '$user,public'
end
end
def test_without_schema_search_path
assert_raise(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) }
end
def test_ignore_nil_schema_search_path
assert_nothing_raised { with_schema_search_path nil }
end
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
"#{name} #{type}" + (default ? " default #{default}" : '')
end
end
def with_schema_search_path(schema_search_path)
@connection.schema_search_path = schema_search_path
yield if block_given?
ensure
@connection.schema_search_path = "'$user', public"
end
end