cartodb/lib/carto/connector/providers/pg_fdw.rb
2020-06-15 10:58:47 +08:00

187 lines
6.4 KiB
Ruby

require_relative './fdw'
module Carto
class Connector
# PostgreSQL provider using Postgres-FDW: https://www.postgresql.org/docs/current/static/postgres-fdw.html
#
# options:
# * https://www.postgresql.org/docs/current/static/postgres-fdw.html#AEN174711
# * https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS
#
# Requirements:
# * postgres_fdw extension must be installed in the user database
#
# Parameters:
#
# * schema: schema name of the remote schema
# * table: name of the remote table to import (also name of imported result table)
# * server: PostgreSQL server name or address
# * port: PostgreSQL server port
# * database: remote database name to connect to
# * username: user name for athentication
# * password: password for athentication
#
# This is not intended for production at the moment, this class is here to validate and test the
# Connector hierarchy design.
#
# Note that the password parameter is required: postgres_fdw won't allow non-superusers to connect
# to foreign servers that don't require a password (and table copy is performed using a non-superuser).
#
# TODO: add support for sql_query parameter
#
class PgFdwProvider < FdwProvider
def initialize(context, params)
super
end
REQUIRED_PARAMETERS = %w(table server database username password).freeze
OPTIONAL_PARAMETERS = %w(schema port).freeze
def optional_parameters
OPTIONAL_PARAMETERS
end
def required_parameters
REQUIRED_PARAMETERS
end
def table_name
@params[:table]
end
def foreign_table_name_for(server_name, name = nil)
fdw_adjusted_table_name("#{unique_prefix_for(server_name)}#{name || table_name}")
end
def unique_prefix_for(server_name)
# server_name should already be unique
"#{server_name}_"
end
def remote_schema_name
schema = @params[:schema]
schema = 'public' if schema.blank?
schema
end
def fdw_create_server(server_name)
execute_as_superuser fdw_create_server_sql('postgres_fdw', server_name, server_options)
end
def fdw_create_usermaps(server_name)
execute_as_superuser fdw_create_usermap_sql(server_name, @connector_context.database_username, user_options)
execute_as_superuser fdw_create_usermap_sql(server_name, 'postgres', user_options)
end
def fdw_create_foreign_table(server_name)
remote_table = table_name
foreign_table = foreign_table_name_for(server_name)
options = table_options
schema = foreign_table_schema
cmds = []
cmds << fdw_import_foreign_schema_limited_sql(server_name, remote_schema_name, schema, remote_table, options)
if remote_table != foreign_table
cmds << fdw_rename_foreign_table_sql(schema, remote_table, foreign_table)
end
cmds << fdw_grant_select_sql(schema, foreign_table, @connector_context.database_username)
execute_as_superuser cmds.join("\n")
foreign_table
end
def fdw_list_tables(server_name, limit)
# Create auxiliar foreign tables for pg_class, pg_namespace
ext_pg_class = foreign_table_name_for(server_name, 'pg_class')
ext_pg_namespace = foreign_table_name_for(server_name, 'pg_namespace')
commands = []
commands << fdw_create_foreign_table_if_not_exists_sql(
server_name, foreign_table_schema, ext_pg_class,
['relname name', 'relnamespace oid', 'relkind char'],
schema_name: 'pg_catalog', table_name: 'pg_class'
)
commands << fdw_create_foreign_table_if_not_exists_sql(
server_name, foreign_table_schema, ext_pg_namespace,
['nspname name', 'oid oid'],
schema_name: 'pg_catalog', table_name: 'pg_namespace'
)
limit_clause = limit.to_i > 0 ? "LIMIT #{limit}" : ''
execute_as_superuser(commands.join("\n"))
execute_as_superuser %{
SELECT n.nspname AS schema, c.relname AS name
FROM #{fdw_qualified_table_name(foreign_table_schema, ext_pg_class)} c
JOIN #{fdw_qualified_table_name(foreign_table_schema, ext_pg_namespace)} n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schema, name
#{limit_clause};
}
ensure
# Drop auxiliar foreign tables for pg_class, pg_namespace
commands = []
commands << fdw_drop_foreign_table_sql(foreign_table_schema, ext_pg_namespace) if ext_pg_namespace
commands << fdw_drop_foreign_table_sql(foreign_table_schema, ext_pg_class) if ext_pg_class
execute_as_superuser(commands.join("\n"))
end
def fdw_check_connection(server_name)
fdw_list_tables(server_name, 1)
true
end
def features_information
{
"sql_queries": false,
"list_databases": false,
"list_tables": true,
"preview_table": false
}
end
private
ATTRIBUTE_NAMES = {
# 'schema' => 'schema_name', # for CREATE FOREIGN TABLE, remote schema name if different from local
# 'table' => 'table_name', # for CREATE FOREIGN TABLE, remote taable name if different from local
'server' => 'host',
'database' => 'dbname',
'port' => 'port',
'username' => 'user',
'password' => 'password'
}.freeze
NON_ATTRIBUTES = %w(schema table provider).freeze
def attribute_name(parameter_name, value)
attribute_name = ATTRIBUTE_NAMES[parameter_name.to_s.downcase] || parameter_name.to_s
if attribute_name == 'host' && IpChecker.is_ip?(value)
attribute_name = 'hostaddr'
end
attribute_name
end
def connection_attributes
@params.except(*NON_ATTRIBUTES).map { |k, v| [attribute_name(k, v), v] }
end
SERVER_OPTIONS = %w(host hostaddr port dbname).freeze
USERMAP_OPTIONS = %w(user password).freeze
def server_options
connection_attributes.slice(*SERVER_OPTIONS).parameters
end
def user_options
connection_attributes.slice(*USERMAP_OPTIONS).parameters
end
def table_options
connection_attributes.except(*(SERVER_OPTIONS + USERMAP_OPTIONS)).parameters
end
end
end
end