Skip to content

Commit

Permalink
pg, PostgreSQL::Name to hold schema qualified names.
Browse files Browse the repository at this point in the history
  • Loading branch information
senny committed May 30, 2014
1 parent 6963e33 commit 7b8d95d
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 19 deletions.
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -150,12 +150,7 @@ def quote_string(s) #:nodoc:
# - "schema.name".table_name # - "schema.name".table_name
# - "schema.name"."table.name" # - "schema.name"."table.name"
def quote_table_name(name) def quote_table_name(name)
schema, table = Utils.extract_schema_and_table(name.to_s) Utils.extract_schema_qualified_name(name.to_s).quoted
if schema
"#{quote_column_name(schema)}.#{quote_column_name(table)}"
else
quote_column_name(table)
end
end end


def quote_table_name_for_assignment(table, attr) def quote_table_name_for_assignment(table, attr)
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -97,16 +97,16 @@ def tables(name = nil)
# If the schema is not specified as part of +name+ then it will only find tables within # If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas) # the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name) def table_exists?(name)
schema, table = Utils.extract_schema_and_table(name.to_s) name = Utils.extract_schema_qualified_name(name.to_s)
return false unless table return false unless name.identifier


exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*) SELECT COUNT(*)
FROM pg_class c FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' AND c.relname = '#{name.identifier}'
AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL SQL
end end


Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -1,23 +1,64 @@
module ActiveRecord module ActiveRecord
module ConnectionAdapters module ConnectionAdapters
module PostgreSQL module PostgreSQL
# Value Object to hold a schema qualified name.
# This is usually the name of a PostgreSQL relation but it can also represent
# schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
# double quoting.
class Name # :nodoc:
SEPARATOR = "."
attr_reader :schema, :identifier

def initialize(schema, identifier)
@schema, @identifier = unquote(schema), unquote(identifier)
end

def to_s
parts.join SEPARATOR
end

def quoted
parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR
end

def ==(o)
o.class == self.class && o.parts == parts
end
alias_method :eql?, :==

def hash
parts.hash
end

protected
def unquote(part)
return unless part
part.gsub(/(^"|"$)/,'')
end

def parts
@parts ||= [@schema, @identifier].compact
end
end

module Utils # :nodoc: module Utils # :nodoc:
extend self extend self


# Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
# +schema_name+ is nil if not specified in +name+. # extracted from +string+.
# +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) # +schema+ is nil if not specified in +string+.
# +name+ supports the range of schema/table references understood by PostgreSQL, for example: # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
# +string+ supports the range of schema/table references understood by PostgreSQL, for example:
# #
# * <tt>table_name</tt> # * <tt>table_name</tt>
# * <tt>"table.name"</tt> # * <tt>"table.name"</tt>
# * <tt>schema_name.table_name</tt> # * <tt>schema_name.table_name</tt>
# * <tt>schema_name."table.name"</tt> # * <tt>schema_name."table.name"</tt>
# * <tt>"schema_name".table_name</tt> # * <tt>"schema_name".table_name</tt>
# * <tt>"schema.name"."table name"</tt> # * <tt>"schema.name"."table name"</tt>
def extract_schema_and_table(name) def extract_schema_qualified_name(string)
table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse
[schema, table] PostgreSQL::Name.new(schema, table)
end end
end end
end end
Expand Down
45 changes: 43 additions & 2 deletions activerecord/test/cases/adapters/postgresql/utils_test.rb
Original file line number Original file line Diff line number Diff line change
@@ -1,9 +1,10 @@
require 'cases/helper' require 'cases/helper'


class PostgreSQLUtilsTest < ActiveSupport::TestCase class PostgreSQLUtilsTest < ActiveSupport::TestCase
Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils


def test_extract_schema_and_table def test_extract_schema_qualified_name
{ {
%(table_name) => [nil,'table_name'], %(table_name) => [nil,'table_name'],
%("table.name") => [nil,'table.name'], %("table.name") => [nil,'table.name'],
Expand All @@ -14,7 +15,47 @@ def test_extract_schema_and_table
%("even spaces".table) => ['even spaces','table'], %("even spaces".table) => ['even spaces','table'],
%(schema."table.name") => ['schema', 'table.name'] %(schema."table.name") => ['schema', 'table.name']
}.each do |given, expect| }.each do |given, expect|
assert_equal expect, extract_schema_and_table(given) assert_equal Name.new(*expect), extract_schema_qualified_name(given)
end end
end end
end end

class PostgreSQLNameTest < ActiveSupport::TestCase
Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name

test "represents itself as schema.name" do
obj = Name.new("public", "articles")
assert_equal "public.articles", obj.to_s
end

test "without schema, represents itself as name only" do
obj = Name.new(nil, "articles")
assert_equal "articles", obj.to_s
end

test "quoted returns a string representation usable in a query" do
assert_equal %("articles"), Name.new(nil, "articles").quoted
assert_equal %("public"."articles"), Name.new("public", "articles").quoted
end

test "prevents double quoting" do
name = Name.new('"quoted_schema"', '"quoted_table"')
assert_equal "quoted_schema.quoted_table", name.to_s
assert_equal %("quoted_schema"."quoted_table"), name.quoted
end

test "equality based on state" do
assert_equal Name.new("access", "users"), Name.new("access", "users")
assert_equal Name.new(nil, "users"), Name.new(nil, "users")
assert_not_equal Name.new(nil, "users"), Name.new("access", "users")
assert_not_equal Name.new("access", "users"), Name.new("public", "users")
assert_not_equal Name.new("public", "users"), Name.new("public", "articles")
end

test "can be used as hash key" do
hash = {Name.new("schema", "article_seq") => "success"}
assert_equal "success", hash[Name.new("schema", "article_seq")]
assert_equal nil, hash[Name.new("schema", "articles")]
assert_equal nil, hash[Name.new("public", "article_seq")]
end
end

0 comments on commit 7b8d95d

Please sign in to comment.