From 26b22a9bf486b6c11857198a4fa2fb5439cbbe10 Mon Sep 17 00:00:00 2001 From: Carla Iriberri Date: Thu, 18 Aug 2016 15:36:32 +0200 Subject: [PATCH 1/7] Use explicit functions for query construction and metadata --- src/pg/sql/50_table_level_functions.sql | 120 +++++++++++++++--------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/src/pg/sql/50_table_level_functions.sql b/src/pg/sql/50_table_level_functions.sql index 27cf678..94d91cf 100644 --- a/src/pg/sql/50_table_level_functions.sql +++ b/src/pg/sql/50_table_level_functions.sql @@ -37,26 +37,34 @@ AS $$ DECLARE colnames text[]; coltypes text[]; - requested_measures text[]; - measure text; BEGIN - - -- Simple mock, there should be real logic in here. - - IF $3 NOT ILIKE 'GetMeasure' OR $3 IS NULL THEN - RAISE 'This function is not supported yet: %', $3; + IF $3 ILIKE 'OBS_GetMeasure' THEN + SELECT r.colnames::text[], r.coltypes::text[] INTO colnames, coltypes + FROM cdb_observatory._OBS_GetMeasureResultMetadata(params) r + LIMIT 1; + ELSE + RAISE 'This function is not supported yet: %', $3; END IF; + RETURN (colnames::text[], coltypes::text[]); +END; +$$ LANGUAGE plpgsql; - SELECT translate($4::json->>'tag_name','[]', '{}')::text[] INTO requested_measures; +CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetMeasureResultMetadata(params json) +RETURNS cdb_observatory.ds_return_metadata +AS $$ +DECLARE + colnames text[]; -- Array to store the name of the measures to be returned + coltypes text[]; -- Array to store the type of the measures to be returned + requested_measures text[]; + measure_id text; +BEGIN + -- By definition, all the measure results for the OBS_GetMeasure API are numeric values + SELECT translate($1::json->>'measure_id','[]', '{}')::text[] INTO requested_measures; - FOREACH measure IN ARRAY requested_measures + FOREACH measure_id IN ARRAY requested_measures LOOP - IF NOT measure ILIKE ANY (Array['total_pop', 'pop_16_over']::text[]) THEN - RAISE 'This measure is not supported yet: %', measure; - END IF; - SELECT array_append(colnames, measure) INTO colnames; - SELECT array_append(coltypes, 'double precision'::text) INTO coltypes; - + SELECT array_append(colnames, measure_id) INTO colnames; + SELECT array_append(coltypes, 'numeric'::text) INTO coltypes; END LOOP; RETURN (colnames::text[], coltypes::text[]); @@ -68,45 +76,71 @@ RETURNS SETOF record AS $$ DECLARE data_query text; - tag_name text[]; - tag text; - tags_list text; - tags_query text; rec RECORD; BEGIN - SELECT translate($6::json->>'tag_name','[]', '{}')::text[] INTO tag_name; - SELECT array_to_string(tag_name, ',') INTO tags_list; - tags_query := ''; - - FOREACH tag IN ARRAY tag_name - LOOP - SELECT tags_query || ' sum(' || tag || '/fraction)::double precision as ' || tag || ', ' INTO tags_query; - - END LOOP; - - -- Simple mock, there should be real logic in here. - data_query := '(WITH _areas AS(SELECT ST_Area(a.the_geom::geography)' - || '/ (1000 * 1000) as fraction, a.geoid, b.cartodb_id FROM ' - || 'observatory.obs_c6fb99c47d61289fbb8e561ff7773799d3fcc308 as a, ' - || table_schema || '.' || table_name || ' AS b ' - || 'WHERE b.the_geom && a.the_geom ), values AS (SELECT geoid, ' - || tags_list - || ' FROM observatory.obs_1a098da56badf5f32e336002b0a81708c40d29cd ) ' - || 'SELECT ' - || tags_query - || ' cartodb_id::int FROM _areas, values ' - || 'WHERE values.geoid = _areas.geoid GROUP BY cartodb_id);'; - + SELECT cdb_observatory._OBS_GetMeasureQuery(table_schema, table_name, params) INTO data_query; FOR rec IN EXECUTE data_query LOOP RETURN NEXT rec; END LOOP; - RETURN; + RETURN; END; $$ LANGUAGE plpgsql SECURITY DEFINER; +CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetMeasureQuery(table_schema text, table_name text, params json) +RETURNS text +AS $$ +DECLARE + data_query text; + measure_ids_arr text[]; + measure_id text; + measures_list text; + measures_query text; + normalize text; + boundary_id text; + time_span text; + geom_table_name text; + data_table_name text; +BEGIN + measures_query := ''; + -- SELECT table_name from obs_meta WHERE boundary_id = {bound} AND [...] INTO geom_table_name + geom_table_name := 'observatory.obs_c6fb99c47d61289fbb8e561ff7773799d3fcc308'; + -- SELECT table_name from obs_meta WHERE time_span = {time} AND [...] INTO data_table_name + data_table_name := 'observatory.obs_1a098da56badf5f32e336002b0a81708c40d29cd'; + + -- Get measure_ids array from JSON + SELECT translate($3::json->>'measure_id','[]', '{}')::text[] INTO measure_ids_arr; + + -- Get a comma-separated list of measures ("total_pop, over_16_pop") to be used in SELECTs + SELECT array_to_string(measure_ids_arr, ',') INTO measures_list; + + FOREACH measure_id IN ARRAY measure_ids_arr + LOOP + -- Build query to compute each value and normalize + -- Assumes the default normalization method, the normalize parameter given in the JSON + -- should be checked in order to build the final query + SELECT measures_query || ' sum(' || measure_id || '/fraction)::numeric as ' || measure_id || ', ' INTO measures_query; + END LOOP; + + -- Data query should select the measures and the cartodb_id of the user table, in that order. + data_query := '(WITH _areas AS(SELECT ST_Area(a.the_geom::geography)' + || '/ (1000 * 1000) as fraction, a.geoid, b.cartodb_id FROM ' + || geom_table_name || ' as a, ' + || table_schema || '.' || table_name || ' AS b ' + || 'WHERE b.the_geom && a.the_geom ), values AS (SELECT geoid, ' + || measures_list + || ' FROM ' || data_table_name || ' ) ' + || 'SELECT ' + || measures_query + || ' cartodb_id::int FROM _areas, values ' + || 'WHERE values.geoid = _areas.geoid GROUP BY cartodb_id);'; + RETURN data_query; +END; +$$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION cdb_observatory._OBS_DisconnectUserTable(username text, orgname text, table_schema text, table_name text, servername text) RETURNS boolean AS $$ From 30f27e5b585af505484dfd93177e637a6f139660 Mon Sep 17 00:00:00 2001 From: Carla Iriberri Date: Thu, 18 Aug 2016 15:43:03 +0200 Subject: [PATCH 2/7] Check function name and use param names instead of --- src/pg/sql/50_table_level_functions.sql | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pg/sql/50_table_level_functions.sql b/src/pg/sql/50_table_level_functions.sql index 94d91cf..4bce532 100644 --- a/src/pg/sql/50_table_level_functions.sql +++ b/src/pg/sql/50_table_level_functions.sql @@ -38,12 +38,12 @@ DECLARE colnames text[]; coltypes text[]; BEGIN - IF $3 ILIKE 'OBS_GetMeasure' THEN + IF function_name ILIKE 'OBS_GetMeasure' THEN SELECT r.colnames::text[], r.coltypes::text[] INTO colnames, coltypes FROM cdb_observatory._OBS_GetMeasureResultMetadata(params) r LIMIT 1; ELSE - RAISE 'This function is not supported yet: %', $3; + RAISE 'This function is not supported yet: %', function_name; END IF; RETURN (colnames::text[], coltypes::text[]); END; @@ -78,7 +78,13 @@ DECLARE data_query text; rec RECORD; BEGIN - SELECT cdb_observatory._OBS_GetMeasureQuery(table_schema, table_name, params) INTO data_query; + + IF function_name ILIKE 'OBS_GetMeasure' THEN + SELECT cdb_observatory._OBS_GetMeasureQuery(table_schema, table_name, params) INTO data_query; + ELSE + RAISE 'Function not supported'; + END IF; + FOR rec IN EXECUTE data_query LOOP @@ -111,7 +117,7 @@ BEGIN data_table_name := 'observatory.obs_1a098da56badf5f32e336002b0a81708c40d29cd'; -- Get measure_ids array from JSON - SELECT translate($3::json->>'measure_id','[]', '{}')::text[] INTO measure_ids_arr; + SELECT translate(params::json->>'measure_id','[]', '{}')::text[] INTO measure_ids_arr; -- Get a comma-separated list of measures ("total_pop, over_16_pop") to be used in SELECTs SELECT array_to_string(measure_ids_arr, ',') INTO measures_list; From 48d1bfdb132f8f69c75fedff00b5418456fad00e Mon Sep 17 00:00:00 2001 From: Carla Iriberri Date: Fri, 19 Aug 2016 12:45:38 +0200 Subject: [PATCH 3/7] Remove JSON manipulation to use json functions --- src/pg/sql/50_table_level_functions.sql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pg/sql/50_table_level_functions.sql b/src/pg/sql/50_table_level_functions.sql index 4bce532..b44db0c 100644 --- a/src/pg/sql/50_table_level_functions.sql +++ b/src/pg/sql/50_table_level_functions.sql @@ -59,7 +59,7 @@ DECLARE measure_id text; BEGIN -- By definition, all the measure results for the OBS_GetMeasure API are numeric values - SELECT translate($1::json->>'measure_id','[]', '{}')::text[] INTO requested_measures; + SELECT ARRAY(SELECT json_array_elements_text(params->'measure_id'))::text[] INTO requested_measures; FOREACH measure_id IN ARRAY requested_measures LOOP @@ -94,7 +94,6 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY DEFINER; - CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetMeasureQuery(table_schema text, table_name text, params json) RETURNS text AS $$ @@ -117,7 +116,7 @@ BEGIN data_table_name := 'observatory.obs_1a098da56badf5f32e336002b0a81708c40d29cd'; -- Get measure_ids array from JSON - SELECT translate(params::json->>'measure_id','[]', '{}')::text[] INTO measure_ids_arr; + SELECT ARRAY(SELECT json_array_elements_text(params->'measure_id'))::text[] INTO measure_ids_arr; -- Get a comma-separated list of measures ("total_pop, over_16_pop") to be used in SELECTs SELECT array_to_string(measure_ids_arr, ',') INTO measures_list; From 62c2693553710837ebb9a48c0b8b7d0c04487590 Mon Sep 17 00:00:00 2001 From: Carla Iriberri Date: Fri, 19 Aug 2016 13:04:54 +0200 Subject: [PATCH 4/7] Avoid function check to dispatch --- src/pg/sql/50_table_level_functions.sql | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pg/sql/50_table_level_functions.sql b/src/pg/sql/50_table_level_functions.sql index b44db0c..32f4ceb 100644 --- a/src/pg/sql/50_table_level_functions.sql +++ b/src/pg/sql/50_table_level_functions.sql @@ -38,13 +38,9 @@ DECLARE colnames text[]; coltypes text[]; BEGIN - IF function_name ILIKE 'OBS_GetMeasure' THEN - SELECT r.colnames::text[], r.coltypes::text[] INTO colnames, coltypes - FROM cdb_observatory._OBS_GetMeasureResultMetadata(params) r - LIMIT 1; - ELSE - RAISE 'This function is not supported yet: %', function_name; - END IF; + EXECUTE FORMAT('SELECT r.colnames::text[], r.coltypes::text[] FROM cdb_observatory._%sResultMetadata(%L::json) r', function_name, params::text) + INTO colnames, coltypes; + RETURN (colnames::text[], coltypes::text[]); END; $$ LANGUAGE plpgsql; From dd5f560359360ea3c1d9609d9684ff7d8ff36c15 Mon Sep 17 00:00:00 2001 From: Carla Iriberri Date: Fri, 19 Aug 2016 16:39:30 +0200 Subject: [PATCH 5/7] Separate functions between files --- src/pg/sql/50_table_level_framework.sql | 77 +++++++++++++++++ ...tions.sql => 51_table_level_functions.sql} | 85 ++----------------- 2 files changed, 82 insertions(+), 80 deletions(-) create mode 100644 src/pg/sql/50_table_level_framework.sql rename src/pg/sql/{50_table_level_functions.sql => 51_table_level_functions.sql} (50%) diff --git a/src/pg/sql/50_table_level_framework.sql b/src/pg/sql/50_table_level_framework.sql new file mode 100644 index 0000000..a42a827 --- /dev/null +++ b/src/pg/sql/50_table_level_framework.sql @@ -0,0 +1,77 @@ +CREATE TYPE cdb_observatory.ds_fdw_metadata as (schemaname text, tabname text, servername text); +CREATE TYPE cdb_observatory.ds_return_metadata as (colnames text[], coltypes text[]); + +CREATE OR REPLACE FUNCTION cdb_observatory._OBS_ConnectUserTable(username text, orgname text, user_db_role text, input_schema text, dbname text, host_addr text, table_name text) +RETURNS cdb_observatory.ds_fdw_metadata +AS $$ +DECLARE + fdw_server text; + fdw_import_schema text; + connection_str json; + import_foreign_schema_q text; + epoch_timestamp text; +BEGIN + + SELECT extract(epoch from now() at time zone 'utc')::int INTO epoch_timestamp; + fdw_server := 'fdw_server_' || username || '_' || epoch_timestamp; + fdw_import_schema:= fdw_server; + + -- Import foreign table + EXECUTE FORMAT ('SELECT cdb_observatory._OBS_ConnectRemoteTable(%L, %L, %L, %L, %L, %L, %L)', fdw_server, fdw_import_schema, dbname, host_addr, user_db_role, table_name, input_schema); + + RETURN (fdw_import_schema::text, table_name::text, fdw_server::text); + +EXCEPTION + WHEN others THEN + -- Disconnect user imported table. Delete schema and FDW server. + EXECUTE 'DROP FOREIGN TABLE IF EXISTS ' || fdw_import_schema || '.' || table_name; + EXECUTE 'DROP SCHEMA IF EXISTS ' || fdw_import_schema || ' CASCADE'; + EXECUTE 'DROP SERVER IF EXISTS ' || fdw_server || ' CASCADE;'; + RETURN (null, null, null); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetReturnMetadata(username text, orgname text, function_name text, params json) +RETURNS cdb_observatory.ds_return_metadata +AS $$ +DECLARE + colnames text[]; + coltypes text[]; +BEGIN + EXECUTE FORMAT('SELECT r.colnames::text[], r.coltypes::text[] FROM cdb_observatory._%sResultMetadata(%L::json) r', function_name, params::text) + INTO colnames, coltypes; + + RETURN (colnames::text[], coltypes::text[]); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION cdb_observatory._OBS_FetchJoinFdwTableData(username text, orgname text, table_schema text, table_name text, function_name text, params json) +RETURNS SETOF record +AS $$ +DECLARE + data_query text; + rec RECORD; +BEGIN + + EXECUTE FORMAT('SELECT cdb_observatory._%sQuery(%L, %L, %L::json)', function_name, table_schema, table_name, params::text) + INTO data_query; + + FOR rec IN EXECUTE data_query + LOOP + RETURN NEXT rec; + END LOOP; + RETURN; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + + +CREATE OR REPLACE FUNCTION cdb_observatory._OBS_DisconnectUserTable(username text, orgname text, table_schema text, table_name text, servername text) +RETURNS boolean +AS $$ +BEGIN + EXECUTE 'DROP FOREIGN TABLE IF EXISTS "' || table_schema || '".' || table_name; + EXECUTE 'DROP SCHEMA IF EXISTS ' || table_schema || ' CASCADE'; + EXECUTE 'DROP SERVER IF EXISTS ' || servername || ' CASCADE;'; + RETURN true; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; diff --git a/src/pg/sql/50_table_level_functions.sql b/src/pg/sql/51_table_level_functions.sql similarity index 50% rename from src/pg/sql/50_table_level_functions.sql rename to src/pg/sql/51_table_level_functions.sql index 32f4ceb..ba85cbc 100644 --- a/src/pg/sql/50_table_level_functions.sql +++ b/src/pg/sql/51_table_level_functions.sql @@ -1,49 +1,8 @@ -CREATE TYPE cdb_observatory.ds_fdw_metadata as (schemaname text, tabname text, servername text); -CREATE TYPE cdb_observatory.ds_return_metadata as (colnames text[], coltypes text[]); - -CREATE OR REPLACE FUNCTION cdb_observatory._OBS_ConnectUserTable(username text, orgname text, user_db_role text, input_schema text, dbname text, host_addr text, table_name text) -RETURNS cdb_observatory.ds_fdw_metadata -AS $$ -DECLARE - fdw_server text; - fdw_import_schema text; - connection_str json; - import_foreign_schema_q text; - epoch_timestamp text; -BEGIN - - SELECT extract(epoch from now() at time zone 'utc')::int INTO epoch_timestamp; - fdw_server := 'fdw_server_' || username || '_' || epoch_timestamp; - fdw_import_schema:= fdw_server; - - -- Import foreign table - EXECUTE FORMAT ('SELECT cdb_observatory._OBS_ConnectRemoteTable(%L, %L, %L, %L, %L, %L, %L)', fdw_server, fdw_import_schema, dbname, host_addr, user_db_role, table_name, input_schema); - - RETURN (fdw_import_schema::text, table_name::text, fdw_server::text); - -EXCEPTION - WHEN others THEN - -- Disconnect user imported table. Delete schema and FDW server. - EXECUTE 'DROP FOREIGN TABLE IF EXISTS ' || fdw_import_schema || '.' || table_name; - EXECUTE 'DROP SCHEMA IF EXISTS ' || fdw_import_schema || ' CASCADE'; - EXECUTE 'DROP SERVER IF EXISTS ' || fdw_server || ' CASCADE;'; - RETURN (null, null, null); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetReturnMetadata(username text, orgname text, function_name text, params json) -RETURNS cdb_observatory.ds_return_metadata -AS $$ -DECLARE - colnames text[]; - coltypes text[]; -BEGIN - EXECUTE FORMAT('SELECT r.colnames::text[], r.coltypes::text[] FROM cdb_observatory._%sResultMetadata(%L::json) r', function_name, params::text) - INTO colnames, coltypes; - - RETURN (colnames::text[], coltypes::text[]); -END; -$$ LANGUAGE plpgsql; +-- +-- +-- OBS_GetMeasure +-- +-- CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetMeasureResultMetadata(params json) RETURNS cdb_observatory.ds_return_metadata @@ -67,29 +26,6 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION cdb_observatory._OBS_FetchJoinFdwTableData(username text, orgname text, table_schema text, table_name text, function_name text, params json) -RETURNS SETOF record -AS $$ -DECLARE - data_query text; - rec RECORD; -BEGIN - - IF function_name ILIKE 'OBS_GetMeasure' THEN - SELECT cdb_observatory._OBS_GetMeasureQuery(table_schema, table_name, params) INTO data_query; - ELSE - RAISE 'Function not supported'; - END IF; - - - FOR rec IN EXECUTE data_query - LOOP - RETURN NEXT rec; - END LOOP; - RETURN; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - CREATE OR REPLACE FUNCTION cdb_observatory._OBS_GetMeasureQuery(table_schema text, table_name text, params json) RETURNS text AS $$ @@ -141,14 +77,3 @@ BEGIN END; $$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION cdb_observatory._OBS_DisconnectUserTable(username text, orgname text, table_schema text, table_name text, servername text) -RETURNS boolean -AS $$ -BEGIN - EXECUTE 'DROP FOREIGN TABLE IF EXISTS "' || table_schema || '".' || table_name; - EXECUTE 'DROP SCHEMA IF EXISTS ' || table_schema || ' CASCADE'; - EXECUTE 'DROP SERVER IF EXISTS ' || servername || ' CASCADE;'; - RETURN true; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; From 5eb4ede219f8a66baae42b988d0f32c79d948b93 Mon Sep 17 00:00:00 2001 From: Carla Iriberri Date: Tue, 23 Aug 2016 17:20:48 +0200 Subject: [PATCH 6/7] Fix --- src/pg/sql/51_table_level_functions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pg/sql/51_table_level_functions.sql b/src/pg/sql/51_table_level_functions.sql index ba85cbc..7702521 100644 --- a/src/pg/sql/51_table_level_functions.sql +++ b/src/pg/sql/51_table_level_functions.sql @@ -14,7 +14,7 @@ DECLARE measure_id text; BEGIN -- By definition, all the measure results for the OBS_GetMeasure API are numeric values - SELECT ARRAY(SELECT json_array_elements_text(params->'measure_id'))::text[] INTO requested_measures; + SELECT ARRAY(SELECT json_array_elements_text(params->'measure_id'))::text[] INTO requested_measures; FOREACH measure_id IN ARRAY requested_measures LOOP From 32d37a74b323f05c7099509dd2dd702365433642 Mon Sep 17 00:00:00 2001 From: Carla Date: Fri, 2 Sep 2016 12:04:03 +0200 Subject: [PATCH 7/7] Remove cascades and quote conveniently --- src/pg/sql/50_table_level_framework.sql | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pg/sql/50_table_level_framework.sql b/src/pg/sql/50_table_level_framework.sql index a42a827..5be85be 100644 --- a/src/pg/sql/50_table_level_framework.sql +++ b/src/pg/sql/50_table_level_framework.sql @@ -24,9 +24,12 @@ BEGIN EXCEPTION WHEN others THEN -- Disconnect user imported table. Delete schema and FDW server. - EXECUTE 'DROP FOREIGN TABLE IF EXISTS ' || fdw_import_schema || '.' || table_name; - EXECUTE 'DROP SCHEMA IF EXISTS ' || fdw_import_schema || ' CASCADE'; - EXECUTE 'DROP SERVER IF EXISTS ' || fdw_server || ' CASCADE;'; + EXECUTE 'DROP FOREIGN TABLE IF EXISTS "' || fdw_import_schema || '".' || table_name; + EXECUTE 'DROP FOREIGN TABLE IF EXISTS "' || fdw_import_schema || '".cdb_tablemetadata'; + EXECUTE 'DROP SCHEMA IF EXISTS "' || fdw_import_schema || '"'; + EXECUTE 'DROP USER MAPPING IF EXISTS FOR public SERVER "' || fdw_server || '"'; + EXECUTE 'DROP SERVER IF EXISTS "' || fdw_server || '"'; + RETURN (null, null, null); END; $$ LANGUAGE plpgsql SECURITY DEFINER; @@ -70,8 +73,10 @@ RETURNS boolean AS $$ BEGIN EXECUTE 'DROP FOREIGN TABLE IF EXISTS "' || table_schema || '".' || table_name; - EXECUTE 'DROP SCHEMA IF EXISTS ' || table_schema || ' CASCADE'; - EXECUTE 'DROP SERVER IF EXISTS ' || servername || ' CASCADE;'; + EXECUTE 'DROP FOREIGN TABLE IF EXISTS "' || table_schema || '".cdb_tablemetadata'; + EXECUTE 'DROP SCHEMA IF EXISTS "' || table_schema || '"'; + EXECUTE 'DROP USER MAPPING IF EXISTS FOR public SERVER "' || servername || '"'; + EXECUTE 'DROP SERVER IF EXISTS "' || servername || '"'; RETURN true; END; $$ LANGUAGE plpgsql SECURITY DEFINER;