diff --git a/.gitignore b/.gitignore index 1e07caf..94fbf38 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ target .settings .classpath .project +legacyworlds-server-main/data-source.xml +legacyworlds-server-data/db-structure/db-config.txt diff --git a/build/post-build.sh b/build/post-build.sh new file mode 100755 index 0000000..3fb786e --- /dev/null +++ b/build/post-build.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +cd legacyworlds-server-data/db-structure +if ! [ -f db-config.txt ]; then + cat > db-config.txt <../legacyworlds-server-data/db-structure sql - *.sql - */*.sql - */*/*.sql + **.sql + db-config.sample.txt - - *.txt - @@ -71,21 +67,12 @@ - ../legacyworlds-server-main/data-source.xml - data-source.sample.xml + ../legacyworlds-server-main/data-source.sample.xml 0600 . - - - ../legacyworlds-server-data/db-structure/db-config.txt - db-config.sample.txt - 0600 - sql - - - \ No newline at end of file + diff --git a/legacyworlds-server-data/db-structure/database.sql b/legacyworlds-server-data/db-structure/database.sql index 675eeb6..7b8f951 100644 --- a/legacyworlds-server-data/db-structure/database.sql +++ b/legacyworlds-server-data/db-structure/database.sql @@ -12,7 +12,9 @@ -- Read configuration from file \set pgadmin `grep ^admin= db-config.txt | sed -e s/.*=//` \set dbname `grep ^db= db-config.txt | sed -e s/.*=//` +\set dbname_string '''':dbname'''' \set dbuser `grep ^user= db-config.txt | sed -e s/.*=//` +\set dbuser_string '''':dbuser'''' \set dbupass ''''`grep ^password= db-config.txt | sed -e s/.*=// -e "s/'/''/g"`'''' @@ -34,8 +36,21 @@ GRANT CONNECT ON DATABASE :dbname TO :dbuser; -- Connect to the LW database with the PostgreSQL admin user \c :dbname :pgadmin --- Register PL/PgSQL -CREATE TRUSTED PROCEDURAL LANGUAGE plpgsql; +-- Register the dblink extension +CREATE EXTENSION dblink; + +-- Create foreign data wrapper and server used to write logs from within +-- transanctions +CREATE FOREIGN DATA WRAPPER pgsql + VALIDATOR postgresql_fdw_validator; +CREATE SERVER srv_logging + FOREIGN DATA WRAPPER pgsql + OPTIONS ( hostaddr '127.0.0.1' , dbname :dbname_string ); +CREATE USER MAPPING FOR :dbuser + SERVER srv_logging + OPTIONS ( user :dbuser_string , password :dbupass ); +GRANT USAGE ON FOREIGN SERVER srv_logging TO :dbuser; + BEGIN; diff --git a/legacyworlds-server-data/db-structure/db-config.txt b/legacyworlds-server-data/db-structure/db-config.sample.txt similarity index 100% rename from legacyworlds-server-data/db-structure/db-config.txt rename to legacyworlds-server-data/db-structure/db-config.sample.txt diff --git a/legacyworlds-server-data/db-structure/parts/functions/002-sys-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/002-sys-functions.sql index 79a3271..6f75303 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/002-sys-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/002-sys-functions.sql @@ -61,7 +61,7 @@ BEGIN THEN UPDATE sys.ticker SET status = 'RUNNING' , auto_start = NULL WHERE id = task_id; - PERFORM sys.write_log( 'Ticker' , 'INFO'::log_level , 'Scheduled task ''' || t_name + PERFORM sys.write_sql_log( 'Ticker' , 'INFO'::log_level , 'Scheduled task ''' || t_name || ''' has been enabled' ); END IF; END; diff --git a/legacyworlds-server-data/db-structure/parts/functions/005-logs-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/005-logs-functions.sql index f7a7c0c..65bd5b7 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/005-logs-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/005-logs-functions.sql @@ -124,6 +124,42 @@ $$ LANGUAGE plpgsql; GRANT EXECUTE ON FUNCTION sys.write_log( TEXT , log_level , TEXT ) TO :dbuser; +-- +-- Remotely append a system log entry +-- +-- This function is called from within transactions in order to write to the +-- system log. Unlike sys.write_log(), entries added through this function +-- will not be lost if the transaction is rolled back. +-- +-- Since the function is meant to be called from SQL code, it does not return +-- anything as the identifier of the new entry is not required. +-- +-- Parameters: +-- _component The component that is appending to the log +-- _level The log level +-- _message The message to write +-- + +CREATE OR REPLACE FUNCTION sys.write_sql_log( _component TEXT , _level log_level , _message TEXT ) + RETURNS VOID + STRICT VOLATILE + SECURITY INVOKER + AS $write_sql_log$ +BEGIN + PERFORM dblink_connect( 'cn_logging' , 'srv_logging' ); + PERFORM * FROM dblink( 'cn_logging' , + 'SELECT * FROM sys.write_log( ' + || quote_literal( _component ) || ' , ''' + || _level::text || '''::log_level , ' + || quote_literal( _message ) || ' )' + ) AS ( entry_id bigint ); + PERFORM dblink_disconnect( 'cn_logging' ); +END; +$write_sql_log$ LANGUAGE plpgsql; + +GRANT EXECUTE ON FUNCTION sys.write_sql_log( TEXT , log_level , TEXT ) TO :dbuser; + + -- -- Append an exception log entry diff --git a/legacyworlds-server-data/db-structure/parts/functions/070-users-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/070-users-functions.sql index 46bb65f..0e666db 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/070-users-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/070-users-functions.sql @@ -181,7 +181,7 @@ BEGIN ELSE RAISE EXCEPTION 'Invalid account status % (account #%)' , st , a_id; END IF; - PERFORM sys.write_log( 'AccountManagement' , 'ERROR'::log_level , 'Account re-activation mail could not be sent for account #' || a_id || ', deleting validation key' ); + PERFORM sys.write_sql_log( 'AccountManagement' , 'ERROR'::log_level , 'Account re-activation mail could not be sent for account #' || a_id || ', deleting validation key' ); END; $$ LANGUAGE plpgsql; @@ -470,7 +470,7 @@ BEGIN WHERE a.address = addr FOR UPDATE; IF NOT FOUND THEN - PERFORM sys.write_log( 'AccountManagement' , 'WARNING'::log_level , 'account for "' + PERFORM sys.write_sql_log( 'AccountManagement' , 'WARNING'::log_level , 'account for "' || addr || '" not found' ); account_error := 1; RETURN; diff --git a/legacyworlds-server-data/db-structure/parts/functions/140-planets-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/140-planets-functions.sql index 06247c1..53bca46 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/140-planets-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/140-planets-functions.sql @@ -990,7 +990,7 @@ DECLARE st_dmg REAL; n_dest INT; BEGIN - PERFORM sys.write_log( 'BattleUpdate' , 'TRACE'::log_level , 'Inflicting ' || dmg + PERFORM sys.write_sql_log( 'BattleUpdate' , 'TRACE'::log_level , 'Inflicting ' || dmg || ' damage to planet #' || p_id ); bp_id := NULL; @@ -1008,7 +1008,7 @@ BEGIN st_dmg := 0; END IF; - PERFORM sys.write_log( 'BattleUpdate' , 'TRACE'::log_level , 'Building type #' || rec.building_id + PERFORM sys.write_sql_log( 'BattleUpdate' , 'TRACE'::log_level , 'Building type #' || rec.building_id || ' - Damage ' || st_dmg || '; destruction: ' || n_dest ); -- Apply damage @@ -1054,7 +1054,7 @@ DECLARE BEGIN tick := sys.get_tick( ) - 1; tot_damage := t_upkeep * d_ratio / debt; - PERFORM sys.write_log( 'EmpireDebt' , 'DEBUG'::log_level , 'Inflicting debt damage to buildings; total upkeep: ' + PERFORM sys.write_sql_log( 'EmpireDebt' , 'DEBUG'::log_level , 'Inflicting debt damage to buildings; total upkeep: ' || t_upkeep || ', damage ratio: ' || d_ratio || ', total damage: ' || tot_damage ); FOR p_rec IN SELECT ep.planet_id AS planet , b.id AS battle diff --git a/legacyworlds-server-data/db-structure/parts/functions/150-battle-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/150-battle-functions.sql index ef34b5e..d6a4607 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/150-battle-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/150-battle-functions.sql @@ -653,7 +653,7 @@ BEGIN IF NOT att THEN st_power := battles.get_defence_power( b_id , tick ); tot_power := tot_power + st_power; - PERFORM sys.write_log( 'BattleUpdate' , 'TRACE'::log_level , 'About to inflict planet damage; total power: ' || tot_power + PERFORM sys.write_sql_log( 'BattleUpdate' , 'TRACE'::log_level , 'About to inflict planet damage; total power: ' || tot_power || '; planet power: ' || st_power || '; computed damage: ' || ( dmg * st_power / tot_power )::REAL ); IF st_power <> 0 THEN PERFORM verse.inflict_battle_damage( planet , st_power , ( dmg * st_power / tot_power )::REAL , b_id , tick ); diff --git a/legacyworlds-server-data/db-structure/parts/functions/165-fleets-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/165-fleets-functions.sql index f7d802c..7bb87dd 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/165-fleets-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/165-fleets-functions.sql @@ -415,17 +415,17 @@ BEGIN -- Fleet redirection IF rec.is_ref_point IS NULL THEN -- Fleet moving in outer space - PERFORM sys.write_log( 'Fleets' , 'TRACE'::log_level , 'About to perform outer space redirect; ' + PERFORM sys.write_sql_log( 'Fleets' , 'TRACE'::log_level , 'About to perform outer space redirect; ' || 'OOFT/2 = ' || rec.flight_time || '; start(x;y)= (' || rec.os_start_x || ';' || rec.os_start_y || '); dest(x;y)= (' || rec.x || ';' || rec.y || '); time left: ' || rec.mv_state_time ); SELECT INTO cx , cy c_x , c_y FROM fleets.compute_current_location( rec.flight_time , rec.os_start_x , rec.os_start_y , rec.x , rec.y , rec.mv_state_time ); - PERFORM sys.write_log( 'Fleets' , 'TRACE'::log_level , 'Computed current coordinates: (' || cx + PERFORM sys.write_sql_log( 'Fleets' , 'TRACE'::log_level , 'Computed current coordinates: (' || cx || ';' || cy || ')' ); SELECT INTO dur , s_dur duration , s_duration FROM fleets.compute_outerspace_redirect( rec.flight_time , cx , cy , dest_id ); - PERFORM sys.write_log( 'Fleets' , 'TRACE'::log_level , 'Computed new total/state duration: ' + PERFORM sys.write_sql_log( 'Fleets' , 'TRACE'::log_level , 'Computed new total/state duration: ' || dur || ' / ' || s_dur ); UPDATE fleets.ms_space SET start_x = cx , start_y = cy WHERE movement_id = rec.id; @@ -1000,7 +1000,7 @@ DECLARE found INT; deleted INT; BEGIN - PERFORM sys.write_log( 'BattleUpdate' , 'TRACE'::log_level , 'Inflicting ' + PERFORM sys.write_sql_log( 'BattleUpdate' , 'TRACE'::log_level , 'Inflicting ' || dmg || ' damage to fleet #' || f_id ); -- Get total fleet power and battle protagonist @@ -1094,7 +1094,7 @@ DECLARE BEGIN tick := sys.get_tick( ) - 1; tot_damage := t_upkeep * d_ratio / debt; - PERFORM sys.write_log( 'EmpireDebt' , 'DEBUG'::log_level , 'Inflicting debt damage to fleets; total upkeep: ' + PERFORM sys.write_sql_log( 'EmpireDebt' , 'DEBUG'::log_level , 'Inflicting debt damage to fleets; total upkeep: ' || t_upkeep || ', damage ratio: ' || d_ratio || ', total damage: ' || tot_damage ); FOR f_rec IN SELECT f.id AS fleet , f.status , f.location_id AS location , diff --git a/legacyworlds-server-data/db-structure/parts/functions/180-messages-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/180-messages-functions.sql index e7319b0..65010e5 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/180-messages-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/180-messages-functions.sql @@ -585,13 +585,13 @@ BEGIN -- Send message (or not) IF do_send THEN - PERFORM sys.write_log( 'Messages' , 'DEBUG'::log_level , 'Delivering message from empire #' || e_id + PERFORM sys.write_sql_log( 'Messages' , 'DEBUG'::log_level , 'Delivering message from empire #' || e_id || ' using method ' || s_meth || ' with target identifier ' || tg_id || ' (title text null? ' || ( ttl_txt IS NULL ) || ' ; content text null? ' || (cnt_txt IS NULL) || ')' ); EXECUTE 'SELECT msgs.deliver_' || s_meth || '( $1 , $2 , $3 , $4 )' USING e_id , tg_id , ttl_txt , cnt_txt; ELSE - PERFORM sys.write_log( 'Messages' , 'DEBUG'::log_level , 'Simulated message delivery from empire #' || e_id + PERFORM sys.write_sql_log( 'Messages' , 'DEBUG'::log_level , 'Simulated message delivery from empire #' || e_id || ' using method ' || s_meth || ' with target identifier ' || tg_id || ' (title text null? ' || ( ttl_txt IS NULL ) || ' ; content text null? ' || (cnt_txt IS NULL) || ')' ); END IF; @@ -860,13 +860,13 @@ BEGIN -- Send message (or not) IF do_send THEN - PERFORM sys.write_log( 'Messages' , 'DEBUG'::log_level , 'Delivering message from admin #' || a_id + PERFORM sys.write_sql_log( 'Messages' , 'DEBUG'::log_level , 'Delivering message from admin #' || a_id || ' using method ' || s_meth || ' with target identifier ' || tg_id || ' (title text null? ' || ( ttl_txt IS NULL ) || ' ; content text null? ' || (cnt_txt IS NULL) || ')' ); EXECUTE 'SELECT msgs.deliver_admin_' || s_meth || '( $1 , $2 , $3 , $4 )' USING a_id , tg_id , ttl_txt , cnt_txt; ELSE - PERFORM sys.write_log( 'Messages' , 'DEBUG'::log_level , 'Simulated message delivery from admin #' || a_id + PERFORM sys.write_sql_log( 'Messages' , 'DEBUG'::log_level , 'Simulated message delivery from admin #' || a_id || ' using method ' || s_meth || ' with target identifier ' || tg_id || ' (title text null? ' || ( ttl_txt IS NULL ) || ' ; content text null? ' || (cnt_txt IS NULL) || ')' ); END IF; diff --git a/legacyworlds-server-data/db-structure/parts/functions/190-admin-functions.sql b/legacyworlds-server-data/db-structure/parts/functions/190-admin-functions.sql index b5bbf6a..d7221d9 100644 --- a/legacyworlds-server-data/db-structure/parts/functions/190-admin-functions.sql +++ b/legacyworlds-server-data/db-structure/parts/functions/190-admin-functions.sql @@ -663,7 +663,7 @@ BEGIN INSERT INTO admin.archived_ban_requests ( request_id , credentials_id ) VALUES ( b_id , u_id ); DELETE FROM admin.active_ban_requests WHERE request_id = b_id; - PERFORM sys.write_log( 'Bans' , 'INFO'::log_level , 'Ban request #' || b_id + PERFORM sys.write_sql_log( 'Bans' , 'INFO'::log_level , 'Ban request #' || b_id || ' (user account #' || u_id || ') expired' ); END LOOP; END; @@ -707,7 +707,7 @@ BEGIN -- Delete empire and active account record PERFORM emp.delete_empire( e_id ); DELETE FROM users.active_accounts WHERE credentials_id = a_id; - PERFORM sys.write_log( 'Bans' , 'INFO'::log_level , 'Deleted empire #' || e_id + PERFORM sys.write_sql_log( 'Bans' , 'INFO'::log_level , 'Deleted empire #' || e_id || ' (user account #' || a_id || ')' ); RETURN TRUE; diff --git a/legacyworlds-server-data/db-structure/tests/pgtap.sql b/legacyworlds-server-data/db-structure/tests/pgtap.sql new file mode 100644 index 0000000..5a14976 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/pgtap.sql @@ -0,0 +1,7402 @@ +-- This file defines pgTAP, a collection of functions for TAP-based unit +-- testing. It is distributed under the revised FreeBSD license. You can +-- find the original here: +-- +-- http://github.com/theory/pgtap/raw/master/sql/pgtap.sql.in +-- +-- The home page for the pgTAP project is: +-- +-- http://pgtap.org/ + +CREATE OR REPLACE FUNCTION pg_version() +RETURNS text AS 'SELECT current_setting(''server_version'')' +LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION pg_version_num() +RETURNS integer AS $$ + SELECT s.a[1]::int * 10000 + + COALESCE(substring(s.a[2] FROM '[[:digit:]]+')::int, 0) * 100 + + COALESCE(substring(s.a[3] FROM '[[:digit:]]+')::int, 0) + FROM ( + SELECT string_to_array(current_setting('server_version'), '.') AS a + ) AS s; +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION os_name() +RETURNS TEXT AS 'SELECT ''''::text;' +LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION pgtap_version() +RETURNS NUMERIC AS 'SELECT 0.90;' +LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION plan( integer ) +RETURNS TEXT AS $$ +DECLARE + rcount INTEGER; +BEGIN + BEGIN + EXECUTE ' + CREATE TEMP SEQUENCE __tcache___id_seq; + CREATE TEMP TABLE __tcache__ ( + id INTEGER NOT NULL DEFAULT nextval(''__tcache___id_seq''), + label TEXT NOT NULL, + value INTEGER NOT NULL, + note TEXT NOT NULL DEFAULT '''' + ); + CREATE UNIQUE INDEX __tcache___key ON __tcache__(id); + GRANT ALL ON TABLE __tcache__ TO PUBLIC; + GRANT ALL ON TABLE __tcache___id_seq TO PUBLIC; + + CREATE TEMP SEQUENCE __tresults___numb_seq; + CREATE TEMP TABLE __tresults__ ( + numb INTEGER NOT NULL DEFAULT nextval(''__tresults___numb_seq''), + ok BOOLEAN NOT NULL DEFAULT TRUE, + aok BOOLEAN NOT NULL DEFAULT TRUE, + descr TEXT NOT NULL DEFAULT '''', + type TEXT NOT NULL DEFAULT '''', + reason TEXT NOT NULL DEFAULT '''' + ); + CREATE UNIQUE INDEX __tresults___key ON __tresults__(numb); + GRANT ALL ON TABLE __tresults__ TO PUBLIC; + GRANT ALL ON TABLE __tresults___numb_seq TO PUBLIC; + '; + + EXCEPTION WHEN duplicate_table THEN + -- Raise an exception if there's already a plan. + EXECUTE 'SELECT TRUE FROM __tcache__ WHERE label = ''plan'''; + GET DIAGNOSTICS rcount = ROW_COUNT; + IF rcount > 0 THEN + RAISE EXCEPTION 'You tried to plan twice!'; + END IF; + END; + + -- Save the plan and return. + PERFORM _set('plan', $1 ); + RETURN '1..' || $1; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION no_plan() +RETURNS SETOF boolean AS $$ +BEGIN + PERFORM plan(0); + RETURN; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _get ( text ) +RETURNS integer AS $$ +DECLARE + ret integer; +BEGIN + EXECUTE 'SELECT value FROM __tcache__ WHERE label = ' || quote_literal($1) || ' LIMIT 1' INTO ret; + RETURN ret; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _get_latest ( text ) +RETURNS integer[] AS $$ +DECLARE + ret integer[]; +BEGIN + EXECUTE 'SELECT ARRAY[ id, value] FROM __tcache__ WHERE label = ' || + quote_literal($1) || ' AND id = (SELECT MAX(id) FROM __tcache__ WHERE label = ' || + quote_literal($1) || ') LIMIT 1' INTO ret; + RETURN ret; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _get_latest ( text, integer ) +RETURNS integer AS $$ +DECLARE + ret integer; +BEGIN + EXECUTE 'SELECT MAX(id) FROM __tcache__ WHERE label = ' || + quote_literal($1) || ' AND value = ' || $2 INTO ret; + RETURN ret; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _get_note ( text ) +RETURNS text AS $$ +DECLARE + ret text; +BEGIN + EXECUTE 'SELECT note FROM __tcache__ WHERE label = ' || quote_literal($1) || ' LIMIT 1' INTO ret; + RETURN ret; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _get_note ( integer ) +RETURNS text AS $$ +DECLARE + ret text; +BEGIN + EXECUTE 'SELECT note FROM __tcache__ WHERE id = ' || $1 || ' LIMIT 1' INTO ret; + RETURN ret; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _set ( text, integer, text ) +RETURNS integer AS $$ +DECLARE + rcount integer; +BEGIN + EXECUTE 'UPDATE __tcache__ SET value = ' || $2 + || CASE WHEN $3 IS NULL THEN '' ELSE ', note = ' || quote_literal($3) END + || ' WHERE label = ' || quote_literal($1); + GET DIAGNOSTICS rcount = ROW_COUNT; + IF rcount = 0 THEN + RETURN _add( $1, $2, $3 ); + END IF; + RETURN $2; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _set ( text, integer ) +RETURNS integer AS $$ + SELECT _set($1, $2, '') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _set ( integer, integer ) +RETURNS integer AS $$ +BEGIN + EXECUTE 'UPDATE __tcache__ SET value = ' || $2 + || ' WHERE id = ' || $1; + RETURN $2; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _add ( text, integer, text ) +RETURNS integer AS $$ +BEGIN + EXECUTE 'INSERT INTO __tcache__ (label, value, note) values (' || + quote_literal($1) || ', ' || $2 || ', ' || quote_literal(COALESCE($3, '')) || ')'; + RETURN $2; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _add ( text, integer ) +RETURNS integer AS $$ + SELECT _add($1, $2, '') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION add_result ( bool, bool, text, text, text ) +RETURNS integer AS $$ +BEGIN + EXECUTE 'INSERT INTO __tresults__ ( ok, aok, descr, type, reason ) + VALUES( ' || $1 || ', ' + || $2 || ', ' + || quote_literal(COALESCE($3, '')) || ', ' + || quote_literal($4) || ', ' + || quote_literal($5) || ' )'; + RETURN currval('__tresults___numb_seq'); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION num_failed () +RETURNS INTEGER AS $$ +DECLARE + ret integer; +BEGIN + EXECUTE 'SELECT COUNT(*)::INTEGER FROM __tresults__ WHERE ok = FALSE' INTO ret; + RETURN ret; +END; +$$ LANGUAGE plpgsql strict; + +CREATE OR REPLACE FUNCTION _finish ( INTEGER, INTEGER, INTEGER) +RETURNS SETOF TEXT AS $$ +DECLARE + curr_test ALIAS FOR $1; + exp_tests INTEGER := $2; + num_faild ALIAS FOR $3; + plural CHAR; +BEGIN + plural := CASE exp_tests WHEN 1 THEN '' ELSE 's' END; + + IF curr_test IS NULL THEN + RAISE EXCEPTION '# No tests run!'; + END IF; + + IF exp_tests = 0 OR exp_tests IS NULL THEN + -- No plan. Output one now. + exp_tests = curr_test; + RETURN NEXT '1..' || exp_tests; + END IF; + + IF curr_test <> exp_tests THEN + RETURN NEXT diag( + 'Looks like you planned ' || exp_tests || ' test' || + plural || ' but ran ' || curr_test + ); + ELSIF num_faild > 0 THEN + RETURN NEXT diag( + 'Looks like you failed ' || num_faild || ' test' || + CASE num_faild WHEN 1 THEN '' ELSE 's' END + || ' of ' || exp_tests + ); + ELSE + + END IF; + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION finish () +RETURNS SETOF TEXT AS $$ + SELECT * FROM _finish( + _get('curr_test'), + _get('plan'), + num_failed() + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION diag ( msg text ) +RETURNS TEXT AS $$ + SELECT '# ' || replace( + replace( + replace( $1, E'\r\n', E'\n# ' ), + E'\n', + E'\n# ' + ), + E'\r', + E'\n# ' + ); +$$ LANGUAGE sql strict; + +CREATE OR REPLACE FUNCTION diag ( msg anyelement ) +RETURNS TEXT AS $$ + SELECT diag($1::text); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION diag( VARIADIC text[] ) +RETURNS TEXT AS $$ + SELECT diag(array_to_string($1, '')); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION diag( VARIADIC anyarray ) +RETURNS TEXT AS $$ + SELECT diag(array_to_string($1, '')); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION ok ( boolean, text ) +RETURNS TEXT AS $$ +DECLARE + aok ALIAS FOR $1; + descr text := $2; + test_num INTEGER; + todo_why TEXT; + ok BOOL; +BEGIN + todo_why := _todo(); + ok := CASE + WHEN aok = TRUE THEN aok + WHEN todo_why IS NULL THEN COALESCE(aok, false) + ELSE TRUE + END; + IF _get('plan') IS NULL THEN + RAISE EXCEPTION 'You tried to run a test without a plan! Gotta have a plan'; + END IF; + + test_num := add_result( + ok, + COALESCE(aok, false), + descr, + CASE WHEN todo_why IS NULL THEN '' ELSE 'todo' END, + COALESCE(todo_why, '') + ); + + RETURN (CASE aok WHEN TRUE THEN '' ELSE 'not ' END) + || 'ok ' || _set( 'curr_test', test_num ) + || CASE descr WHEN '' THEN '' ELSE COALESCE( ' - ' || substr(diag( descr ), 3), '' ) END + || COALESCE( ' ' || diag( 'TODO ' || todo_why ), '') + || CASE aok WHEN TRUE THEN '' ELSE E'\n' || + diag('Failed ' || + CASE WHEN todo_why IS NULL THEN '' ELSE '(TODO) ' END || + 'test ' || test_num || + CASE descr WHEN '' THEN '' ELSE COALESCE(': "' || descr || '"', '') END ) || + CASE WHEN aok IS NULL THEN E'\n' || diag(' (test result was NULL)') ELSE '' END + END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION ok ( boolean ) +RETURNS TEXT AS $$ + SELECT ok( $1, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION is (anyelement, anyelement, text) +RETURNS TEXT AS $$ +DECLARE + result BOOLEAN; + output TEXT; +BEGIN + -- Would prefer $1 IS NOT DISTINCT FROM, but that's not supported by 8.1. + result := NOT $1 IS DISTINCT FROM $2; + output := ok( result, $3 ); + RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( + ' have: ' || CASE WHEN $1 IS NULL THEN 'NULL' ELSE $1::text END || + E'\n want: ' || CASE WHEN $2 IS NULL THEN 'NULL' ELSE $2::text END + ) END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION is (anyelement, anyelement) +RETURNS TEXT AS $$ + SELECT is( $1, $2, NULL); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION isnt (anyelement, anyelement, text) +RETURNS TEXT AS $$ +DECLARE + result BOOLEAN; + output TEXT; +BEGIN + result := $1 IS DISTINCT FROM $2; + output := ok( result, $3 ); + RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( + ' have: ' || COALESCE( $1::text, 'NULL' ) || + E'\n want: anything else' + ) END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION isnt (anyelement, anyelement) +RETURNS TEXT AS $$ + SELECT isnt( $1, $2, NULL); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _alike ( BOOLEAN, ANYELEMENT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + result ALIAS FOR $1; + got ALIAS FOR $2; + rx ALIAS FOR $3; + descr ALIAS FOR $4; + output TEXT; +BEGIN + output := ok( result, descr ); + RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( + ' ' || COALESCE( quote_literal(got), 'NULL' ) || + E'\n doesn''t match: ' || COALESCE( quote_literal(rx), 'NULL' ) + ) END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION matches ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~ $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION matches ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~ $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION imatches ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~* $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION imatches ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~* $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION alike ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~~ $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION alike ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~~ $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION ialike ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~~* $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION ialike ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _alike( $1 ~~* $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _unalike ( BOOLEAN, ANYELEMENT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + result ALIAS FOR $1; + got ALIAS FOR $2; + rx ALIAS FOR $3; + descr ALIAS FOR $4; + output TEXT; +BEGIN + output := ok( result, descr ); + RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( + ' ' || COALESCE( quote_literal(got), 'NULL' ) || + E'\n matches: ' || COALESCE( quote_literal(rx), 'NULL' ) + ) END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION doesnt_match ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~ $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION doesnt_match ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~ $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION doesnt_imatch ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~* $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION doesnt_imatch ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~* $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION unalike ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~~ $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION unalike ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~~ $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION unialike ( anyelement, text, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~~* $2, $1, $2, $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION unialike ( anyelement, text ) +RETURNS TEXT AS $$ + SELECT _unalike( $1 !~~* $2, $1, $2, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION cmp_ok (anyelement, text, anyelement, text) +RETURNS TEXT AS $$ +DECLARE + have ALIAS FOR $1; + op ALIAS FOR $2; + want ALIAS FOR $3; + descr ALIAS FOR $4; + result BOOLEAN; + output TEXT; +BEGIN + EXECUTE 'SELECT ' || + COALESCE(quote_literal( have ), 'NULL') || '::' || pg_typeof(have) || ' ' + || op || ' ' || + COALESCE(quote_literal( want ), 'NULL') || '::' || pg_typeof(want) + INTO result; + output := ok( COALESCE(result, FALSE), descr ); + RETURN output || CASE result WHEN TRUE THEN '' ELSE E'\n' || diag( + ' ' || COALESCE( quote_literal(have), 'NULL' ) || + E'\n ' || op || + E'\n ' || COALESCE( quote_literal(want), 'NULL' ) + ) END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION cmp_ok (anyelement, text, anyelement) +RETURNS TEXT AS $$ + SELECT cmp_ok( $1, $2, $3, NULL ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION pass ( text ) +RETURNS TEXT AS $$ + SELECT ok( TRUE, $1 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION pass () +RETURNS TEXT AS $$ + SELECT ok( TRUE, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION fail ( text ) +RETURNS TEXT AS $$ + SELECT ok( FALSE, $1 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION fail () +RETURNS TEXT AS $$ + SELECT ok( FALSE, NULL ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION todo ( why text, how_many int ) +RETURNS SETOF BOOLEAN AS $$ +BEGIN + PERFORM _add('todo', COALESCE(how_many, 1), COALESCE(why, '')); + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION todo ( how_many int, why text ) +RETURNS SETOF BOOLEAN AS $$ +BEGIN + PERFORM _add('todo', COALESCE(how_many, 1), COALESCE(why, '')); + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION todo ( why text ) +RETURNS SETOF BOOLEAN AS $$ +BEGIN + PERFORM _add('todo', 1, COALESCE(why, '')); + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION todo ( how_many int ) +RETURNS SETOF BOOLEAN AS $$ +BEGIN + PERFORM _add('todo', COALESCE(how_many, 1), ''); + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION todo_start (text) +RETURNS SETOF BOOLEAN AS $$ +BEGIN + PERFORM _add('todo', -1, COALESCE($1, '')); + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION todo_start () +RETURNS SETOF BOOLEAN AS $$ +BEGIN + PERFORM _add('todo', -1, ''); + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION in_todo () +RETURNS BOOLEAN AS $$ +DECLARE + todos integer; +BEGIN + todos := _get('todo'); + RETURN CASE WHEN todos IS NULL THEN FALSE ELSE TRUE END; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION todo_end () +RETURNS SETOF BOOLEAN AS $$ +DECLARE + id integer; +BEGIN + id := _get_latest( 'todo', -1 ); + IF id IS NULL THEN + RAISE EXCEPTION 'todo_end() called without todo_start()'; + END IF; + EXECUTE 'DELETE FROM __tcache__ WHERE id = ' || id; + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _todo() +RETURNS TEXT AS $$ +DECLARE + todos INT[]; + note text; +BEGIN + -- Get the latest id and value, because todo() might have been called + -- again before the todos ran out for the first call to todo(). This + -- allows them to nest. + todos := _get_latest('todo'); + IF todos IS NULL THEN + -- No todos. + RETURN NULL; + END IF; + IF todos[2] = 0 THEN + -- Todos depleted. Clean up. + EXECUTE 'DELETE FROM __tcache__ WHERE id = ' || todos[1]; + RETURN NULL; + END IF; + -- Decrement the count of counted todos and return the reason. + IF todos[2] <> -1 THEN + PERFORM _set(todos[1], todos[2] - 1); + END IF; + note := _get_note(todos[1]); + + IF todos[2] = 1 THEN + -- This was the last todo, so delete the record. + EXECUTE 'DELETE FROM __tcache__ WHERE id = ' || todos[1]; + END IF; + + RETURN note; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION skip ( why text, how_many int ) +RETURNS TEXT AS $$ +DECLARE + output TEXT[]; +BEGIN + output := '{}'; + FOR i IN 1..how_many LOOP + output = array_append(output, ok( TRUE, 'SKIP: ' || COALESCE( why, '') ) ); + END LOOP; + RETURN array_to_string(output, E'\n'); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION skip ( text ) +RETURNS TEXT AS $$ + SELECT ok( TRUE, 'SKIP: ' || $1 ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION skip( int, text ) +RETURNS TEXT AS 'SELECT skip($2, $1)' +LANGUAGE sql; + +CREATE OR REPLACE FUNCTION skip( int ) +RETURNS TEXT AS 'SELECT skip(NULL, $1)' +LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _query( TEXT ) +RETURNS TEXT AS $$ + SELECT CASE + WHEN $1 LIKE '"%' OR $1 !~ '[[:space:]]' THEN 'EXECUTE ' || $1 + ELSE $1 + END; +$$ LANGUAGE SQL; + +-- throws_ok ( sql, errcode, errmsg, description ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT, CHAR(5), TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + query TEXT := _query($1); + errcode ALIAS FOR $2; + errmsg ALIAS FOR $3; + desctext ALIAS FOR $4; + descr TEXT; +BEGIN + descr := COALESCE( + desctext, + 'threw ' || errcode || ': ' || errmsg, + 'threw ' || errcode, + 'threw ' || errmsg, + 'threw an exception' + ); + EXECUTE query; + RETURN ok( FALSE, descr ) || E'\n' || diag( + ' caught: no exception' || + E'\n wanted: ' || COALESCE( errcode, 'an exception' ) + ); +EXCEPTION WHEN OTHERS THEN + IF (errcode IS NULL OR SQLSTATE = errcode) + AND ( errmsg IS NULL OR SQLERRM = errmsg) + THEN + -- The expected errcode and/or message was thrown. + RETURN ok( TRUE, descr ); + ELSE + -- This was not the expected errcode or errmsg. + RETURN ok( FALSE, descr ) || E'\n' || diag( + ' caught: ' || SQLSTATE || ': ' || SQLERRM || + E'\n wanted: ' || COALESCE( errcode, 'an exception' ) || + COALESCE( ': ' || errmsg, '') + ); + END IF; +END; +$$ LANGUAGE plpgsql; + +-- throws_ok ( sql, errcode, errmsg ) +-- throws_ok ( sql, errmsg, description ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF octet_length($2) = 5 THEN + RETURN throws_ok( $1, $2::char(5), $3, NULL ); + ELSE + RETURN throws_ok( $1, NULL, $2, $3 ); + END IF; +END; +$$ LANGUAGE plpgsql; + +-- throws_ok ( query, errcode ) +-- throws_ok ( query, errmsg ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF octet_length($2) = 5 THEN + RETURN throws_ok( $1, $2::char(5), NULL, NULL ); + ELSE + RETURN throws_ok( $1, NULL, $2, NULL ); + END IF; +END; +$$ LANGUAGE plpgsql; + +-- throws_ok ( sql ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT ) +RETURNS TEXT AS $$ + SELECT throws_ok( $1, NULL, NULL, NULL ); +$$ LANGUAGE SQL; + +-- Magically cast integer error codes. +-- throws_ok ( sql, errcode, errmsg, description ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT, int4, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT throws_ok( $1, $2::char(5), $3, $4 ); +$$ LANGUAGE SQL; + +-- throws_ok ( sql, errcode, errmsg ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT, int4, TEXT ) +RETURNS TEXT AS $$ + SELECT throws_ok( $1, $2::char(5), $3, NULL ); +$$ LANGUAGE SQL; + +-- throws_ok ( sql, errcode ) +CREATE OR REPLACE FUNCTION throws_ok ( TEXT, int4 ) +RETURNS TEXT AS $$ + SELECT throws_ok( $1, $2::char(5), NULL, NULL ); +$$ LANGUAGE SQL; + +-- lives_ok( sql, description ) +CREATE OR REPLACE FUNCTION lives_ok ( TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + code TEXT := _query($1); + descr ALIAS FOR $2; +BEGIN + EXECUTE code; + RETURN ok( TRUE, descr ); +EXCEPTION WHEN OTHERS THEN + -- There should have been no exception. + RETURN ok( FALSE, descr ) || E'\n' || diag( + ' died: ' || SQLSTATE || ': ' || SQLERRM + ); +END; +$$ LANGUAGE plpgsql; + +-- lives_ok( sql ) +CREATE OR REPLACE FUNCTION lives_ok ( TEXT ) +RETURNS TEXT AS $$ + SELECT lives_ok( $1, NULL ); +$$ LANGUAGE SQL; + +-- performs_ok ( sql, milliseconds, description ) +CREATE OR REPLACE FUNCTION performs_ok ( TEXT, NUMERIC, TEXT ) +RETURNS TEXT AS $$ +DECLARE + query TEXT := _query($1); + max_time ALIAS FOR $2; + descr ALIAS FOR $3; + starts_at TEXT; + act_time NUMERIC; +BEGIN + starts_at := timeofday(); + EXECUTE query; + act_time := extract( millisecond from timeofday()::timestamptz - starts_at::timestamptz); + IF act_time < max_time THEN RETURN ok(TRUE, descr); END IF; + RETURN ok( FALSE, descr ) || E'\n' || diag( + ' runtime: ' || act_time || ' ms' || + E'\n exceeds: ' || max_time || ' ms' + ); +END; +$$ LANGUAGE plpgsql; + +-- performs_ok ( sql, milliseconds ) +CREATE OR REPLACE FUNCTION performs_ok ( TEXT, NUMERIC ) +RETURNS TEXT AS $$ + SELECT performs_ok( + $1, $2, 'Should run in less than ' || $2 || ' ms' + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _rexists ( CHAR, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + WHERE c.relkind = $1 + AND n.nspname = $2 + AND c.relname = $3 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _rexists ( CHAR, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_class c + WHERE c.relkind = $1 + AND pg_catalog.pg_table_is_visible(c.oid) + AND c.relname = $2 + ); +$$ LANGUAGE SQL; + +-- has_table( schema, table, description ) +CREATE OR REPLACE FUNCTION has_table ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _rexists( 'r', $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- has_table( table, description ) +CREATE OR REPLACE FUNCTION has_table ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _rexists( 'r', $1 ), $2 ); +$$ LANGUAGE SQL; + +-- has_table( table ) +CREATE OR REPLACE FUNCTION has_table ( NAME ) +RETURNS TEXT AS $$ + SELECT has_table( $1, 'Table ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_table( schema, table, description ) +CREATE OR REPLACE FUNCTION hasnt_table ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _rexists( 'r', $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_table( table, description ) +CREATE OR REPLACE FUNCTION hasnt_table ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _rexists( 'r', $1 ), $2 ); +$$ LANGUAGE SQL; + +-- hasnt_table( table ) +CREATE OR REPLACE FUNCTION hasnt_table ( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_table( $1, 'Table ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE SQL; + +-- has_view( schema, view, description ) +CREATE OR REPLACE FUNCTION has_view ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _rexists( 'v', $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- has_view( view, description ) +CREATE OR REPLACE FUNCTION has_view ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _rexists( 'v', $1 ), $2 ); +$$ LANGUAGE SQL; + +-- has_view( view ) +CREATE OR REPLACE FUNCTION has_view ( NAME ) +RETURNS TEXT AS $$ + SELECT has_view( $1, 'View ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_view( schema, view, description ) +CREATE OR REPLACE FUNCTION hasnt_view ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _rexists( 'v', $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_view( view, description ) +CREATE OR REPLACE FUNCTION hasnt_view ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _rexists( 'v', $1 ), $2 ); +$$ LANGUAGE SQL; + +-- hasnt_view( view ) +CREATE OR REPLACE FUNCTION hasnt_view ( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_view( $1, 'View ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE SQL; + +-- has_sequence( schema, sequence, description ) +CREATE OR REPLACE FUNCTION has_sequence ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _rexists( 'S', $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- has_sequence( sequence, description ) +CREATE OR REPLACE FUNCTION has_sequence ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _rexists( 'S', $1 ), $2 ); +$$ LANGUAGE SQL; + +-- has_sequence( sequence ) +CREATE OR REPLACE FUNCTION has_sequence ( NAME ) +RETURNS TEXT AS $$ + SELECT has_sequence( $1, 'Sequence ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_sequence( schema, sequence, description ) +CREATE OR REPLACE FUNCTION hasnt_sequence ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _rexists( 'S', $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_sequence( sequence, description ) +CREATE OR REPLACE FUNCTION hasnt_sequence ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _rexists( 'S', $1 ), $2 ); +$$ LANGUAGE SQL; + +-- hasnt_sequence( sequence ) +CREATE OR REPLACE FUNCTION hasnt_sequence ( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_sequence( $1, 'Sequence ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _cexists ( NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $3 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _cexists ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE c.relname = $1 + AND pg_catalog.pg_table_is_visible(c.oid) + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $2 + ); +$$ LANGUAGE SQL; + +-- has_column( schema, table, column, description ) +CREATE OR REPLACE FUNCTION has_column ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _cexists( $1, $2, $3 ), $4 ); +$$ LANGUAGE SQL; + +-- has_column( table, column, description ) +CREATE OR REPLACE FUNCTION has_column ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _cexists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- has_column( table, column ) +CREATE OR REPLACE FUNCTION has_column ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT has_column( $1, $2, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_column( schema, table, column, description ) +CREATE OR REPLACE FUNCTION hasnt_column ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _cexists( $1, $2, $3 ), $4 ); +$$ LANGUAGE SQL; + +-- hasnt_column( table, column, description ) +CREATE OR REPLACE FUNCTION hasnt_column ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _cexists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_column( table, column ) +CREATE OR REPLACE FUNCTION hasnt_column ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_column( $1, $2, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); +$$ LANGUAGE SQL; + +-- _col_is_null( schema, table, column, desc, null ) +CREATE OR REPLACE FUNCTION _col_is_null ( NAME, NAME, NAME, TEXT, bool ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2, $3 ) THEN + RETURN fail( $4 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || '.' || quote_ident($3) || ' does not exist' ); + END IF; + RETURN ok( + EXISTS( + SELECT true + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $3 + AND a.attnotnull = $5 + ), $4 + ); +END; +$$ LANGUAGE plpgsql; + +-- _col_is_null( table, column, desc, null ) +CREATE OR REPLACE FUNCTION _col_is_null ( NAME, NAME, TEXT, bool ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2 ) THEN + RETURN fail( $3 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || ' does not exist' ); + END IF; + RETURN ok( + EXISTS( + SELECT true + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE pg_catalog.pg_table_is_visible(c.oid) + AND c.relname = $1 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $2 + AND a.attnotnull = $4 + ), $3 + ); +END; +$$ LANGUAGE plpgsql; + +-- col_not_null( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_not_null ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _col_is_null( $1, $2, $3, $4, true ); +$$ LANGUAGE SQL; + +-- col_not_null( table, column, description ) +CREATE OR REPLACE FUNCTION col_not_null ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _col_is_null( $1, $2, $3, true ); +$$ LANGUAGE SQL; + +-- col_not_null( table, column ) +CREATE OR REPLACE FUNCTION col_not_null ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT _col_is_null( $1, $2, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should be NOT NULL', true ); +$$ LANGUAGE SQL; + +-- col_is_null( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_null ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _col_is_null( $1, $2, $3, $4, false ); +$$ LANGUAGE SQL; + +-- col_is_null( schema, table, column ) +CREATE OR REPLACE FUNCTION col_is_null ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT _col_is_null( $1, $2, $3, false ); +$$ LANGUAGE SQL; + +-- col_is_null( table, column ) +CREATE OR REPLACE FUNCTION col_is_null ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT _col_is_null( $1, $2, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should allow NULL', false ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION display_type ( OID, INTEGER ) +RETURNS TEXT AS $$ + SELECT COALESCE(substring( + pg_catalog.format_type($1, $2), + '(("(?!")([^"]|"")+"|[^.]+)([(][^)]+[)])?)$' + ), '') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION display_type ( NAME, OID, INTEGER ) +RETURNS TEXT AS $$ + SELECT CASE WHEN $1 IS NULL THEN '' ELSE quote_ident($1) || '.' END + || display_type($2, $3) +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _get_col_type ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT display_type(a.atttypid, a.atttypmod) + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attname = $3 + AND attnum > 0 + AND NOT a.attisdropped +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _get_col_type ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT display_type(a.atttypid, a.atttypmod) + FROM pg_catalog.pg_attribute a + JOIN pg_catalog.pg_class c ON a.attrelid = c.oid + WHERE pg_table_is_visible(c.oid) + AND c.relname = $1 + AND a.attname = $2 + AND attnum > 0 + AND NOT a.attisdropped + AND pg_type_is_visible(a.atttypid) +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _get_col_ns_type ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT display_type(tn.nspname, a.atttypid, a.atttypmod) + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + JOIN pg_catalog.pg_type t ON a.atttypid = t.oid + JOIN pg_catalog.pg_namespace tn ON t.typnamespace = tn.oid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attname = $3 + AND attnum > 0 + AND NOT a.attisdropped +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _quote_ident_like(TEXT, TEXT) +RETURNS TEXT AS $$ +DECLARE + have TEXT; + pcision TEXT; +BEGIN + -- Just return it if rhs isn't quoted. + IF $2 !~ '"' THEN RETURN $1; END IF; + + -- If it's quoted ident without precision, return it quoted. + IF $2 ~ '"$' THEN RETURN quote_ident($1); END IF; + + pcision := substring($1 FROM '[(][^")]+[)]$'); + + -- Just quote it if thre is no precision. + if pcision IS NULL THEN RETURN quote_ident($1); END IF; + + -- Quote the non-precision part and concatenate with precision. + RETURN quote_ident(substring($1 FOR char_length($1) - char_length(pcision))) + || pcision; +END; +$$ LANGUAGE plpgsql; + +-- col_type_is( schema, table, column, schema, type, description ) +CREATE OR REPLACE FUNCTION col_type_is ( NAME, NAME, NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have_type TEXT := _get_col_ns_type($1, $2, $3); + want_type TEXT; +BEGIN + IF have_type IS NULL THEN + RETURN fail( $6 ) || E'\n' || diag ( + ' Column ' || COALESCE(quote_ident($1) || '.', '') + || quote_ident($2) || '.' || quote_ident($3) || ' does not exist' + ); + END IF; + + want_type := quote_ident($4) || '.' || _quote_ident_like($5, have_type); + IF have_type = want_type THEN + -- We're good to go. + RETURN ok( true, $6 ); + END IF; + + -- Wrong data type. tell 'em what we really got. + RETURN ok( false, $6 ) || E'\n' || diag( + ' have: ' || have_type || + E'\n want: ' || want_type + ); +END; +$$ LANGUAGE plpgsql; + +-- col_type_is( schema, table, column, schema, type ) +CREATE OR REPLACE FUNCTION col_type_is ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_type_is( $1, $2, $3, $4, $5, 'Column ' || quote_ident($1) || '.' || quote_ident($2) + || '.' || quote_ident($3) || ' should be type ' || quote_ident($4) || '.' || $5); +$$ LANGUAGE SQL; + +-- col_type_is( schema, table, column, type, description ) +CREATE OR REPLACE FUNCTION col_type_is ( NAME, NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have_type TEXT; + want_type TEXT; +BEGIN + -- Get the data type. + IF $1 IS NULL THEN + have_type := _get_col_type($2, $3); + ELSE + have_type := _get_col_type($1, $2, $3); + END IF; + + IF have_type IS NULL THEN + RETURN fail( $5 ) || E'\n' || diag ( + ' Column ' || COALESCE(quote_ident($1) || '.', '') + || quote_ident($2) || '.' || quote_ident($3) || ' does not exist' + ); + END IF; + + want_type := _quote_ident_like($4, have_type); + IF have_type = want_type THEN + -- We're good to go. + RETURN ok( true, $5 ); + END IF; + + -- Wrong data type. tell 'em what we really got. + RETURN ok( false, $5 ) || E'\n' || diag( + ' have: ' || have_type || + E'\n want: ' || want_type + ); +END; +$$ LANGUAGE plpgsql; + +-- col_type_is( schema, table, column, type ) +CREATE OR REPLACE FUNCTION col_type_is ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_type_is( $1, $2, $3, $4, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || '.' || quote_ident($3) || ' should be type ' || $4 ); +$$ LANGUAGE SQL; + +-- col_type_is( table, column, type, description ) +CREATE OR REPLACE FUNCTION col_type_is ( NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT col_type_is( NULL, $1, $2, $3, $4 ); +$$ LANGUAGE SQL; + +-- col_type_is( table, column, type ) +CREATE OR REPLACE FUNCTION col_type_is ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_type_is( $1, $2, $3, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should be type ' || $3 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _has_def ( NAME, NAME, NAME ) +RETURNS boolean AS $$ + SELECT a.atthasdef + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $3 +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _has_def ( NAME, NAME ) +RETURNS boolean AS $$ + SELECT a.atthasdef + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE c.relname = $1 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $2 +$$ LANGUAGE sql; + +-- col_has_default( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_has_default ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2, $3 ) THEN + RETURN fail( $4 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || '.' || quote_ident($3) || ' does not exist' ); + END IF; + RETURN ok( _has_def( $1, $2, $3 ), $4 ); +END +$$ LANGUAGE plpgsql; + +-- col_has_default( table, column, description ) +CREATE OR REPLACE FUNCTION col_has_default ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2 ) THEN + RETURN fail( $3 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || ' does not exist' ); + END IF; + RETURN ok( _has_def( $1, $2 ), $3 ); +END; +$$ LANGUAGE plpgsql; + +-- col_has_default( table, column ) +CREATE OR REPLACE FUNCTION col_has_default ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_has_default( $1, $2, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should have a default' ); +$$ LANGUAGE SQL; + +-- col_hasnt_default( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_hasnt_default ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2, $3 ) THEN + RETURN fail( $4 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || '.' || quote_ident($3) || ' does not exist' ); + END IF; + RETURN ok( NOT _has_def( $1, $2, $3 ), $4 ); +END; +$$ LANGUAGE plpgsql; + +-- col_hasnt_default( table, column, description ) +CREATE OR REPLACE FUNCTION col_hasnt_default ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2 ) THEN + RETURN fail( $3 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || ' does not exist' ); + END IF; + RETURN ok( NOT _has_def( $1, $2 ), $3 ); +END; +$$ LANGUAGE plpgsql; + +-- col_hasnt_default( table, column ) +CREATE OR REPLACE FUNCTION col_hasnt_default ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_hasnt_default( $1, $2, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should not have a default' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _def_is( TEXT, TEXT, anyelement, TEXT ) +RETURNS TEXT AS $$ +DECLARE + thing text; +BEGIN + IF $1 ~ '^[^'']+[(]' THEN + -- It's a functional default. + RETURN is( $1, $3, $4 ); + END IF; + + EXECUTE 'SELECT is(' + || COALESCE($1, 'NULL' || '::' || $2) || '::' || $2 || ', ' + || COALESCE(quote_literal($3), 'NULL') || '::' || $2 || ', ' + || COALESCE(quote_literal($4), 'NULL') + || ')' INTO thing; + RETURN thing; +END; +$$ LANGUAGE plpgsql; + +-- _cdi( schema, table, column, default, description ) +CREATE OR REPLACE FUNCTION _cdi ( NAME, NAME, NAME, anyelement, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2, $3 ) THEN + RETURN fail( $5 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || '.' || quote_ident($3) || ' does not exist' ); + END IF; + + IF NOT _has_def( $1, $2, $3 ) THEN + RETURN fail( $5 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || '.' || quote_ident($3) || ' has no default' ); + END IF; + + RETURN _def_is( + pg_catalog.pg_get_expr(d.adbin, d.adrelid), + display_type(a.atttypid, a.atttypmod), + $4, $5 + ) + FROM pg_catalog.pg_namespace n, pg_catalog.pg_class c, pg_catalog.pg_attribute a, + pg_catalog.pg_attrdef d + WHERE n.oid = c.relnamespace + AND c.oid = a.attrelid + AND a.atthasdef + AND a.attrelid = d.adrelid + AND a.attnum = d.adnum + AND n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $3; +END; +$$ LANGUAGE plpgsql; + +-- _cdi( table, column, default, description ) +CREATE OR REPLACE FUNCTION _cdi ( NAME, NAME, anyelement, TEXT ) +RETURNS TEXT AS $$ +BEGIN + IF NOT _cexists( $1, $2 ) THEN + RETURN fail( $4 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || ' does not exist' ); + END IF; + + IF NOT _has_def( $1, $2 ) THEN + RETURN fail( $4 ) || E'\n' + || diag (' Column ' || quote_ident($1) || '.' || quote_ident($2) || ' has no default' ); + END IF; + + RETURN _def_is( + pg_catalog.pg_get_expr(d.adbin, d.adrelid), + display_type(a.atttypid, a.atttypmod), + $3, $4 + ) + FROM pg_catalog.pg_class c, pg_catalog.pg_attribute a, pg_catalog.pg_attrdef d + WHERE c.oid = a.attrelid + AND pg_table_is_visible(c.oid) + AND a.atthasdef + AND a.attrelid = d.adrelid + AND a.attnum = d.adnum + AND c.relname = $1 + AND a.attnum > 0 + AND NOT a.attisdropped + AND a.attname = $2; +END; +$$ LANGUAGE plpgsql; + +-- _cdi( table, column, default ) +CREATE OR REPLACE FUNCTION _cdi ( NAME, NAME, anyelement ) +RETURNS TEXT AS $$ + SELECT col_default_is( + $1, $2, $3, + 'Column ' || quote_ident($1) || '.' || quote_ident($2) || ' should default to ' + || COALESCE( quote_literal($3), 'NULL') + ); +$$ LANGUAGE sql; + +-- col_default_is( schema, table, column, default, description ) +CREATE OR REPLACE FUNCTION col_default_is ( NAME, NAME, NAME, anyelement, TEXT ) +RETURNS TEXT AS $$ + SELECT _cdi( $1, $2, $3, $4, $5 ); +$$ LANGUAGE sql; + +-- col_default_is( schema, table, column, default, description ) +CREATE OR REPLACE FUNCTION col_default_is ( NAME, NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _cdi( $1, $2, $3, $4, $5 ); +$$ LANGUAGE sql; + +-- col_default_is( table, column, default, description ) +CREATE OR REPLACE FUNCTION col_default_is ( NAME, NAME, anyelement, TEXT ) +RETURNS TEXT AS $$ + SELECT _cdi( $1, $2, $3, $4 ); +$$ LANGUAGE sql; + +-- col_default_is( table, column, default, description ) +CREATE OR REPLACE FUNCTION col_default_is ( NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _cdi( $1, $2, $3, $4 ); +$$ LANGUAGE sql; + +-- col_default_is( table, column, default ) +CREATE OR REPLACE FUNCTION col_default_is ( NAME, NAME, anyelement ) +RETURNS TEXT AS $$ + SELECT _cdi( $1, $2, $3 ); +$$ LANGUAGE sql; + +-- col_default_is( table, column, default::text ) +CREATE OR REPLACE FUNCTION col_default_is ( NAME, NAME, text ) +RETURNS TEXT AS $$ + SELECT _cdi( $1, $2, $3 ); +$$ LANGUAGE sql; + +-- _hasc( schema, table, constraint_type ) +CREATE OR REPLACE FUNCTION _hasc ( NAME, NAME, CHAR ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON c.relnamespace = n.oid + JOIN pg_catalog.pg_constraint x ON c.oid = x.conrelid + WHERE c.relhaspkey = true + AND n.nspname = $1 + AND c.relname = $2 + AND x.contype = $3 + ); +$$ LANGUAGE sql; + +-- _hasc( table, constraint_type ) +CREATE OR REPLACE FUNCTION _hasc ( NAME, CHAR ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_constraint x ON c.oid = x.conrelid + WHERE c.relhaspkey = true + AND pg_table_is_visible(c.oid) + AND c.relname = $1 + AND x.contype = $2 + ); +$$ LANGUAGE sql; + +-- has_pk( schema, table, description ) +CREATE OR REPLACE FUNCTION has_pk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, $2, 'p' ), $3 ); +$$ LANGUAGE sql; + +-- has_pk( table, description ) +CREATE OR REPLACE FUNCTION has_pk ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, 'p' ), $2 ); +$$ LANGUAGE sql; + +-- has_pk( table ) +CREATE OR REPLACE FUNCTION has_pk ( NAME ) +RETURNS TEXT AS $$ + SELECT has_pk( $1, 'Table ' || quote_ident($1) || ' should have a primary key' ); +$$ LANGUAGE sql; + +-- hasnt_pk( schema, table, description ) +CREATE OR REPLACE FUNCTION hasnt_pk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _hasc( $1, $2, 'p' ), $3 ); +$$ LANGUAGE sql; + +-- hasnt_pk( table, description ) +CREATE OR REPLACE FUNCTION hasnt_pk ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _hasc( $1, 'p' ), $2 ); +$$ LANGUAGE sql; + +-- hasnt_pk( table ) +CREATE OR REPLACE FUNCTION hasnt_pk ( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_pk( $1, 'Table ' || quote_ident($1) || ' should not have a primary key' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _ident_array_to_string( name[], text ) +RETURNS text AS $$ + SELECT array_to_string(ARRAY( + SELECT quote_ident($1[i]) + FROM generate_series(1, array_upper($1, 1)) s(i) + ORDER BY i + ), $2); +$$ LANGUAGE SQL immutable; + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +CREATE OR REPLACE FUNCTION _pg_sv_column_array( OID, SMALLINT[] ) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT a.attname + FROM pg_catalog.pg_attribute a + JOIN generate_series(1, array_upper($2, 1)) s(i) ON a.attnum = $2[i] + WHERE attrelid = $1 + ORDER BY i + ) +$$ LANGUAGE SQL stable; + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +CREATE OR REPLACE FUNCTION _pg_sv_table_accessible( OID, OID ) +RETURNS BOOLEAN AS $$ + SELECT CASE WHEN has_schema_privilege($1, 'USAGE') THEN ( + has_table_privilege($2, 'SELECT') + OR has_table_privilege($2, 'INSERT') + or has_table_privilege($2, 'UPDATE') + OR has_table_privilege($2, 'DELETE') + OR has_table_privilege($2, 'RULE') + OR has_table_privilege($2, 'REFERENCES') + OR has_table_privilege($2, 'TRIGGER') + ) ELSE FALSE + END; +$$ LANGUAGE SQL immutable strict; + +-- Borrowed from newsysviews: http://pgfoundry.org/projects/newsysviews/ +CREATE OR REPLACE VIEW pg_all_foreign_keys +AS + SELECT n1.nspname AS fk_schema_name, + c1.relname AS fk_table_name, + k1.conname AS fk_constraint_name, + c1.oid AS fk_table_oid, + _pg_sv_column_array(k1.conrelid,k1.conkey) AS fk_columns, + n2.nspname AS pk_schema_name, + c2.relname AS pk_table_name, + k2.conname AS pk_constraint_name, + c2.oid AS pk_table_oid, + ci.relname AS pk_index_name, + _pg_sv_column_array(k1.confrelid,k1.confkey) AS pk_columns, + CASE k1.confmatchtype WHEN 'f' THEN 'FULL' + WHEN 'p' THEN 'PARTIAL' + WHEN 'u' THEN 'NONE' + else null + END AS match_type, + CASE k1.confdeltype WHEN 'a' THEN 'NO ACTION' + WHEN 'c' THEN 'CASCADE' + WHEN 'd' THEN 'SET DEFAULT' + WHEN 'n' THEN 'SET NULL' + WHEN 'r' THEN 'RESTRICT' + else null + END AS on_delete, + CASE k1.confupdtype WHEN 'a' THEN 'NO ACTION' + WHEN 'c' THEN 'CASCADE' + WHEN 'd' THEN 'SET DEFAULT' + WHEN 'n' THEN 'SET NULL' + WHEN 'r' THEN 'RESTRICT' + ELSE NULL + END AS on_update, + k1.condeferrable AS is_deferrable, + k1.condeferred AS is_deferred + FROM pg_catalog.pg_constraint k1 + JOIN pg_catalog.pg_namespace n1 ON (n1.oid = k1.connamespace) + JOIN pg_catalog.pg_class c1 ON (c1.oid = k1.conrelid) + JOIN pg_catalog.pg_class c2 ON (c2.oid = k1.confrelid) + JOIN pg_catalog.pg_namespace n2 ON (n2.oid = c2.relnamespace) + JOIN pg_catalog.pg_depend d ON ( + d.classid = 'pg_constraint'::regclass + AND d.objid = k1.oid + AND d.objsubid = 0 + AND d.deptype = 'n' + AND d.refclassid = 'pg_class'::regclass + AND d.refobjsubid=0 + ) + JOIN pg_catalog.pg_class ci ON (ci.oid = d.refobjid AND ci.relkind = 'i') + LEFT JOIN pg_depend d2 ON ( + d2.classid = 'pg_class'::regclass + AND d2.objid = ci.oid + AND d2.objsubid = 0 + AND d2.deptype = 'i' + AND d2.refclassid = 'pg_constraint'::regclass + AND d2.refobjsubid = 0 + ) + LEFT JOIN pg_catalog.pg_constraint k2 ON ( + k2.oid = d2.refobjid + AND k2.contype IN ('p', 'u') + ) + WHERE k1.conrelid != 0 + AND k1.confrelid != 0 + AND k1.contype = 'f' + AND _pg_sv_table_accessible(n1.oid, c1.oid); + +-- _keys( schema, table, constraint_type ) +CREATE OR REPLACE FUNCTION _keys ( NAME, NAME, CHAR ) +RETURNS SETOF NAME[] AS $$ + SELECT _pg_sv_column_array(x.conrelid,x.conkey) + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_constraint x ON c.oid = x.conrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND x.contype = $3 +$$ LANGUAGE sql; + +-- _keys( table, constraint_type ) +CREATE OR REPLACE FUNCTION _keys ( NAME, CHAR ) +RETURNS SETOF NAME[] AS $$ + SELECT _pg_sv_column_array(x.conrelid,x.conkey) + FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_constraint x ON c.oid = x.conrelid + AND c.relname = $1 + AND x.contype = $2 +$$ LANGUAGE sql; + +-- _ckeys( schema, table, constraint_type ) +CREATE OR REPLACE FUNCTION _ckeys ( NAME, NAME, CHAR ) +RETURNS NAME[] AS $$ + SELECT * FROM _keys($1, $2, $3) LIMIT 1; +$$ LANGUAGE sql; + +-- _ckeys( table, constraint_type ) +CREATE OR REPLACE FUNCTION _ckeys ( NAME, CHAR ) +RETURNS NAME[] AS $$ + SELECT * FROM _keys($1, $2) LIMIT 1; +$$ LANGUAGE sql; + +-- col_is_pk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT is( _ckeys( $1, $2, 'p' ), $3, $4 ); +$$ LANGUAGE sql; + +-- col_is_pk( table, column, description ) +CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT is( _ckeys( $1, 'p' ), $2, $3 ); +$$ LANGUAGE sql; + +-- col_is_pk( table, column[] ) +CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT col_is_pk( $1, $2, 'Columns ' || quote_ident($1) || '(' || _ident_array_to_string($2, ', ') || ') should be a primary key' ); +$$ LANGUAGE sql; + +-- col_is_pk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_is_pk( $1, $2, ARRAY[$3], $4 ); +$$ LANGUAGE sql; + +-- col_is_pk( table, column, description ) +CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_is_pk( $1, ARRAY[$2], $3 ); +$$ LANGUAGE sql; + +-- col_is_pk( table, column ) +CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_is_pk( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should be a primary key' ); +$$ LANGUAGE sql; + +-- col_isnt_pk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_pk ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT isnt( _ckeys( $1, $2, 'p' ), $3, $4 ); +$$ LANGUAGE sql; + +-- col_isnt_pk( table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_pk ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT isnt( _ckeys( $1, 'p' ), $2, $3 ); +$$ LANGUAGE sql; + +-- col_isnt_pk( table, column[] ) +CREATE OR REPLACE FUNCTION col_isnt_pk ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT col_isnt_pk( $1, $2, 'Columns ' || quote_ident($1) || '(' || _ident_array_to_string($2, ', ') || ') should not be a primary key' ); +$$ LANGUAGE sql; + +-- col_isnt_pk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_pk ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_isnt_pk( $1, $2, ARRAY[$3], $4 ); +$$ LANGUAGE sql; + +-- col_isnt_pk( table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_pk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_isnt_pk( $1, ARRAY[$2], $3 ); +$$ LANGUAGE sql; + +-- col_isnt_pk( table, column ) +CREATE OR REPLACE FUNCTION col_isnt_pk ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_isnt_pk( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should not be a primary key' ); +$$ LANGUAGE sql; + +-- has_fk( schema, table, description ) +CREATE OR REPLACE FUNCTION has_fk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, $2, 'f' ), $3 ); +$$ LANGUAGE sql; + +-- has_fk( table, description ) +CREATE OR REPLACE FUNCTION has_fk ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, 'f' ), $2 ); +$$ LANGUAGE sql; + +-- has_fk( table ) +CREATE OR REPLACE FUNCTION has_fk ( NAME ) +RETURNS TEXT AS $$ + SELECT has_fk( $1, 'Table ' || quote_ident($1) || ' should have a foreign key constraint' ); +$$ LANGUAGE sql; + +-- hasnt_fk( schema, table, description ) +CREATE OR REPLACE FUNCTION hasnt_fk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _hasc( $1, $2, 'f' ), $3 ); +$$ LANGUAGE sql; + +-- hasnt_fk( table, description ) +CREATE OR REPLACE FUNCTION hasnt_fk ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _hasc( $1, 'f' ), $2 ); +$$ LANGUAGE sql; + +-- hasnt_fk( table ) +CREATE OR REPLACE FUNCTION hasnt_fk ( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_fk( $1, 'Table ' || quote_ident($1) || ' should not have a foreign key constraint' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _fkexists ( NAME, NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT TRUE + FROM pg_all_foreign_keys + WHERE fk_schema_name = $1 + AND quote_ident(fk_table_name) = quote_ident($2) + AND fk_columns = $3 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _fkexists ( NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT TRUE + FROM pg_all_foreign_keys + WHERE quote_ident(fk_table_name) = quote_ident($1) + AND fk_columns = $2 + ); +$$ LANGUAGE SQL; + +-- col_is_fk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_fk ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + names text[]; +BEGIN + IF _fkexists($1, $2, $3) THEN + RETURN pass( $4 ); + END IF; + + -- Try to show the columns. + SELECT ARRAY( + SELECT _ident_array_to_string(fk_columns, ', ') + FROM pg_all_foreign_keys + WHERE fk_schema_name = $1 + AND fk_table_name = $2 + ORDER BY fk_columns + ) INTO names; + + IF names[1] IS NOT NULL THEN + RETURN fail($4) || E'\n' || diag( + ' Table ' || quote_ident($1) || '.' || quote_ident($2) || E' has foreign key constraints on these columns:\n ' + || array_to_string( names, E'\n ' ) + ); + END IF; + + -- No FKs in this table. + RETURN fail($4) || E'\n' || diag( + ' Table ' || quote_ident($1) || '.' || quote_ident($2) || ' has no foreign key columns' + ); +END; +$$ LANGUAGE plpgsql; + +-- col_is_fk( table, column, description ) +CREATE OR REPLACE FUNCTION col_is_fk ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + names text[]; +BEGIN + IF _fkexists($1, $2) THEN + RETURN pass( $3 ); + END IF; + + -- Try to show the columns. + SELECT ARRAY( + SELECT _ident_array_to_string(fk_columns, ', ') + FROM pg_all_foreign_keys + WHERE fk_table_name = $1 + ORDER BY fk_columns + ) INTO names; + + IF NAMES[1] IS NOT NULL THEN + RETURN fail($3) || E'\n' || diag( + ' Table ' || quote_ident($1) || E' has foreign key constraints on these columns:\n ' + || array_to_string( names, E'\n ' ) + ); + END IF; + + -- No FKs in this table. + RETURN fail($3) || E'\n' || diag( + ' Table ' || quote_ident($1) || ' has no foreign key columns' + ); +END; +$$ LANGUAGE plpgsql; + +-- col_is_fk( table, column[] ) +CREATE OR REPLACE FUNCTION col_is_fk ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT col_is_fk( $1, $2, 'Columns ' || quote_ident($1) || '(' || _ident_array_to_string($2, ', ') || ') should be a foreign key' ); +$$ LANGUAGE sql; + +-- col_is_fk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_fk ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_is_fk( $1, $2, ARRAY[$3], $4 ); +$$ LANGUAGE sql; + +-- col_is_fk( table, column, description ) +CREATE OR REPLACE FUNCTION col_is_fk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_is_fk( $1, ARRAY[$2], $3 ); +$$ LANGUAGE sql; + +-- col_is_fk( table, column ) +CREATE OR REPLACE FUNCTION col_is_fk ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_is_fk( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should be a foreign key' ); +$$ LANGUAGE sql; + +-- col_isnt_fk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_fk ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _fkexists( $1, $2, $3 ), $4 ); +$$ LANGUAGE SQL; + +-- col_isnt_fk( table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_fk ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _fkexists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- col_isnt_fk( table, column[] ) +CREATE OR REPLACE FUNCTION col_isnt_fk ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT col_isnt_fk( $1, $2, 'Columns ' || quote_ident($1) || '(' || _ident_array_to_string($2, ', ') || ') should not be a foreign key' ); +$$ LANGUAGE sql; + +-- col_isnt_fk( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_fk ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_isnt_fk( $1, $2, ARRAY[$3], $4 ); +$$ LANGUAGE sql; + +-- col_isnt_fk( table, column, description ) +CREATE OR REPLACE FUNCTION col_isnt_fk ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_isnt_fk( $1, ARRAY[$2], $3 ); +$$ LANGUAGE sql; + +-- col_isnt_fk( table, column ) +CREATE OR REPLACE FUNCTION col_isnt_fk ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_isnt_fk( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should not be a foreign key' ); +$$ LANGUAGE sql; + +-- has_unique( schema, table, description ) +CREATE OR REPLACE FUNCTION has_unique ( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, $2, 'u' ), $3 ); +$$ LANGUAGE sql; + +-- has_unique( table, description ) +CREATE OR REPLACE FUNCTION has_unique ( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, 'u' ), $2 ); +$$ LANGUAGE sql; + +-- has_unique( table ) +CREATE OR REPLACE FUNCTION has_unique ( TEXT ) +RETURNS TEXT AS $$ + SELECT has_unique( $1, 'Table ' || quote_ident($1) || ' should have a unique constraint' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _constraint ( NAME, NAME, CHAR, NAME[], TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + akey NAME[]; + keys TEXT[] := '{}'; + have TEXT; +BEGIN + FOR akey IN SELECT * FROM _keys($1, $2, $3) LOOP + IF akey = $4 THEN RETURN pass($5); END IF; + keys = keys || akey::text; + END LOOP; + IF array_upper(keys, 0) = 1 THEN + have := 'No ' || $6 || ' constriants'; + ELSE + have := array_to_string(keys, E'\n '); + END IF; + + RETURN fail($5) || E'\n' || diag( + ' have: ' || have + || E'\n want: ' || CASE WHEN $4 IS NULL THEN 'NULL' ELSE $4::text END + ); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _constraint ( NAME, CHAR, NAME[], TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + akey NAME[]; + keys TEXT[] := '{}'; + have TEXT; +BEGIN + FOR akey IN SELECT * FROM _keys($1, $2) LOOP + IF akey = $3 THEN RETURN pass($4); END IF; + keys = keys || akey::text; + END LOOP; + IF array_upper(keys, 0) = 1 THEN + have := 'No ' || $5 || ' constriants'; + ELSE + have := array_to_string(keys, E'\n '); + END IF; + + RETURN fail($4) || E'\n' || diag( + ' have: ' || have + || E'\n want: ' || CASE WHEN $3 IS NULL THEN 'NULL' ELSE $3::text END + ); +END; +$$ LANGUAGE plpgsql; + +-- col_is_unique( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_unique ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _constraint( $1, $2, 'u', $3, $4, 'unique' ); +$$ LANGUAGE sql; + +-- col_is_unique( table, column, description ) +CREATE OR REPLACE FUNCTION col_is_unique ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _constraint( $1, 'u', $2, $3, 'unique' ); +$$ LANGUAGE sql; + +-- col_is_unique( table, column[] ) +CREATE OR REPLACE FUNCTION col_is_unique ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT col_is_unique( $1, $2, 'Columns ' || quote_ident($1) || '(' || _ident_array_to_string($2, ', ') || ') should have a unique constraint' ); +$$ LANGUAGE sql; + +-- col_is_unique( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_is_unique ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_is_unique( $1, $2, ARRAY[$3], $4 ); +$$ LANGUAGE sql; + +-- col_is_unique( table, column, description ) +CREATE OR REPLACE FUNCTION col_is_unique ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_is_unique( $1, ARRAY[$2], $3 ); +$$ LANGUAGE sql; + +-- col_is_unique( table, column ) +CREATE OR REPLACE FUNCTION col_is_unique ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_is_unique( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should have a unique constraint' ); +$$ LANGUAGE sql; + +-- has_check( schema, table, description ) +CREATE OR REPLACE FUNCTION has_check ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, $2, 'c' ), $3 ); +$$ LANGUAGE sql; + +-- has_check( table, description ) +CREATE OR REPLACE FUNCTION has_check ( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _hasc( $1, 'c' ), $2 ); +$$ LANGUAGE sql; + +-- has_check( table ) +CREATE OR REPLACE FUNCTION has_check ( NAME ) +RETURNS TEXT AS $$ + SELECT has_check( $1, 'Table ' || quote_ident($1) || ' should have a check constraint' ); +$$ LANGUAGE sql; + +-- col_has_check( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_has_check ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _constraint( $1, $2, 'c', $3, $4, 'check' ); +$$ LANGUAGE sql; + +-- col_has_check( table, column, description ) +CREATE OR REPLACE FUNCTION col_has_check ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _constraint( $1, 'c', $2, $3, 'check' ); +$$ LANGUAGE sql; + +-- col_has_check( table, column[] ) +CREATE OR REPLACE FUNCTION col_has_check ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT col_has_check( $1, $2, 'Columns ' || quote_ident($1) || '(' || _ident_array_to_string($2, ', ') || ') should have a check constraint' ); +$$ LANGUAGE sql; + +-- col_has_check( schema, table, column, description ) +CREATE OR REPLACE FUNCTION col_has_check ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_has_check( $1, $2, ARRAY[$3], $4 ); +$$ LANGUAGE sql; + +-- col_has_check( table, column, description ) +CREATE OR REPLACE FUNCTION col_has_check ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT col_has_check( $1, ARRAY[$2], $3 ); +$$ LANGUAGE sql; + +-- col_has_check( table, column ) +CREATE OR REPLACE FUNCTION col_has_check ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT col_has_check( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should have a check constraint' ); +$$ LANGUAGE sql; + +-- fk_ok( fk_schema, fk_table, fk_column[], pk_schema, pk_table, pk_column[], description ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME, NAME[], NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + sch name; + tab name; + cols name[]; +BEGIN + SELECT pk_schema_name, pk_table_name, pk_columns + FROM pg_all_foreign_keys + WHERE fk_schema_name = $1 + AND fk_table_name = $2 + AND fk_columns = $3 + INTO sch, tab, cols; + + RETURN is( + -- have + quote_ident($1) || '.' || quote_ident($2) || '(' || _ident_array_to_string( $3, ', ' ) + || ') REFERENCES ' || COALESCE ( sch || '.' || tab || '(' || _ident_array_to_string( cols, ', ' ) || ')', 'NOTHING' ), + -- want + quote_ident($1) || '.' || quote_ident($2) || '(' || _ident_array_to_string( $3, ', ' ) + || ') REFERENCES ' || + $4 || '.' || $5 || '(' || _ident_array_to_string( $6, ', ' ) || ')', + $7 + ); +END; +$$ LANGUAGE plpgsql; + +-- fk_ok( fk_table, fk_column[], pk_table, pk_column[], description ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME[], NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + tab name; + cols name[]; +BEGIN + SELECT pk_table_name, pk_columns + FROM pg_all_foreign_keys + WHERE fk_table_name = $1 + AND fk_columns = $2 + INTO tab, cols; + + RETURN is( + -- have + $1 || '(' || _ident_array_to_string( $2, ', ' ) + || ') REFERENCES ' || COALESCE( tab || '(' || _ident_array_to_string( cols, ', ' ) || ')', 'NOTHING'), + -- want + $1 || '(' || _ident_array_to_string( $2, ', ' ) + || ') REFERENCES ' || + $3 || '(' || _ident_array_to_string( $4, ', ' ) || ')', + $5 + ); +END; +$$ LANGUAGE plpgsql; + +-- fk_ok( fk_schema, fk_table, fk_column[], fk_schema, pk_table, pk_column[] ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME, NAME[], NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT fk_ok( $1, $2, $3, $4, $5, $6, + quote_ident($1) || '.' || quote_ident($2) || '(' || _ident_array_to_string( $3, ', ' ) + || ') should reference ' || + $4 || '.' || $5 || '(' || _ident_array_to_string( $6, ', ' ) || ')' + ); +$$ LANGUAGE sql; + +-- fk_ok( fk_table, fk_column[], pk_table, pk_column[] ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME[], NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT fk_ok( $1, $2, $3, $4, + $1 || '(' || _ident_array_to_string( $2, ', ' ) + || ') should reference ' || + $3 || '(' || _ident_array_to_string( $4, ', ' ) || ')' + ); +$$ LANGUAGE sql; + +-- fk_ok( fk_schema, fk_table, fk_column, pk_schema, pk_table, pk_column, description ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME, NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT fk_ok( $1, $2, ARRAY[$3], $4, $5, ARRAY[$6], $7 ); +$$ LANGUAGE sql; + +-- fk_ok( fk_schema, fk_table, fk_column, pk_schema, pk_table, pk_column ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT fk_ok( $1, $2, ARRAY[$3], $4, $5, ARRAY[$6] ); +$$ LANGUAGE sql; + +-- fk_ok( fk_table, fk_column, pk_table, pk_column, description ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT fk_ok( $1, ARRAY[$2], $3, ARRAY[$4], $5 ); +$$ LANGUAGE sql; + +-- fk_ok( fk_table, fk_column, pk_table, pk_column ) +CREATE OR REPLACE FUNCTION fk_ok ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT fk_ok( $1, ARRAY[$2], $3, ARRAY[$4] ); +$$ LANGUAGE sql; + +CREATE OR REPLACE VIEW tap_funky + AS SELECT p.oid AS oid, + n.nspname AS schema, + p.proname AS name, + array_to_string(p.proargtypes::regtype[], ',') AS args, + CASE p.proretset WHEN TRUE THEN 'setof ' ELSE '' END + || p.prorettype::regtype AS returns, + p.prolang AS langoid, + p.proisstrict AS is_strict, + p.proisagg AS is_agg, + p.prosecdef AS is_definer, + p.proretset AS returns_set, + p.provolatile::char AS volatility, + pg_catalog.pg_function_is_visible(p.oid) AS is_visible + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid +; + +CREATE OR REPLACE FUNCTION _got_func ( NAME, NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT TRUE + FROM tap_funky + WHERE schema = $1 + AND name = $2 + AND args = array_to_string($3, ',') + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _got_func ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( SELECT TRUE FROM tap_funky WHERE schema = $1 AND name = $2 ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _got_func ( NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT TRUE + FROM tap_funky + WHERE name = $1 + AND args = array_to_string($2, ',') + AND is_visible + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _got_func ( NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( SELECT TRUE FROM tap_funky WHERE name = $1 AND is_visible); +$$ LANGUAGE SQL; + +-- has_function( schema, function, args[], description ) +CREATE OR REPLACE FUNCTION has_function ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _got_func($1, $2, $3), $4 ); +$$ LANGUAGE SQL; + +-- has_function( schema, function, args[] ) +CREATE OR REPLACE FUNCTION has_function( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _got_func($1, $2, $3), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should exist' + ); +$$ LANGUAGE sql; + +-- has_function( schema, function, description ) +CREATE OR REPLACE FUNCTION has_function ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _got_func($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- has_function( schema, function ) +CREATE OR REPLACE FUNCTION has_function( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _got_func($1, $2), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should exist' + ); +$$ LANGUAGE sql; + +-- has_function( function, args[], description ) +CREATE OR REPLACE FUNCTION has_function ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _got_func($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- has_function( function, args[] ) +CREATE OR REPLACE FUNCTION has_function( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _got_func($1, $2), + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should exist' + ); +$$ LANGUAGE sql; + +-- has_function( function, description ) +CREATE OR REPLACE FUNCTION has_function( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _got_func($1), $2 ); +$$ LANGUAGE sql; + +-- has_function( function ) +CREATE OR REPLACE FUNCTION has_function( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _got_func($1), 'Function ' || quote_ident($1) || '() should exist' ); +$$ LANGUAGE sql; + +-- hasnt_function( schema, function, args[], description ) +CREATE OR REPLACE FUNCTION hasnt_function ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _got_func($1, $2, $3), $4 ); +$$ LANGUAGE SQL; + +-- hasnt_function( schema, function, args[] ) +CREATE OR REPLACE FUNCTION hasnt_function( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _got_func($1, $2, $3), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should not exist' + ); +$$ LANGUAGE sql; + +-- hasnt_function( schema, function, description ) +CREATE OR REPLACE FUNCTION hasnt_function ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _got_func($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_function( schema, function ) +CREATE OR REPLACE FUNCTION hasnt_function( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _got_func($1, $2), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should not exist' + ); +$$ LANGUAGE sql; + +-- hasnt_function( function, args[], description ) +CREATE OR REPLACE FUNCTION hasnt_function ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _got_func($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_function( function, args[] ) +CREATE OR REPLACE FUNCTION hasnt_function( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _got_func($1, $2), + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should not exist' + ); +$$ LANGUAGE sql; + +-- hasnt_function( function, description ) +CREATE OR REPLACE FUNCTION hasnt_function( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _got_func($1), $2 ); +$$ LANGUAGE sql; + +-- hasnt_function( function ) +CREATE OR REPLACE FUNCTION hasnt_function( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _got_func($1), 'Function ' || quote_ident($1) || '() should not exist' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _pg_sv_type_array( OID[] ) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT t.typname + FROM pg_catalog.pg_type t + JOIN generate_series(1, array_upper($1, 1)) s(i) ON t.oid = $1[i] + ORDER BY i + ) +$$ LANGUAGE SQL stable; + +-- can( schema, functions[], description ) +CREATE OR REPLACE FUNCTION can ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + missing text[]; +BEGIN + SELECT ARRAY( + SELECT quote_ident($2[i]) + FROM generate_series(1, array_upper($2, 1)) s(i) + LEFT JOIN tap_funky ON name = $2[i] AND schema = $1 + WHERE oid IS NULL + GROUP BY $2[i], s.i + ORDER BY MIN(s.i) + ) INTO missing; + IF missing[1] IS NULL THEN + RETURN ok( true, $3 ); + END IF; + RETURN ok( false, $3 ) || E'\n' || diag( + ' ' || quote_ident($1) || '.' || + array_to_string( missing, E'() missing\n ' || quote_ident($1) || '.') || + '() missing' + ); +END; +$$ LANGUAGE plpgsql; + +-- can( schema, functions[] ) +CREATE OR REPLACE FUNCTION can ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT can( $1, $2, 'Schema ' || quote_ident($1) || ' can' ); +$$ LANGUAGE sql; + +-- can( functions[], description ) +CREATE OR REPLACE FUNCTION can ( NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + missing text[]; +BEGIN + SELECT ARRAY( + SELECT quote_ident($1[i]) + FROM generate_series(1, array_upper($1, 1)) s(i) + LEFT JOIN pg_catalog.pg_proc p + ON $1[i] = p.proname + AND pg_catalog.pg_function_is_visible(p.oid) + WHERE p.oid IS NULL + ORDER BY s.i + ) INTO missing; + IF missing[1] IS NULL THEN + RETURN ok( true, $2 ); + END IF; + RETURN ok( false, $2 ) || E'\n' || diag( + ' ' || + array_to_string( missing, E'() missing\n ') || + '() missing' + ); +END; +$$ LANGUAGE plpgsql; + +-- can( functions[] ) +CREATE OR REPLACE FUNCTION can ( NAME[] ) +RETURNS TEXT AS $$ + SELECT can( $1, 'Schema ' || _ident_array_to_string(current_schemas(true), ' or ') || ' can' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _ikeys( NAME, NAME, NAME) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT a.attname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + JOIN pg_catalog.pg_attribute a ON ct.oid = a.attrelid + JOIN generate_series(0, current_setting('max_index_keys')::int - 1) s(i) + ON a.attnum = x.indkey[s.i] + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 + ORDER BY s.i + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _ikeys( NAME, NAME) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT a.attname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_attribute a ON ct.oid = a.attrelid + JOIN generate_series(0, current_setting('max_index_keys')::int - 1) s(i) + ON a.attnum = x.indkey[s.i] + WHERE ct.relname = $1 + AND ci.relname = $2 + AND pg_catalog.pg_table_is_visible(ct.oid) + ORDER BY s.i + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _have_index( NAME, NAME, NAME) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _have_index( NAME, NAME) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + WHERE ct.relname = $1 + AND ci.relname = $2 + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _iexpr( NAME, NAME, NAME) +RETURNS TEXT AS $$ + SELECT pg_catalog.pg_get_expr( x.indexprs, ct.oid ) + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _iexpr( NAME, NAME) +RETURNS TEXT AS $$ + SELECT pg_catalog.pg_get_expr( x.indexprs, ct.oid ) + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + WHERE ct.relname = $1 + AND ci.relname = $2 + AND pg_catalog.pg_table_is_visible(ct.oid) +$$ LANGUAGE sql; + +-- has_index( schema, table, index, columns[], description ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME, NAME[], text ) +RETURNS TEXT AS $$ +DECLARE + index_cols name[]; +BEGIN + index_cols := _ikeys($1, $2, $3 ); + + IF index_cols IS NULL OR index_cols = '{}'::name[] THEN + RETURN ok( false, $5 ) || E'\n' + || diag( 'Index ' || quote_ident($3) || ' ON ' || quote_ident($1) || '.' || quote_ident($2) || ' not found'); + END IF; + + RETURN is( + quote_ident($3) || ' ON ' || quote_ident($1) || '.' || quote_ident($2) || '(' || _ident_array_to_string( index_cols, ', ' ) || ')', + quote_ident($3) || ' ON ' || quote_ident($1) || '.' || quote_ident($2) || '(' || _ident_array_to_string( $4, ', ' ) || ')', + $5 + ); +END; +$$ LANGUAGE plpgsql; + +-- has_index( schema, table, index, columns[] ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT has_index( $1, $2, $3, $4, 'Index ' || quote_ident($3) || ' should exist' ); +$$ LANGUAGE sql; + +-- has_index( schema, table, index, column/expression, description ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + expr text; +BEGIN + IF $4 NOT LIKE '%(%' THEN + -- Not a functional index. + RETURN has_index( $1, $2, $3, ARRAY[$4], $5 ); + END IF; + + -- Get the functional expression. + expr := _iexpr($1, $2, $3); + + IF expr IS NULL THEN + RETURN ok( false, $5 ) || E'\n' + || diag( 'Index ' || quote_ident($3) || ' ON ' || quote_ident($1) || '.' || quote_ident($2) || ' not found'); + END IF; + + RETURN is( + quote_ident($3) || ' ON ' || quote_ident($1) || '.' || quote_ident($2) || '(' || expr || ')', + quote_ident($3) || ' ON ' || quote_ident($1) || '.' || quote_ident($2) || '(' || $4 || ')', + $5 + ); +END; +$$ LANGUAGE plpgsql; + +-- has_index( schema, table, index, columns/expression ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT has_index( $1, $2, $3, $4, 'Index ' || quote_ident($3) || ' should exist' ); +$$ LANGUAGE sql; + +-- has_index( table, index, columns[], description ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME[], text ) +RETURNS TEXT AS $$ +DECLARE + index_cols name[]; +BEGIN + index_cols := _ikeys($1, $2 ); + + IF index_cols IS NULL OR index_cols = '{}'::name[] THEN + RETURN ok( false, $4 ) || E'\n' + || diag( 'Index ' || quote_ident($2) || ' ON ' || quote_ident($1) || ' not found'); + END IF; + + RETURN is( + quote_ident($2) || ' ON ' || quote_ident($1) || '(' || _ident_array_to_string( index_cols, ', ' ) || ')', + quote_ident($2) || ' ON ' || quote_ident($1) || '(' || _ident_array_to_string( $3, ', ' ) || ')', + $4 + ); +END; +$$ LANGUAGE plpgsql; + +-- has_index( table, index, columns[], description ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT has_index( $1, $2, $3, 'Index ' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE sql; + +-- _is_schema( schema ) +CREATE OR REPLACE FUNCTION _is_schema( NAME ) +returns boolean AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_namespace + WHERE nspname = $1 + ); +$$ LANGUAGE sql; + +-- has_index( table, index, column/expression, description ) +-- has_index( schema, table, index, column/expression ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + want_expr text; + descr text; + have_expr text; + idx name; + tab text; +BEGIN + IF $3 NOT LIKE '%(%' THEN + -- Not a functional index. + IF _is_schema( $1 ) THEN + -- Looking for schema.table index. + RETURN ok ( _have_index( $1, $2, $3 ), $4); + END IF; + -- Looking for particular columns. + RETURN has_index( $1, $2, ARRAY[$3], $4 ); + END IF; + + -- Get the functional expression. + IF _is_schema( $1 ) THEN + -- Looking for an index within a schema. + have_expr := _iexpr($1, $2, $3); + want_expr := $4; + descr := 'Index ' || quote_ident($3) || ' should exist'; + idx := $3; + tab := quote_ident($1) || '.' || quote_ident($2); + ELSE + -- Looking for an index without a schema spec. + have_expr := _iexpr($1, $2); + want_expr := $3; + descr := $4; + idx := $2; + tab := quote_ident($1); + END IF; + + IF have_expr IS NULL THEN + RETURN ok( false, descr ) || E'\n' + || diag( 'Index ' || idx || ' ON ' || tab || ' not found'); + END IF; + + RETURN is( + quote_ident(idx) || ' ON ' || tab || '(' || have_expr || ')', + quote_ident(idx) || ' ON ' || tab || '(' || want_expr || ')', + descr + ); +END; +$$ LANGUAGE plpgsql; + +-- has_index( table, index, column/expression ) +-- has_index( schema, table, index ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ +BEGIN + IF _is_schema($1) THEN + -- ( schema, table, index ) + RETURN ok( _have_index( $1, $2, $3 ), 'Index ' || quote_ident($3) || ' should exist' ); + ELSE + -- ( table, index, column/expression ) + RETURN has_index( $1, $2, $3, 'Index ' || quote_ident($2) || ' should exist' ); + END IF; +END; +$$ LANGUAGE plpgsql; + +-- has_index( table, index, description ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME, text ) +RETURNS TEXT AS $$ + SELECT CASE WHEN $3 LIKE '%(%' + THEN has_index( $1, $2, $3::name ) + ELSE ok( _have_index( $1, $2 ), $3 ) + END; +$$ LANGUAGE sql; + +-- has_index( table, index ) +CREATE OR REPLACE FUNCTION has_index ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _have_index( $1, $2 ), 'Index ' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE sql; + +-- hasnt_index( schema, table, index, description ) +CREATE OR REPLACE FUNCTION hasnt_index ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +BEGIN + RETURN ok( NOT _have_index( $1, $2, $3 ), $4 ); +END; +$$ LANGUAGE plpgSQL; + +-- hasnt_index( schema, table, index ) +CREATE OR REPLACE FUNCTION hasnt_index ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _have_index( $1, $2, $3 ), + 'Index ' || quote_ident($3) || ' should not exist' + ); +$$ LANGUAGE SQL; + +-- hasnt_index( table, index, description ) +CREATE OR REPLACE FUNCTION hasnt_index ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _have_index( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_index( table, index ) +CREATE OR REPLACE FUNCTION hasnt_index ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _have_index( $1, $2 ), + 'Index ' || quote_ident($2) || ' should not exist' + ); +$$ LANGUAGE SQL; + +-- index_is_unique( schema, table, index, description ) +CREATE OR REPLACE FUNCTION index_is_unique ( NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisunique + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 + INTO res; + + RETURN ok( COALESCE(res, false), $4 ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_unique( schema, table, index ) +CREATE OR REPLACE FUNCTION index_is_unique ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT index_is_unique( + $1, $2, $3, + 'Index ' || quote_ident($3) || ' should be unique' + ); +$$ LANGUAGE sql; + +-- index_is_unique( table, index ) +CREATE OR REPLACE FUNCTION index_is_unique ( NAME, NAME ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisunique + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + WHERE ct.relname = $1 + AND ci.relname = $2 + AND pg_catalog.pg_table_is_visible(ct.oid) + INTO res; + + RETURN ok( + COALESCE(res, false), + 'Index ' || quote_ident($2) || ' should be unique' + ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_unique( index ) +CREATE OR REPLACE FUNCTION index_is_unique ( NAME ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisunique + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + WHERE ci.relname = $1 + AND pg_catalog.pg_table_is_visible(ct.oid) + INTO res; + + RETURN ok( + COALESCE(res, false), + 'Index ' || quote_ident($1) || ' should be unique' + ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_primary( schema, table, index, description ) +CREATE OR REPLACE FUNCTION index_is_primary ( NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisprimary + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 + INTO res; + + RETURN ok( COALESCE(res, false), $4 ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_primary( schema, table, index ) +CREATE OR REPLACE FUNCTION index_is_primary ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT index_is_primary( + $1, $2, $3, + 'Index ' || quote_ident($3) || ' should be on a primary key' + ); +$$ LANGUAGE sql; + +-- index_is_primary( table, index ) +CREATE OR REPLACE FUNCTION index_is_primary ( NAME, NAME ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisprimary + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + WHERE ct.relname = $1 + AND ci.relname = $2 + AND pg_catalog.pg_table_is_visible(ct.oid) + INTO res; + + RETURN ok( + COALESCE(res, false), + 'Index ' || quote_ident($2) || ' should be on a primary key' + ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_primary( index ) +CREATE OR REPLACE FUNCTION index_is_primary ( NAME ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisprimary + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + WHERE ci.relname = $1 + AND pg_catalog.pg_table_is_visible(ct.oid) + INTO res; + + RETURN ok( + COALESCE(res, false), + 'Index ' || quote_ident($1) || ' should be on a primary key' + ); +END; +$$ LANGUAGE plpgsql; + +-- is_clustered( schema, table, index, description ) +CREATE OR REPLACE FUNCTION is_clustered ( NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisclustered + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 + INTO res; + + RETURN ok( COALESCE(res, false), $4 ); +END; +$$ LANGUAGE plpgsql; + +-- is_clustered( schema, table, index ) +CREATE OR REPLACE FUNCTION is_clustered ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT is_clustered( + $1, $2, $3, + 'Table ' || quote_ident($1) || '.' || quote_ident($2) || + ' should be clustered on index ' || quote_ident($3) + ); +$$ LANGUAGE sql; + +-- is_clustered( table, index ) +CREATE OR REPLACE FUNCTION is_clustered ( NAME, NAME ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisclustered + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + WHERE ct.relname = $1 + AND ci.relname = $2 + INTO res; + + RETURN ok( + COALESCE(res, false), + 'Table ' || quote_ident($1) || ' should be clustered on index ' || quote_ident($2) + ); +END; +$$ LANGUAGE plpgsql; + +-- is_clustered( index ) +CREATE OR REPLACE FUNCTION is_clustered ( NAME ) +RETURNS TEXT AS $$ +DECLARE + res boolean; +BEGIN + SELECT x.indisclustered + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + WHERE ci.relname = $1 + INTO res; + + RETURN ok( + COALESCE(res, false), + 'Table should be clustered on index ' || quote_ident($1) + ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_type( schema, table, index, type, description ) +CREATE OR REPLACE FUNCTION index_is_type ( NAME, NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + aname name; +BEGIN + SELECT am.amname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + JOIN pg_catalog.pg_am am ON ci.relam = am.oid + WHERE ct.relname = $2 + AND ci.relname = $3 + AND n.nspname = $1 + INTO aname; + + return is( aname, $4, $5 ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_type( schema, table, index, type ) +CREATE OR REPLACE FUNCTION index_is_type ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT index_is_type( + $1, $2, $3, $4, + 'Index ' || quote_ident($3) || ' should be a ' || quote_ident($4) || ' index' + ); +$$ LANGUAGE SQL; + +-- index_is_type( table, index, type ) +CREATE OR REPLACE FUNCTION index_is_type ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ +DECLARE + aname name; +BEGIN + SELECT am.amname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_am am ON ci.relam = am.oid + WHERE ct.relname = $1 + AND ci.relname = $2 + INTO aname; + + return is( + aname, $3, + 'Index ' || quote_ident($2) || ' should be a ' || quote_ident($3) || ' index' + ); +END; +$$ LANGUAGE plpgsql; + +-- index_is_type( index, type ) +CREATE OR REPLACE FUNCTION index_is_type ( NAME, NAME ) +RETURNS TEXT AS $$ +DECLARE + aname name; +BEGIN + SELECT am.amname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_am am ON ci.relam = am.oid + WHERE ci.relname = $1 + INTO aname; + + return is( + aname, $2, + 'Index ' || quote_ident($1) || ' should be a ' || quote_ident($2) || ' index' + ); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _trig ( NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = $1 + AND c.relname = $2 + AND t.tgname = $3 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _trig ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid + WHERE c.relname = $1 + AND t.tgname = $2 + ); +$$ LANGUAGE SQL; + +-- has_trigger( schema, table, trigger, description ) +CREATE OR REPLACE FUNCTION has_trigger ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _trig($1, $2, $3), $4); +$$ LANGUAGE SQL; + +-- has_trigger( schema, table, trigger ) +CREATE OR REPLACE FUNCTION has_trigger ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT has_trigger( + $1, $2, $3, + 'Table ' || quote_ident($1) || '.' || quote_ident($2) || ' should have trigger ' || quote_ident($3) + ); +$$ LANGUAGE sql; + +-- has_trigger( table, trigger, description ) +CREATE OR REPLACE FUNCTION has_trigger ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _trig($1, $2), $3); +$$ LANGUAGE sql; + +-- has_trigger( table, trigger ) +CREATE OR REPLACE FUNCTION has_trigger ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _trig($1, $2), 'Table ' || quote_ident($1) || ' should have trigger ' || quote_ident($2)); +$$ LANGUAGE SQL; + +-- hasnt_trigger( schema, table, trigger, description ) +CREATE OR REPLACE FUNCTION hasnt_trigger ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _trig($1, $2, $3), $4); +$$ LANGUAGE SQL; + +-- hasnt_trigger( schema, table, trigger ) +CREATE OR REPLACE FUNCTION hasnt_trigger ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _trig($1, $2, $3), + 'Table ' || quote_ident($1) || '.' || quote_ident($2) || ' should not have trigger ' || quote_ident($3) + ); +$$ LANGUAGE sql; + +-- hasnt_trigger( table, trigger, description ) +CREATE OR REPLACE FUNCTION hasnt_trigger ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _trig($1, $2), $3); +$$ LANGUAGE sql; + +-- hasnt_trigger( table, trigger ) +CREATE OR REPLACE FUNCTION hasnt_trigger ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _trig($1, $2), 'Table ' || quote_ident($1) || ' should not have trigger ' || quote_ident($2)); +$$ LANGUAGE SQL; + +-- trigger_is( schema, table, trigger, schema, function, description ) +CREATE OR REPLACE FUNCTION trigger_is ( NAME, NAME, NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + pname text; +BEGIN + SELECT quote_ident(ni.nspname) || '.' || quote_ident(p.proname) + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class ct ON ct.oid = t.tgrelid + JOIN pg_catalog.pg_namespace nt ON nt.oid = ct.relnamespace + JOIN pg_catalog.pg_proc p ON p.oid = t.tgfoid + JOIN pg_catalog.pg_namespace ni ON ni.oid = p.pronamespace + WHERE nt.nspname = $1 + AND ct.relname = $2 + AND t.tgname = $3 + INTO pname; + + RETURN is( pname, quote_ident($4) || '.' || quote_ident($5), $6 ); +END; +$$ LANGUAGE plpgsql; + +-- trigger_is( schema, table, trigger, schema, function ) +CREATE OR REPLACE FUNCTION trigger_is ( NAME, NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT trigger_is( + $1, $2, $3, $4, $5, + 'Trigger ' || quote_ident($3) || ' should call ' || quote_ident($4) || '.' || quote_ident($5) || '()' + ); +$$ LANGUAGE sql; + +-- trigger_is( table, trigger, function, description ) +CREATE OR REPLACE FUNCTION trigger_is ( NAME, NAME, NAME, text ) +RETURNS TEXT AS $$ +DECLARE + pname text; +BEGIN + SELECT p.proname + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class ct ON ct.oid = t.tgrelid + JOIN pg_catalog.pg_proc p ON p.oid = t.tgfoid + WHERE ct.relname = $1 + AND t.tgname = $2 + AND pg_catalog.pg_table_is_visible(ct.oid) + INTO pname; + + RETURN is( pname, $3::text, $4 ); +END; +$$ LANGUAGE plpgsql; + +-- trigger_is( table, trigger, function ) +CREATE OR REPLACE FUNCTION trigger_is ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT trigger_is( + $1, $2, $3, + 'Trigger ' || quote_ident($2) || ' should call ' || quote_ident($3) || '()' + ); +$$ LANGUAGE sql; + +-- has_schema( schema, description ) +CREATE OR REPLACE FUNCTION has_schema( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( + EXISTS( + SELECT true + FROM pg_catalog.pg_namespace + WHERE nspname = $1 + ), $2 + ); +$$ LANGUAGE sql; + +-- has_schema( schema ) +CREATE OR REPLACE FUNCTION has_schema( NAME ) +RETURNS TEXT AS $$ + SELECT has_schema( $1, 'Schema ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE sql; + +-- hasnt_schema( schema, description ) +CREATE OR REPLACE FUNCTION hasnt_schema( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( + NOT EXISTS( + SELECT true + FROM pg_catalog.pg_namespace + WHERE nspname = $1 + ), $2 + ); +$$ LANGUAGE sql; + +-- hasnt_schema( schema ) +CREATE OR REPLACE FUNCTION hasnt_schema( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_schema( $1, 'Schema ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE sql; + +-- has_tablespace( tablespace, location, description ) +CREATE OR REPLACE FUNCTION has_tablespace( NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( + EXISTS( + SELECT true + FROM pg_catalog.pg_tablespace + WHERE spcname = $1 + AND spclocation = $2 + ), $3 + ); +$$ LANGUAGE sql; + +-- has_tablespace( tablespace, description ) +CREATE OR REPLACE FUNCTION has_tablespace( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( + EXISTS( + SELECT true + FROM pg_catalog.pg_tablespace + WHERE spcname = $1 + ), $2 + ); +$$ LANGUAGE sql; + +-- has_tablespace( tablespace ) +CREATE OR REPLACE FUNCTION has_tablespace( NAME ) +RETURNS TEXT AS $$ + SELECT has_tablespace( $1, 'Tablespace ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE sql; + +-- hasnt_tablespace( tablespace, description ) +CREATE OR REPLACE FUNCTION hasnt_tablespace( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( + NOT EXISTS( + SELECT true + FROM pg_catalog.pg_tablespace + WHERE spcname = $1 + ), $2 + ); +$$ LANGUAGE sql; + +-- hasnt_tablespace( tablespace ) +CREATE OR REPLACE FUNCTION hasnt_tablespace( NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_tablespace( $1, 'Tablespace ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _has_type( NAME, NAME, CHAR[] ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid + WHERE t.typisdefined + AND n.nspname = $1 + AND t.typname = $2 + AND t.typtype = ANY( COALESCE($3, ARRAY['b', 'c', 'd', 'p', 'e']) ) + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _has_type( NAME, CHAR[] ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_type t + WHERE t.typisdefined + AND pg_catalog.pg_type_is_visible(t.oid) + AND t.typname = $1 + AND t.typtype = ANY( COALESCE($2, ARRAY['b', 'c', 'd', 'p', 'e']) ) + ); +$$ LANGUAGE sql; + +-- has_type( schema, type, description ) +CREATE OR REPLACE FUNCTION has_type( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, $2, NULL ), $3 ); +$$ LANGUAGE sql; + +-- has_type( schema, type ) +CREATE OR REPLACE FUNCTION has_type( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT has_type( $1, $2, 'Type ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE sql; + +-- has_type( type, description ) +CREATE OR REPLACE FUNCTION has_type( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, NULL ), $2 ); +$$ LANGUAGE sql; + +-- has_type( type ) +CREATE OR REPLACE FUNCTION has_type( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, NULL ), ('Type ' || quote_ident($1) || ' should exist')::text ); +$$ LANGUAGE sql; + +-- hasnt_type( schema, type, description ) +CREATE OR REPLACE FUNCTION hasnt_type( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, $2, NULL ), $3 ); +$$ LANGUAGE sql; + +-- hasnt_type( schema, type ) +CREATE OR REPLACE FUNCTION hasnt_type( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_type( $1, $2, 'Type ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); +$$ LANGUAGE sql; + +-- hasnt_type( type, description ) +CREATE OR REPLACE FUNCTION hasnt_type( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, NULL ), $2 ); +$$ LANGUAGE sql; + +-- hasnt_type( type ) +CREATE OR REPLACE FUNCTION hasnt_type( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, NULL ), ('Type ' || quote_ident($1) || ' should not exist')::text ); +$$ LANGUAGE sql; + +-- has_domain( schema, domain, description ) +CREATE OR REPLACE FUNCTION has_domain( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, $2, ARRAY['d'] ), $3 ); +$$ LANGUAGE sql; + +-- has_domain( schema, domain ) +CREATE OR REPLACE FUNCTION has_domain( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT has_domain( $1, $2, 'Domain ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE sql; + +-- has_domain( domain, description ) +CREATE OR REPLACE FUNCTION has_domain( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, ARRAY['d'] ), $2 ); +$$ LANGUAGE sql; + +-- has_domain( domain ) +CREATE OR REPLACE FUNCTION has_domain( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, ARRAY['d'] ), ('Domain ' || quote_ident($1) || ' should exist')::text ); +$$ LANGUAGE sql; + +-- hasnt_domain( schema, domain, description ) +CREATE OR REPLACE FUNCTION hasnt_domain( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, $2, ARRAY['d'] ), $3 ); +$$ LANGUAGE sql; + +-- hasnt_domain( schema, domain ) +CREATE OR REPLACE FUNCTION hasnt_domain( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_domain( $1, $2, 'Domain ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); +$$ LANGUAGE sql; + +-- hasnt_domain( domain, description ) +CREATE OR REPLACE FUNCTION hasnt_domain( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, ARRAY['d'] ), $2 ); +$$ LANGUAGE sql; + +-- hasnt_domain( domain ) +CREATE OR REPLACE FUNCTION hasnt_domain( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, ARRAY['d'] ), ('Domain ' || quote_ident($1) || ' should not exist')::text ); +$$ LANGUAGE sql; + +-- has_enum( schema, enum, description ) +CREATE OR REPLACE FUNCTION has_enum( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, $2, ARRAY['e'] ), $3 ); +$$ LANGUAGE sql; + +-- has_enum( schema, enum ) +CREATE OR REPLACE FUNCTION has_enum( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT has_enum( $1, $2, 'Enum ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE sql; + +-- has_enum( enum, description ) +CREATE OR REPLACE FUNCTION has_enum( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, ARRAY['e'] ), $2 ); +$$ LANGUAGE sql; + +-- has_enum( enum ) +CREATE OR REPLACE FUNCTION has_enum( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _has_type( $1, ARRAY['e'] ), ('Enum ' || quote_ident($1) || ' should exist')::text ); +$$ LANGUAGE sql; + +-- hasnt_enum( schema, enum, description ) +CREATE OR REPLACE FUNCTION hasnt_enum( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, $2, ARRAY['e'] ), $3 ); +$$ LANGUAGE sql; + +-- hasnt_enum( schema, enum ) +CREATE OR REPLACE FUNCTION hasnt_enum( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT hasnt_enum( $1, $2, 'Enum ' || quote_ident($1) || '.' || quote_ident($2) || ' should not exist' ); +$$ LANGUAGE sql; + +-- hasnt_enum( enum, description ) +CREATE OR REPLACE FUNCTION hasnt_enum( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, ARRAY['e'] ), $2 ); +$$ LANGUAGE sql; + +-- hasnt_enum( enum ) +CREATE OR REPLACE FUNCTION hasnt_enum( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_type( $1, ARRAY['e'] ), ('Enum ' || quote_ident($1) || ' should not exist')::text ); +$$ LANGUAGE sql; + +-- enum_has_labels( schema, enum, labels, description ) +CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT is( + ARRAY( + SELECT e.enumlabel + FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid + JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid + WHERE t.typisdefined + AND n.nspname = $1 + AND t.typname = $2 + AND t.typtype = 'e' + ORDER BY e.oid + ), + $3, + $4 + ); +$$ LANGUAGE sql; + +-- enum_has_labels( schema, enum, labels ) +CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT enum_has_labels( + $1, $2, $3, + 'Enum ' || quote_ident($1) || '.' || quote_ident($2) || ' should have labels (' || array_to_string( $3, ', ' ) || ')' + ); +$$ LANGUAGE sql; + +-- enum_has_labels( enum, labels, description ) +CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT is( + ARRAY( + SELECT e.enumlabel + FROM pg_catalog.pg_type t + JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid + WHERE t.typisdefined + AND pg_catalog.pg_type_is_visible(t.oid) + AND t.typname = $1 + AND t.typtype = 'e' + ORDER BY e.oid + ), + $2, + $3 + ); +$$ LANGUAGE sql; + +-- enum_has_labels( enum, labels ) +CREATE OR REPLACE FUNCTION enum_has_labels( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT enum_has_labels( + $1, $2, + 'Enum ' || quote_ident($1) || ' should have labels (' || array_to_string( $2, ', ' ) || ')' + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _has_role( NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_roles + WHERE rolname = $1 + ); +$$ LANGUAGE sql STRICT; + +-- has_role( role, description ) +CREATE OR REPLACE FUNCTION has_role( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_role($1), $2 ); +$$ LANGUAGE sql; + +-- has_role( role ) +CREATE OR REPLACE FUNCTION has_role( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _has_role($1), 'Role ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE sql; + +-- hasnt_role( role, description ) +CREATE OR REPLACE FUNCTION hasnt_role( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_role($1), $2 ); +$$ LANGUAGE sql; + +-- hasnt_role( role ) +CREATE OR REPLACE FUNCTION hasnt_role( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_role($1), 'Role ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _has_user( NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( SELECT true FROM pg_catalog.pg_user WHERE usename = $1); +$$ LANGUAGE sql STRICT; + +-- has_user( user, description ) +CREATE OR REPLACE FUNCTION has_user( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_user($1), $2 ); +$$ LANGUAGE sql; + +-- has_user( user ) +CREATE OR REPLACE FUNCTION has_user( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _has_user( $1 ), 'User ' || quote_ident($1) || ' should exist'); +$$ LANGUAGE sql; + +-- hasnt_user( user, description ) +CREATE OR REPLACE FUNCTION hasnt_user( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_user($1), $2 ); +$$ LANGUAGE sql; + +-- hasnt_user( user ) +CREATE OR REPLACE FUNCTION hasnt_user( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_user( $1 ), 'User ' || quote_ident($1) || ' should not exist'); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _is_super( NAME ) +RETURNS BOOLEAN AS $$ + SELECT rolsuper + FROM pg_catalog.pg_roles + WHERE rolname = $1 +$$ LANGUAGE sql STRICT; + +-- is_superuser( user, description ) +CREATE OR REPLACE FUNCTION is_superuser( NAME, TEXT ) +RETURNS TEXT AS $$ +DECLARE + is_super boolean := _is_super($1); +BEGIN + IF is_super IS NULL THEN + RETURN fail( $2 ) || E'\n' || diag( ' User ' || quote_ident($1) || ' does not exist') ; + END IF; + RETURN ok( is_super, $2 ); +END; +$$ LANGUAGE plpgsql; + +-- is_superuser( user ) +CREATE OR REPLACE FUNCTION is_superuser( NAME ) +RETURNS TEXT AS $$ + SELECT is_superuser( $1, 'User ' || quote_ident($1) || ' should be a super user' ); +$$ LANGUAGE sql; + +-- isnt_superuser( user, description ) +CREATE OR REPLACE FUNCTION isnt_superuser( NAME, TEXT ) +RETURNS TEXT AS $$ +DECLARE + is_super boolean := _is_super($1); +BEGIN + IF is_super IS NULL THEN + RETURN fail( $2 ) || E'\n' || diag( ' User ' || quote_ident($1) || ' does not exist') ; + END IF; + RETURN ok( NOT is_super, $2 ); +END; +$$ LANGUAGE plpgsql; + +-- isnt_superuser( user ) +CREATE OR REPLACE FUNCTION isnt_superuser( NAME ) +RETURNS TEXT AS $$ + SELECT isnt_superuser( $1, 'User ' || quote_ident($1) || ' should not be a super user' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _has_group( NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_group + WHERE groname = $1 + ); +$$ LANGUAGE sql STRICT; + +-- has_group( group, description ) +CREATE OR REPLACE FUNCTION has_group( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _has_group($1), $2 ); +$$ LANGUAGE sql; + +-- has_group( group ) +CREATE OR REPLACE FUNCTION has_group( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _has_group($1), 'Group ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE sql; + +-- hasnt_group( group, description ) +CREATE OR REPLACE FUNCTION hasnt_group( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_group($1), $2 ); +$$ LANGUAGE sql; + +-- hasnt_group( group ) +CREATE OR REPLACE FUNCTION hasnt_group( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _has_group($1), 'Group ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _grolist ( NAME ) +RETURNS oid[] AS $$ + SELECT ARRAY( + SELECT member + FROM pg_catalog.pg_auth_members m + JOIN pg_catalog.pg_roles r ON m.roleid = r.oid + WHERE r.rolname = $1 + ); +$$ LANGUAGE sql; + +-- is_member_of( group, user[], description ) +CREATE OR REPLACE FUNCTION is_member_of( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + missing text[]; +BEGIN + IF NOT _has_role($1) THEN + RETURN fail( $3 ) || E'\n' || diag ( + ' Role ' || quote_ident($1) || ' does not exist' + ); + END IF; + + SELECT ARRAY( + SELECT quote_ident($2[i]) + FROM generate_series(1, array_upper($2, 1)) s(i) + LEFT JOIN pg_catalog.pg_user ON usename = $2[i] + WHERE usesysid IS NULL + OR NOT usesysid = ANY ( _grolist($1) ) + ORDER BY s.i + ) INTO missing; + IF missing[1] IS NULL THEN + RETURN ok( true, $3 ); + END IF; + RETURN ok( false, $3 ) || E'\n' || diag( + ' Users missing from the ' || quote_ident($1) || E' group:\n ' || + array_to_string( missing, E'\n ') + ); +END; +$$ LANGUAGE plpgsql; + +-- is_member_of( group, user, description ) +CREATE OR REPLACE FUNCTION is_member_of( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT is_member_of( $1, ARRAY[$2], $3 ); +$$ LANGUAGE SQL; + +-- is_member_of( group, user[] ) +CREATE OR REPLACE FUNCTION is_member_of( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT is_member_of( $1, $2, 'Should have members of group ' || quote_ident($1) ); +$$ LANGUAGE SQL; + +-- is_member_of( group, user ) +CREATE OR REPLACE FUNCTION is_member_of( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT is_member_of( $1, ARRAY[$2] ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _cmp_types(oid, name) +RETURNS BOOLEAN AS $$ +DECLARE + dtype TEXT := display_type($1, NULL); +BEGIN + RETURN dtype = _quote_ident_like($2, dtype); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _cast_exists ( NAME, NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_cast c + JOIN pg_catalog.pg_proc p ON c.castfunc = p.oid + JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid + WHERE _cmp_types(castsource, $1) + AND _cmp_types(casttarget, $2) + AND n.nspname = $3 + AND p.proname = $4 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _cast_exists ( NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_cast c + JOIN pg_catalog.pg_proc p ON c.castfunc = p.oid + WHERE _cmp_types(castsource, $1) + AND _cmp_types(casttarget, $2) + AND p.proname = $3 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _cast_exists ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_cast c + WHERE _cmp_types(castsource, $1) + AND _cmp_types(casttarget, $2) + ); +$$ LANGUAGE SQL; + +-- has_cast( source_type, target_type, schema, function, description ) +CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _cast_exists( $1, $2, $3, $4 ), $5 ); +$$ LANGUAGE SQL; + +-- has_cast( source_type, target_type, schema, function ) +CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _cast_exists( $1, $2, $3, $4 ), + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') WITH FUNCTION ' || quote_ident($3) + || '.' || quote_ident($4) || '() should exist' + ); +$$ LANGUAGE SQL; + +-- has_cast( source_type, target_type, function, description ) +CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _cast_exists( $1, $2, $3 ), $4 ); +$$ LANGUAGE SQL; + +-- has_cast( source_type, target_type, function ) +CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _cast_exists( $1, $2, $3 ), + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') WITH FUNCTION ' || quote_ident($3) || '() should exist' + ); +$$ LANGUAGE SQL; + +-- has_cast( source_type, target_type, description ) +CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _cast_exists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- has_cast( source_type, target_type ) +CREATE OR REPLACE FUNCTION has_cast ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _cast_exists( $1, $2 ), + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') should exist' + ); +$$ LANGUAGE SQL; + +-- hasnt_cast( source_type, target_type, schema, function, description ) +CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _cast_exists( $1, $2, $3, $4 ), $5 ); +$$ LANGUAGE SQL; + +-- hasnt_cast( source_type, target_type, schema, function ) +CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _cast_exists( $1, $2, $3, $4 ), + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') WITH FUNCTION ' || quote_ident($3) + || '.' || quote_ident($4) || '() should not exist' + ); +$$ LANGUAGE SQL; + +-- hasnt_cast( source_type, target_type, function, description ) +CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _cast_exists( $1, $2, $3 ), $4 ); +$$ LANGUAGE SQL; + +-- hasnt_cast( source_type, target_type, function ) +CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _cast_exists( $1, $2, $3 ), + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') WITH FUNCTION ' || quote_ident($3) || '() should not exist' + ); +$$ LANGUAGE SQL; + +-- hasnt_cast( source_type, target_type, description ) +CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _cast_exists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_cast( source_type, target_type ) +CREATE OR REPLACE FUNCTION hasnt_cast ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + NOT _cast_exists( $1, $2 ), + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') should not exist' + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _expand_context( char ) +RETURNS text AS $$ + SELECT CASE $1 + WHEN 'i' THEN 'implicit' + WHEN 'a' THEN 'assignment' + WHEN 'e' THEN 'explicit' + ELSE 'unknown' END +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION _get_context( NAME, NAME ) +RETURNS "char" AS $$ + SELECT c.castcontext + FROM pg_catalog.pg_cast c + WHERE _cmp_types(castsource, $1) + AND _cmp_types(casttarget, $2) +$$ LANGUAGE SQL; + +-- cast_context_is( source_type, target_type, context, description ) +CREATE OR REPLACE FUNCTION cast_context_is( NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want char = substring(LOWER($3) FROM 1 FOR 1); + have char := _get_context($1, $2); +BEGIN + IF have IS NOT NULL THEN + RETURN is( _expand_context(have), _expand_context(want), $4 ); + END IF; + + RETURN ok( false, $4 ) || E'\n' || diag( + ' Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') does not exist' + ); +END; +$$ LANGUAGE plpgsql; + +-- cast_context_is( source_type, target_type, context ) +CREATE OR REPLACE FUNCTION cast_context_is( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT cast_context_is( + $1, $2, $3, + 'Cast (' || quote_ident($1) || ' AS ' || quote_ident($2) + || ') context should be ' || _expand_context(substring(LOWER($3) FROM 1 FOR 1)) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _op_exists ( NAME, NAME, NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_namespace n ON o.oprnamespace = n.oid + WHERE n.nspname = $2 + AND o.oprname = $3 + AND CASE o.oprkind WHEN 'l' THEN $1 IS NULL + ELSE _cmp_types(o.oprleft, $1) END + AND CASE o.oprkind WHEN 'r' THEN $4 IS NULL + ELSE _cmp_types(o.oprright, $4) END + AND _cmp_types(o.oprresult, $5) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _op_exists ( NAME, NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_operator o + WHERE pg_catalog.pg_operator_is_visible(o.oid) + AND o.oprname = $2 + AND CASE o.oprkind WHEN 'l' THEN $1 IS NULL + ELSE _cmp_types(o.oprleft, $1) END + AND CASE o.oprkind WHEN 'r' THEN $3 IS NULL + ELSE _cmp_types(o.oprright, $3) END + AND _cmp_types(o.oprresult, $4) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _op_exists ( NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_operator o + WHERE pg_catalog.pg_operator_is_visible(o.oid) + AND o.oprname = $2 + AND CASE o.oprkind WHEN 'l' THEN $1 IS NULL + ELSE _cmp_types(o.oprleft, $1) END + AND CASE o.oprkind WHEN 'r' THEN $3 IS NULL + ELSE _cmp_types(o.oprright, $3) END + ); +$$ LANGUAGE SQL; + +-- has_operator( left_type, schema, name, right_type, return_type, description ) +CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists($1, $2, $3, $4, $5 ), $6 ); +$$ LANGUAGE SQL; + +-- has_operator( left_type, schema, name, right_type, return_type ) +CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists($1, $2, $3, $4, $5 ), + 'Operator ' || quote_ident($2) || '.' || $3 || '(' || $1 || ',' || $4 + || ') RETURNS ' || $5 || ' should exist' + ); +$$ LANGUAGE SQL; + +-- has_operator( left_type, name, right_type, return_type, description ) +CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists($1, $2, $3, $4 ), $5 ); +$$ LANGUAGE SQL; + +-- has_operator( left_type, name, right_type, return_type ) +CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists($1, $2, $3, $4 ), + 'Operator ' || $2 || '(' || $1 || ',' || $3 + || ') RETURNS ' || $4 || ' should exist' + ); +$$ LANGUAGE SQL; + +-- has_operator( left_type, name, right_type, description ) +CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists($1, $2, $3 ), $4 ); +$$ LANGUAGE SQL; + +-- has_operator( left_type, name, right_type ) +CREATE OR REPLACE FUNCTION has_operator ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists($1, $2, $3 ), + 'Operator ' || $2 || '(' || $1 || ',' || $3 + || ') should exist' + ); +$$ LANGUAGE SQL; + +-- has_leftop( schema, name, right_type, return_type, description ) +CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists(NULL, $1, $2, $3, $4), $5 ); +$$ LANGUAGE SQL; + +-- has_leftop( schema, name, right_type, return_type ) +CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists(NULL, $1, $2, $3, $4 ), + 'Left operator ' || quote_ident($1) || '.' || $2 || '(NONE,' + || $3 || ') RETURNS ' || $4 || ' should exist' + ); +$$ LANGUAGE SQL; + +-- has_leftop( name, right_type, return_type, description ) +CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists(NULL, $1, $2, $3), $4 ); +$$ LANGUAGE SQL; + +-- has_leftop( name, right_type, return_type ) +CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists(NULL, $1, $2, $3 ), + 'Left operator ' || $1 || '(NONE,' || $2 || ') RETURNS ' || $3 || ' should exist' + ); +$$ LANGUAGE SQL; + +-- has_leftop( name, right_type, description ) +CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists(NULL, $1, $2), $3 ); +$$ LANGUAGE SQL; + +-- has_leftop( name, right_type ) +CREATE OR REPLACE FUNCTION has_leftop ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists(NULL, $1, $2 ), + 'Left operator ' || $1 || '(NONE,' || $2 || ') should exist' + ); +$$ LANGUAGE SQL; + +-- has_rightop( left_type, schema, name, return_type, description ) +CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists( $1, $2, $3, NULL, $4), $5 ); +$$ LANGUAGE SQL; + +-- has_rightop( left_type, schema, name, return_type ) +CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists($1, $2, $3, NULL, $4 ), + 'Right operator ' || quote_ident($2) || '.' || $3 || '(' + || $1 || ',NONE) RETURNS ' || $4 || ' should exist' + ); +$$ LANGUAGE SQL; + +-- has_rightop( left_type, name, return_type, description ) +CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists( $1, $2, NULL, $3), $4 ); +$$ LANGUAGE SQL; + +-- has_rightop( left_type, name, return_type ) +CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists($1, $2, NULL, $3 ), + 'Right operator ' || $2 || '(' + || $1 || ',NONE) RETURNS ' || $3 || ' should exist' + ); +$$ LANGUAGE SQL; + +-- has_rightop( left_type, name, description ) +CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _op_exists( $1, $2, NULL), $3 ); +$$ LANGUAGE SQL; + +-- has_rightop( left_type, name ) +CREATE OR REPLACE FUNCTION has_rightop ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _op_exists($1, $2, NULL ), + 'Right operator ' || $2 || '(' || $1 || ',NONE) should exist' + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _are ( text, name[], name[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + what ALIAS FOR $1; + extras ALIAS FOR $2; + missing ALIAS FOR $3; + descr ALIAS FOR $4; + msg TEXT := ''; + res BOOLEAN := TRUE; +BEGIN + IF extras[1] IS NOT NULL THEN + res = FALSE; + msg := E'\n' || diag( + ' Extra ' || what || E':\n ' + || _ident_array_to_string( extras, E'\n ' ) + ); + END IF; + IF missing[1] IS NOT NULL THEN + res = FALSE; + msg := msg || E'\n' || diag( + ' Missing ' || what || E':\n ' + || _ident_array_to_string( missing, E'\n ' ) + ); + END IF; + + RETURN ok(res, descr) || msg; +END; +$$ LANGUAGE plpgsql; + +-- tablespaces_are( tablespaces, description ) +CREATE OR REPLACE FUNCTION tablespaces_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'tablespaces', + ARRAY( + SELECT spcname + FROM pg_catalog.pg_tablespace + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT spcname + FROM pg_catalog.pg_tablespace + ), + $2 + ); +$$ LANGUAGE SQL; + +-- tablespaces_are( tablespaces ) +CREATE OR REPLACE FUNCTION tablespaces_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT tablespaces_are( $1, 'There should be the correct tablespaces' ); +$$ LANGUAGE SQL; + +-- schemas_are( schemas, description ) +CREATE OR REPLACE FUNCTION schemas_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'schemas', + ARRAY( + SELECT nspname + FROM pg_catalog.pg_namespace + WHERE nspname NOT LIKE 'pg_%' + AND nspname <> 'information_schema' + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT nspname + FROM pg_catalog.pg_namespace + WHERE nspname NOT LIKE 'pg_%' + AND nspname <> 'information_schema' + ), + $2 + ); +$$ LANGUAGE SQL; + +-- schemas_are( schemas ) +CREATE OR REPLACE FUNCTION schemas_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT schemas_are( $1, 'There should be the correct schemas' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _extras ( CHAR, NAME, NAME[] ) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT c.relname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + WHERE c.relkind = $1 + AND n.nspname = $2 + AND c.relname NOT IN('pg_all_foreign_keys', 'tap_funky', '__tresults___numb_seq', '__tcache___id_seq') + EXCEPT + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _extras ( CHAR, NAME[] ) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT c.relname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + WHERE pg_catalog.pg_table_is_visible(c.oid) + AND n.nspname <> 'pg_catalog' + AND c.relkind = $1 + AND c.relname NOT IN ('__tcache__', '__tresults__', 'pg_all_foreign_keys', 'tap_funky', '__tresults___numb_seq', '__tcache___id_seq') + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _missing ( CHAR, NAME, NAME[] ) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + EXCEPT + SELECT c.relname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + WHERE c.relkind = $1 + AND n.nspname = $2 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _missing ( CHAR, NAME[] ) +RETURNS NAME[] AS $$ + SELECT ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT c.relname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + WHERE pg_catalog.pg_table_is_visible(c.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND c.relkind = $1 + ); +$$ LANGUAGE SQL; + +-- tables_are( schema, tables, description ) +CREATE OR REPLACE FUNCTION tables_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( 'tables', _extras('r', $1, $2), _missing('r', $1, $2), $3); +$$ LANGUAGE SQL; + +-- tables_are( tables, description ) +CREATE OR REPLACE FUNCTION tables_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( 'tables', _extras('r', $1), _missing('r', $1), $2); +$$ LANGUAGE SQL; + +-- tables_are( schema, tables ) +CREATE OR REPLACE FUNCTION tables_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'tables', _extras('r', $1, $2), _missing('r', $1, $2), + 'Schema ' || quote_ident($1) || ' should have the correct tables' + ); +$$ LANGUAGE SQL; + +-- tables_are( tables ) +CREATE OR REPLACE FUNCTION tables_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'tables', _extras('r', $1), _missing('r', $1), + 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct tables' + ); +$$ LANGUAGE SQL; + +-- views_are( schema, views, description ) +CREATE OR REPLACE FUNCTION views_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( 'views', _extras('v', $1, $2), _missing('v', $1, $2), $3); +$$ LANGUAGE SQL; + +-- views_are( views, description ) +CREATE OR REPLACE FUNCTION views_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( 'views', _extras('v', $1), _missing('v', $1), $2); +$$ LANGUAGE SQL; + +-- views_are( schema, views ) +CREATE OR REPLACE FUNCTION views_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'views', _extras('v', $1, $2), _missing('v', $1, $2), + 'Schema ' || quote_ident($1) || ' should have the correct views' + ); +$$ LANGUAGE SQL; + +-- views_are( views ) +CREATE OR REPLACE FUNCTION views_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'views', _extras('v', $1), _missing('v', $1), + 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct views' + ); +$$ LANGUAGE SQL; + +-- sequences_are( schema, sequences, description ) +CREATE OR REPLACE FUNCTION sequences_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( 'sequences', _extras('S', $1, $2), _missing('S', $1, $2), $3); +$$ LANGUAGE SQL; + +-- sequences_are( sequences, description ) +CREATE OR REPLACE FUNCTION sequences_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( 'sequences', _extras('S', $1), _missing('S', $1), $2); +$$ LANGUAGE SQL; + +-- sequences_are( schema, sequences ) +CREATE OR REPLACE FUNCTION sequences_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'sequences', _extras('S', $1, $2), _missing('S', $1, $2), + 'Schema ' || quote_ident($1) || ' should have the correct sequences' + ); +$$ LANGUAGE SQL; + +-- sequences_are( sequences ) +CREATE OR REPLACE FUNCTION sequences_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'sequences', _extras('S', $1), _missing('S', $1), + 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct sequences' + ); +$$ LANGUAGE SQL; + +-- functions_are( schema, functions[], description ) +CREATE OR REPLACE FUNCTION functions_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'functions', + ARRAY( + SELECT name FROM tap_funky WHERE schema = $1 + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT name FROM tap_funky WHERE schema = $1 + ), + $3 + ); +$$ LANGUAGE SQL; + +-- functions_are( schema, functions[] ) +CREATE OR REPLACE FUNCTION functions_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT functions_are( $1, $2, 'Schema ' || quote_ident($1) || ' should have the correct functions' ); +$$ LANGUAGE SQL; + +-- functions_are( functions[], description ) +CREATE OR REPLACE FUNCTION functions_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'functions', + ARRAY( + SELECT name FROM tap_funky WHERE is_visible + AND schema NOT IN ('pg_catalog', 'information_schema') + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT name FROM tap_funky WHERE is_visible + AND schema NOT IN ('pg_catalog', 'information_schema') + ), + $2 + ); +$$ LANGUAGE SQL; + +-- functions_are( functions[] ) +CREATE OR REPLACE FUNCTION functions_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT functions_are( $1, 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct functions' ); +$$ LANGUAGE SQL; + +-- indexes_are( schema, table, indexes[], description ) +CREATE OR REPLACE FUNCTION indexes_are( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'indexes', + ARRAY( + SELECT ci.relname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND n.nspname = $1 + EXCEPT + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + ), + ARRAY( + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + EXCEPT + SELECT ci.relname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $2 + AND n.nspname = $1 + ), + $4 + ); +$$ LANGUAGE SQL; + +-- indexes_are( schema, table, indexes[] ) +CREATE OR REPLACE FUNCTION indexes_are( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT indexes_are( $1, $2, $3, 'Table ' || quote_ident($1) || '.' || quote_ident($2) || ' should have the correct indexes' ); +$$ LANGUAGE SQL; + +-- indexes_are( table, indexes[], description ) +CREATE OR REPLACE FUNCTION indexes_are( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'indexes', + ARRAY( + SELECT ci.relname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $1 + AND pg_catalog.pg_table_is_visible(ct.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT ci.relname + FROM pg_catalog.pg_index x + JOIN pg_catalog.pg_class ct ON ct.oid = x.indrelid + JOIN pg_catalog.pg_class ci ON ci.oid = x.indexrelid + JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace + WHERE ct.relname = $1 + AND pg_catalog.pg_table_is_visible(ct.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + ), + $3 + ); +$$ LANGUAGE SQL; + +-- indexes_are( table, indexes[] ) +CREATE OR REPLACE FUNCTION indexes_are( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT indexes_are( $1, $2, 'Table ' || quote_ident($1) || ' should have the correct indexes' ); +$$ LANGUAGE SQL; + +-- users_are( users[], description ) +CREATE OR REPLACE FUNCTION users_are( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'users', + ARRAY( + SELECT usename + FROM pg_catalog.pg_user + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT usename + FROM pg_catalog.pg_user + ), + $2 + ); +$$ LANGUAGE SQL; + +-- users_are( users[] ) +CREATE OR REPLACE FUNCTION users_are( NAME[] ) +RETURNS TEXT AS $$ + SELECT users_are( $1, 'There should be the correct users' ); +$$ LANGUAGE SQL; + +-- groups_are( groups[], description ) +CREATE OR REPLACE FUNCTION groups_are( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'groups', + ARRAY( + SELECT groname + FROM pg_catalog.pg_group + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT groname + FROM pg_catalog.pg_group + ), + $2 + ); +$$ LANGUAGE SQL; + +-- groups_are( groups[] ) +CREATE OR REPLACE FUNCTION groups_are( NAME[] ) +RETURNS TEXT AS $$ + SELECT groups_are( $1, 'There should be the correct groups' ); +$$ LANGUAGE SQL; + +-- languages_are( languages[], description ) +CREATE OR REPLACE FUNCTION languages_are( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'languages', + ARRAY( + SELECT lanname + FROM pg_catalog.pg_language + WHERE lanispl + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT lanname + FROM pg_catalog.pg_language + WHERE lanispl + ), + $2 + ); +$$ LANGUAGE SQL; + +-- languages_are( languages[] ) +CREATE OR REPLACE FUNCTION languages_are( NAME[] ) +RETURNS TEXT AS $$ + SELECT languages_are( $1, 'There should be the correct procedural languages' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _is_trusted( NAME ) +RETURNS BOOLEAN AS $$ + SELECT lanpltrusted FROM pg_catalog.pg_language WHERE lanname = $1; +$$ LANGUAGE SQL; + +-- has_language( language, description) +CREATE OR REPLACE FUNCTION has_language( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _is_trusted($1) IS NOT NULL, $2 ); +$$ LANGUAGE SQL; + +-- has_language( language ) +CREATE OR REPLACE FUNCTION has_language( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _is_trusted($1) IS NOT NULL, 'Procedural language ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_language( language, description) +CREATE OR REPLACE FUNCTION hasnt_language( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _is_trusted($1) IS NULL, $2 ); +$$ LANGUAGE SQL; + +-- hasnt_language( language ) +CREATE OR REPLACE FUNCTION hasnt_language( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _is_trusted($1) IS NULL, 'Procedural language ' || quote_ident($1) || ' should not exist' ); +$$ LANGUAGE SQL; + +-- language_is_trusted( language, description ) +CREATE OR REPLACE FUNCTION language_is_trusted( NAME, TEXT ) +RETURNS TEXT AS $$ +DECLARE + is_trusted boolean := _is_trusted($1); +BEGIN + IF is_trusted IS NULL THEN + RETURN fail( $2 ) || E'\n' || diag( ' Procedural language ' || quote_ident($1) || ' does not exist') ; + END IF; + RETURN ok( is_trusted, $2 ); +END; +$$ LANGUAGE plpgsql; + +-- language_is_trusted( language ) +CREATE OR REPLACE FUNCTION language_is_trusted( NAME ) +RETURNS TEXT AS $$ + SELECT language_is_trusted($1, 'Procedural language ' || quote_ident($1) || ' should be trusted' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _opc_exists( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS ( + SELECT TRUE + FROM pg_catalog.pg_opclass oc + JOIN pg_catalog.pg_namespace n ON oc.opcnamespace = n.oid + WHERE n.nspname = COALESCE($1, n.nspname) + AND oc.opcname = $2 + ); +$$ LANGUAGE SQL; + +-- has_opclass( schema, name, description ) +CREATE OR REPLACE FUNCTION has_opclass( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _opc_exists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- has_opclass( schema, name ) +CREATE OR REPLACE FUNCTION has_opclass( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _opc_exists( $1, $2 ), 'Operator class ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE SQL; + +-- has_opclass( name, description ) +CREATE OR REPLACE FUNCTION has_opclass( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _opc_exists( NULL, $1 ), $2) +$$ LANGUAGE SQL; + +-- has_opclass( name ) +CREATE OR REPLACE FUNCTION has_opclass( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _opc_exists( NULL, $1 ), 'Operator class ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_opclass( schema, name, description ) +CREATE OR REPLACE FUNCTION hasnt_opclass( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _opc_exists( $1, $2 ), $3 ); +$$ LANGUAGE SQL; + +-- hasnt_opclass( schema, name ) +CREATE OR REPLACE FUNCTION hasnt_opclass( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _opc_exists( $1, $2 ), 'Operator class ' || quote_ident($1) || '.' || quote_ident($2) || ' should exist' ); +$$ LANGUAGE SQL; + +-- hasnt_opclass( name, description ) +CREATE OR REPLACE FUNCTION hasnt_opclass( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( NOT _opc_exists( NULL, $1 ), $2) +$$ LANGUAGE SQL; + +-- hasnt_opclass( name ) +CREATE OR REPLACE FUNCTION hasnt_opclass( NAME ) +RETURNS TEXT AS $$ + SELECT ok( NOT _opc_exists( NULL, $1 ), 'Operator class ' || quote_ident($1) || ' should exist' ); +$$ LANGUAGE SQL; + +-- opclasses_are( schema, opclasses[], description ) +CREATE OR REPLACE FUNCTION opclasses_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'operator classes', + ARRAY( + SELECT oc.opcname + FROM pg_catalog.pg_opclass oc + JOIN pg_catalog.pg_namespace n ON oc.opcnamespace = n.oid + WHERE n.nspname = $1 + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT oc.opcname + FROM pg_catalog.pg_opclass oc + JOIN pg_catalog.pg_namespace n ON oc.opcnamespace = n.oid + WHERE n.nspname = $1 + ), + $3 + ); +$$ LANGUAGE SQL; + +-- opclasses_are( schema, opclasses[] ) +CREATE OR REPLACE FUNCTION opclasses_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT opclasses_are( $1, $2, 'Schema ' || quote_ident($1) || ' should have the correct operator classes' ); +$$ LANGUAGE SQL; + +-- opclasses_are( opclasses[], description ) +CREATE OR REPLACE FUNCTION opclasses_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'operator classes', + ARRAY( + SELECT oc.opcname + FROM pg_catalog.pg_opclass oc + JOIN pg_catalog.pg_namespace n ON oc.opcnamespace = n.oid + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_opclass_is_visible(oc.oid) + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT oc.opcname + FROM pg_catalog.pg_opclass oc + JOIN pg_catalog.pg_namespace n ON oc.opcnamespace = n.oid + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_opclass_is_visible(oc.oid) + ), + $2 + ); +$$ LANGUAGE SQL; + +-- opclasses_are( opclasses[] ) +CREATE OR REPLACE FUNCTION opclasses_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT opclasses_are( $1, 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct operator classes' ); +$$ LANGUAGE SQL; + +-- rules_are( schema, table, rules[], description ) +CREATE OR REPLACE FUNCTION rules_are( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'rules', + ARRAY( + SELECT r.rulename + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid + WHERE c.relname = $2 + AND n.nspname = $1 + EXCEPT + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + ), + ARRAY( + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + EXCEPT + SELECT r.rulename + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid + WHERE c.relname = $2 + AND n.nspname = $1 + ), + $4 + ); +$$ LANGUAGE SQL; + +-- rules_are( schema, table, rules[] ) +CREATE OR REPLACE FUNCTION rules_are( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT rules_are( $1, $2, $3, 'Relation ' || quote_ident($1) || '.' || quote_ident($2) || ' should have the correct rules' ); +$$ LANGUAGE SQL; + +-- rules_are( table, rules[], description ) +CREATE OR REPLACE FUNCTION rules_are( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'rules', + ARRAY( + SELECT r.rulename + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid + WHERE c.relname = $1 + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_table_is_visible(c.oid) + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT r.rulename + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid + AND c.relname = $1 + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_table_is_visible(c.oid) + ), + $3 + ); +$$ LANGUAGE SQL; + +-- rules_are( table, rules[] ) +CREATE OR REPLACE FUNCTION rules_are( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT rules_are( $1, $2, 'Relation ' || quote_ident($1) || ' should have the correct rules' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _is_instead( NAME, NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT r.is_instead + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid + WHERE r.rulename = $3 + AND c.relname = $2 + AND n.nspname = $1 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _is_instead( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT r.is_instead + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + WHERE r.rulename = $2 + AND c.relname = $1 + AND pg_catalog.pg_table_is_visible(c.oid) +$$ LANGUAGE SQL; + +-- has_rule( schema, table, rule, description ) +CREATE OR REPLACE FUNCTION has_rule( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2, $3) IS NOT NULL, $4 ); +$$ LANGUAGE SQL; + +-- has_rule( schema, table, rule ) +CREATE OR REPLACE FUNCTION has_rule( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2, $3) IS NOT NULL, 'Relation ' || quote_ident($1) || '.' || quote_ident($2) || ' should have rule ' || quote_ident($3) ); +$$ LANGUAGE SQL; + +-- has_rule( table, rule, description ) +CREATE OR REPLACE FUNCTION has_rule( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2) IS NOT NULL, $3 ); +$$ LANGUAGE SQL; + +-- has_rule( table, rule ) +CREATE OR REPLACE FUNCTION has_rule( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2) IS NOT NULL, 'Relation ' || quote_ident($1) || ' should have rule ' || quote_ident($2) ); +$$ LANGUAGE SQL; + +-- hasnt_rule( schema, table, rule, description ) +CREATE OR REPLACE FUNCTION hasnt_rule( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2, $3) IS NULL, $4 ); +$$ LANGUAGE SQL; + +-- hasnt_rule( schema, table, rule ) +CREATE OR REPLACE FUNCTION hasnt_rule( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2, $3) IS NULL, 'Relation ' || quote_ident($1) || '.' || quote_ident($2) || ' should not have rule ' || quote_ident($3) ); +$$ LANGUAGE SQL; + +-- hasnt_rule( table, rule, description ) +CREATE OR REPLACE FUNCTION hasnt_rule( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2) IS NULL, $3 ); +$$ LANGUAGE SQL; + +-- hasnt_rule( table, rule ) +CREATE OR REPLACE FUNCTION hasnt_rule( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( _is_instead($1, $2) IS NULL, 'Relation ' || quote_ident($1) || ' should not have rule ' || quote_ident($2) ); +$$ LANGUAGE SQL; + +-- rule_is_instead( schema, table, rule, description ) +CREATE OR REPLACE FUNCTION rule_is_instead( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +DECLARE + is_it boolean := _is_instead($1, $2, $3); +BEGIN + IF is_it IS NOT NULL THEN RETURN ok( is_it, $4 ); END IF; + RETURN ok( FALSE, $4 ) || E'\n' || diag( + ' Rule ' || quote_ident($3) || ' does not exist' + ); +END; +$$ LANGUAGE plpgsql; + +-- rule_is_instead( schema, table, rule ) +CREATE OR REPLACE FUNCTION rule_is_instead( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT rule_is_instead( $1, $2, $3, 'Rule ' || quote_ident($3) || ' on relation ' || quote_ident($1) || '.' || quote_ident($2) || ' should be an INSTEAD rule' ); +$$ LANGUAGE SQL; + +-- rule_is_instead( table, rule, description ) +CREATE OR REPLACE FUNCTION rule_is_instead( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +DECLARE + is_it boolean := _is_instead($1, $2); +BEGIN + IF is_it IS NOT NULL THEN RETURN ok( is_it, $3 ); END IF; + RETURN ok( FALSE, $3 ) || E'\n' || diag( + ' Rule ' || quote_ident($2) || ' does not exist' + ); +END; +$$ LANGUAGE plpgsql; + +-- rule_is_instead( table, rule ) +CREATE OR REPLACE FUNCTION rule_is_instead( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT rule_is_instead($1, $2, 'Rule ' || quote_ident($2) || ' on relation ' || quote_ident($1) || ' should be an INSTEAD rule' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _expand_on( char ) +RETURNS text AS $$ + SELECT CASE $1 + WHEN '1' THEN 'SELECT' + WHEN '2' THEN 'UPDATE' + WHEN '3' THEN 'INSERT' + WHEN '4' THEN 'DELETE' + ELSE 'UNKNOWN' END +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION _contract_on( TEXT ) +RETURNS "char" AS $$ + SELECT CASE substring(LOWER($1) FROM 1 FOR 1) + WHEN 's' THEN '1'::"char" + WHEN 'u' THEN '2'::"char" + WHEN 'i' THEN '3'::"char" + WHEN 'd' THEN '4'::"char" + ELSE '0'::"char" END +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION _rule_on( NAME, NAME, NAME ) +RETURNS "char" AS $$ + SELECT r.ev_type + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid + WHERE r.rulename = $3 + AND c.relname = $2 + AND n.nspname = $1 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _rule_on( NAME, NAME ) +RETURNS "char" AS $$ + SELECT r.ev_type + FROM pg_catalog.pg_rewrite r + JOIN pg_catalog.pg_class c ON c.oid = r.ev_class + WHERE r.rulename = $2 + AND c.relname = $1 +$$ LANGUAGE SQL; + +-- rule_is_on( schema, table, rule, event, description ) +CREATE OR REPLACE FUNCTION rule_is_on( NAME, NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want char := _contract_on($4); + have char := _rule_on($1, $2, $3); +BEGIN + IF have IS NOT NULL THEN + RETURN is( _expand_on(have), _expand_on(want), $5 ); + END IF; + + RETURN ok( false, $5 ) || E'\n' || diag( + ' Rule ' || quote_ident($3) || ' does not exist on ' + || quote_ident($1) || '.' || quote_ident($2) + ); +END; +$$ LANGUAGE plpgsql; + +-- rule_is_on( schema, table, rule, event ) +CREATE OR REPLACE FUNCTION rule_is_on( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT rule_is_on( + $1, $2, $3, $4, + 'Rule ' || quote_ident($3) || ' should be on ' || _expand_on(_contract_on($4)::char) + || ' to ' || quote_ident($1) || '.' || quote_ident($2) + ); +$$ LANGUAGE SQL; + +-- rule_is_on( table, rule, event, description ) +CREATE OR REPLACE FUNCTION rule_is_on( NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want char := _contract_on($3); + have char := _rule_on($1, $2); +BEGIN + IF have IS NOT NULL THEN + RETURN is( _expand_on(have), _expand_on(want), $4 ); + END IF; + + RETURN ok( false, $4 ) || E'\n' || diag( + ' Rule ' || quote_ident($2) || ' does not exist on ' + || quote_ident($1) + ); +END; +$$ LANGUAGE plpgsql; + +-- rule_is_on( table, rule, event ) +CREATE OR REPLACE FUNCTION rule_is_on( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT rule_is_on( + $1, $2, $3, + 'Rule ' || quote_ident($2) || ' should be on ' + || _expand_on(_contract_on($3)::char) || ' to ' || quote_ident($1) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _nosuch( NAME, NAME, NAME[]) +RETURNS TEXT AS $$ + SELECT E'\n' || diag( + ' Function ' + || CASE WHEN $1 IS NOT NULL THEN quote_ident($1) || '.' ELSE '' END + || quote_ident($2) || '(' + || array_to_string($3, ', ') || ') does not exist' + ); +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, NAME[], anyelement, anyelement, TEXT) +RETURNS TEXT AS $$ + SELECT CASE WHEN $4 IS NULL + THEN ok( FALSE, $6 ) || _nosuch($1, $2, $3) + ELSE is( $4, $5, $6 ) + END; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, NAME[], boolean, TEXT) +RETURNS TEXT AS $$ + SELECT CASE WHEN $4 IS NULL + THEN ok( FALSE, $5 ) || _nosuch($1, $2, $3) + ELSE ok( $4, $5 ) + END; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, anyelement, anyelement, TEXT) +RETURNS TEXT AS $$ + SELECT CASE WHEN $3 IS NULL + THEN ok( FALSE, $5 ) || _nosuch($1, $2, '{}') + ELSE is( $3, $4, $5 ) + END; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _func_compare( NAME, NAME, boolean, TEXT) +RETURNS TEXT AS $$ + SELECT CASE WHEN $3 IS NULL + THEN ok( FALSE, $4 ) || _nosuch($1, $2, '{}') + ELSE ok( $3, $4 ) + END; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _lang ( NAME, NAME, NAME[] ) +RETURNS NAME AS $$ + SELECT l.lanname + FROM tap_funky f + JOIN pg_catalog.pg_language l ON f.langoid = l.oid + WHERE f.schema = $1 + and f.name = $2 + AND f.args = array_to_string($3, ',') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _lang ( NAME, NAME ) +RETURNS NAME AS $$ + SELECT l.lanname + FROM tap_funky f + JOIN pg_catalog.pg_language l ON f.langoid = l.oid + WHERE f.schema = $1 + and f.name = $2 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _lang ( NAME, NAME[] ) +RETURNS NAME AS $$ + SELECT l.lanname + FROM tap_funky f + JOIN pg_catalog.pg_language l ON f.langoid = l.oid + WHERE f.name = $1 + AND f.args = array_to_string($2, ',') + AND f.is_visible; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _lang ( NAME ) +RETURNS NAME AS $$ + SELECT l.lanname + FROM tap_funky f + JOIN pg_catalog.pg_language l ON f.langoid = l.oid + WHERE f.name = $1 + AND f.is_visible; +$$ LANGUAGE SQL; + +-- function_lang_is( schema, function, args[], language, description ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME[], NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, $3, _lang($1, $2, $3), $4, $5 ); +$$ LANGUAGE SQL; + +-- function_lang_is( schema, function, args[], language ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME[], NAME ) +RETURNS TEXT AS $$ + SELECT function_lang_is( + $1, $2, $3, $4, + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should be written in ' || quote_ident($4) + ); +$$ LANGUAGE SQL; + +-- function_lang_is( schema, function, language, description ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, _lang($1, $2), $3, $4 ); +$$ LANGUAGE SQL; + +-- function_lang_is( schema, function, language ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, NAME ) +RETURNS TEXT AS $$ + SELECT function_lang_is( + $1, $2, $3, + 'Function ' || quote_ident($1) || '.' || quote_ident($2) + || '() should be written in ' || quote_ident($3) + ); +$$ LANGUAGE SQL; + +-- function_lang_is( function, args[], language, description ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME[], NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, $2, _lang($1, $2), $3, $4 ); +$$ LANGUAGE SQL; + +-- function_lang_is( function, args[], language ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME[], NAME ) +RETURNS TEXT AS $$ + SELECT function_lang_is( + $1, $2, $3, + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should be written in ' || quote_ident($3) + ); +$$ LANGUAGE SQL; + +-- function_lang_is( function, language, description ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, _lang($1), $2, $3 ); +$$ LANGUAGE SQL; + +-- function_lang_is( function, language ) +CREATE OR REPLACE FUNCTION function_lang_is( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT function_lang_is( + $1, $2, + 'Function ' || quote_ident($1) + || '() should be written in ' || quote_ident($2) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _returns ( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT returns + FROM tap_funky + WHERE schema = $1 + AND name = $2 + AND args = array_to_string($3, ',') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _returns ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT returns FROM tap_funky WHERE schema = $1 AND name = $2 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _returns ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT returns + FROM tap_funky + WHERE name = $1 + AND args = array_to_string($2, ',') + AND is_visible; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _returns ( NAME ) +RETURNS TEXT AS $$ + SELECT returns FROM tap_funky WHERE name = $1 AND is_visible; +$$ LANGUAGE SQL; + +-- function_returns( schema, function, args[], type, description ) +CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, NAME[], TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, $3, _returns($1, $2, $3), $4, $5 ); +$$ LANGUAGE SQL; + +-- function_returns( schema, function, args[], type ) +CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT function_returns( + $1, $2, $3, $4, + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should return ' || $4 + ); +$$ LANGUAGE SQL; + +-- function_returns( schema, function, type, description ) +CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, _returns($1, $2), $3, $4 ); +$$ LANGUAGE SQL; + +-- function_returns( schema, function, type ) +CREATE OR REPLACE FUNCTION function_returns( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT function_returns( + $1, $2, $3, + 'Function ' || quote_ident($1) || '.' || quote_ident($2) + || '() should return ' || $3 + ); +$$ LANGUAGE SQL; + +-- function_returns( function, args[], type, description ) +CREATE OR REPLACE FUNCTION function_returns( NAME, NAME[], TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, $2, _returns($1, $2), $3, $4 ); +$$ LANGUAGE SQL; + +-- function_returns( function, args[], type ) +CREATE OR REPLACE FUNCTION function_returns( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT function_returns( + $1, $2, $3, + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should return ' || $3 + ); +$$ LANGUAGE SQL; + +-- function_returns( function, type, description ) +CREATE OR REPLACE FUNCTION function_returns( NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, _returns($1), $2, $3 ); +$$ LANGUAGE SQL; + +-- function_returns( function, type ) +CREATE OR REPLACE FUNCTION function_returns( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT function_returns( + $1, $2, + 'Function ' || quote_ident($1) || '() should return ' || $2 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _definer ( NAME, NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT is_definer + FROM tap_funky + WHERE schema = $1 + AND name = $2 + AND args = array_to_string($3, ',') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _definer ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT is_definer FROM tap_funky WHERE schema = $1 AND name = $2 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _definer ( NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT is_definer + FROM tap_funky + WHERE name = $1 + AND args = array_to_string($2, ',') + AND is_visible; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _definer ( NAME ) +RETURNS BOOLEAN AS $$ + SELECT is_definer FROM tap_funky WHERE name = $1 AND is_visible; +$$ LANGUAGE SQL; + +-- is_definer( schema, function, args[], description ) +CREATE OR REPLACE FUNCTION is_definer ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, $3, _definer($1, $2, $3), $4 ); +$$ LANGUAGE SQL; + +-- is_definer( schema, function, args[] ) +CREATE OR REPLACE FUNCTION is_definer( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _definer($1, $2, $3), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should be security definer' + ); +$$ LANGUAGE sql; + +-- is_definer( schema, function, description ) +CREATE OR REPLACE FUNCTION is_definer ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, _definer($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- is_definer( schema, function ) +CREATE OR REPLACE FUNCTION is_definer( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _definer($1, $2), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be security definer' + ); +$$ LANGUAGE sql; + +-- is_definer( function, args[], description ) +CREATE OR REPLACE FUNCTION is_definer ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, $2, _definer($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- is_definer( function, args[] ) +CREATE OR REPLACE FUNCTION is_definer( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _definer($1, $2), + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should be security definer' + ); +$$ LANGUAGE sql; + +-- is_definer( function, description ) +CREATE OR REPLACE FUNCTION is_definer( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, _definer($1), $2 ); +$$ LANGUAGE sql; + +-- is_definer( function ) +CREATE OR REPLACE FUNCTION is_definer( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _definer($1), 'Function ' || quote_ident($1) || '() should be security definer' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _agg ( NAME, NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT is_agg + FROM tap_funky + WHERE schema = $1 + AND name = $2 + AND args = array_to_string($3, ',') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _agg ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT is_agg FROM tap_funky WHERE schema = $1 AND name = $2 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _agg ( NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT is_agg + FROM tap_funky + WHERE name = $1 + AND args = array_to_string($2, ',') + AND is_visible; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _agg ( NAME ) +RETURNS BOOLEAN AS $$ + SELECT is_agg FROM tap_funky WHERE name = $1 AND is_visible; +$$ LANGUAGE SQL; + +-- is_aggregate( schema, function, args[], description ) +CREATE OR REPLACE FUNCTION is_aggregate ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, $3, _agg($1, $2, $3), $4 ); +$$ LANGUAGE SQL; + +-- is_aggregate( schema, function, args[] ) +CREATE OR REPLACE FUNCTION is_aggregate( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _agg($1, $2, $3), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should be an aggregate function' + ); +$$ LANGUAGE sql; + +-- is_aggregate( schema, function, description ) +CREATE OR REPLACE FUNCTION is_aggregate ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, _agg($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- is_aggregate( schema, function ) +CREATE OR REPLACE FUNCTION is_aggregate( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _agg($1, $2), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be an aggregate function' + ); +$$ LANGUAGE sql; + +-- is_aggregate( function, args[], description ) +CREATE OR REPLACE FUNCTION is_aggregate ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, $2, _agg($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- is_aggregate( function, args[] ) +CREATE OR REPLACE FUNCTION is_aggregate( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _agg($1, $2), + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should be an aggregate function' + ); +$$ LANGUAGE sql; + +-- is_aggregate( function, description ) +CREATE OR REPLACE FUNCTION is_aggregate( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, _agg($1), $2 ); +$$ LANGUAGE sql; + +-- is_aggregate( function ) +CREATE OR REPLACE FUNCTION is_aggregate( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _agg($1), 'Function ' || quote_ident($1) || '() should be an aggregate function' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _strict ( NAME, NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT is_strict + FROM tap_funky + WHERE schema = $1 + AND name = $2 + AND args = array_to_string($3, ',') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _strict ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT is_strict FROM tap_funky WHERE schema = $1 AND name = $2 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _strict ( NAME, NAME[] ) +RETURNS BOOLEAN AS $$ + SELECT is_strict + FROM tap_funky + WHERE name = $1 + AND args = array_to_string($2, ',') + AND is_visible; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _strict ( NAME ) +RETURNS BOOLEAN AS $$ + SELECT is_strict FROM tap_funky WHERE name = $1 AND is_visible; +$$ LANGUAGE SQL; + +-- is_strict( schema, function, args[], description ) +CREATE OR REPLACE FUNCTION is_strict ( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, $3, _strict($1, $2, $3), $4 ); +$$ LANGUAGE SQL; + +-- is_strict( schema, function, args[] ) +CREATE OR REPLACE FUNCTION is_strict( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _strict($1, $2, $3), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should be strict' + ); +$$ LANGUAGE sql; + +-- is_strict( schema, function, description ) +CREATE OR REPLACE FUNCTION is_strict ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, _strict($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- is_strict( schema, function ) +CREATE OR REPLACE FUNCTION is_strict( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT ok( + _strict($1, $2), + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '() should be strict' + ); +$$ LANGUAGE sql; + +-- is_strict( function, args[], description ) +CREATE OR REPLACE FUNCTION is_strict ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, $2, _strict($1, $2), $3 ); +$$ LANGUAGE SQL; + +-- is_strict( function, args[] ) +CREATE OR REPLACE FUNCTION is_strict( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT ok( + _strict($1, $2), + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should be strict' + ); +$$ LANGUAGE sql; + +-- is_strict( function, description ) +CREATE OR REPLACE FUNCTION is_strict( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, _strict($1), $2 ); +$$ LANGUAGE sql; + +-- is_strict( function ) +CREATE OR REPLACE FUNCTION is_strict( NAME ) +RETURNS TEXT AS $$ + SELECT ok( _strict($1), 'Function ' || quote_ident($1) || '() should be strict' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _expand_vol( char ) +RETURNS TEXT AS $$ + SELECT CASE $1 + WHEN 'i' THEN 'IMMUTABLE' + WHEN 's' THEN 'STABLE' + WHEN 'v' THEN 'VOLATILE' + ELSE 'UNKNOWN' END +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION _refine_vol( text ) +RETURNS text AS $$ + SELECT _expand_vol(substring(LOWER($1) FROM 1 FOR 1)::char); +$$ LANGUAGE SQL IMMUTABLE; + +CREATE OR REPLACE FUNCTION _vol ( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _expand_vol(volatility) + FROM tap_funky f + WHERE f.schema = $1 + and f.name = $2 + AND f.args = array_to_string($3, ',') +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _vol ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT _expand_vol(volatility) FROM tap_funky f + WHERE f.schema = $1 and f.name = $2 +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _vol ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _expand_vol(volatility) + FROM tap_funky f + WHERE f.name = $1 + AND f.args = array_to_string($2, ',') + AND f.is_visible; +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _vol ( NAME ) +RETURNS TEXT AS $$ + SELECT _expand_vol(volatility) FROM tap_funky f + WHERE f.name = $1 AND f.is_visible; +$$ LANGUAGE SQL; + +-- volatility_is( schema, function, args[], volatility, description ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, NAME[], TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, $3, _vol($1, $2, $3), _refine_vol($4), $5 ); +$$ LANGUAGE SQL; + +-- volatility_is( schema, function, args[], volatility ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT volatility_is( + $1, $2, $3, $4, + 'Function ' || quote_ident($1) || '.' || quote_ident($2) || '(' || + array_to_string($3, ', ') || ') should be ' || _refine_vol($4) + ); +$$ LANGUAGE SQL; + +-- volatility_is( schema, function, volatility, description ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare($1, $2, _vol($1, $2), _refine_vol($3), $4 ); +$$ LANGUAGE SQL; + +-- volatility_is( schema, function, volatility ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT volatility_is( + $1, $2, $3, + 'Function ' || quote_ident($1) || '.' || quote_ident($2) + || '() should be ' || _refine_vol($3) + ); +$$ LANGUAGE SQL; + +-- volatility_is( function, args[], volatility, description ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME[], TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, $2, _vol($1, $2), _refine_vol($3), $4 ); +$$ LANGUAGE SQL; + +-- volatility_is( function, args[], volatility ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT volatility_is( + $1, $2, $3, + 'Function ' || quote_ident($1) || '(' || + array_to_string($2, ', ') || ') should be ' || _refine_vol($3) + ); +$$ LANGUAGE SQL; + +-- volatility_is( function, volatility, description ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _func_compare(NULL, $1, _vol($1), _refine_vol($2), $3 ); +$$ LANGUAGE SQL; + +-- volatility_is( function, volatility ) +CREATE OR REPLACE FUNCTION volatility_is( NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT volatility_is( + $1, $2, + 'Function ' || quote_ident($1) || '() should be ' || _refine_vol($2) + ); +$$ LANGUAGE SQL; + +-- check_test( test_output, pass, name, description, diag, match_diag ) +CREATE OR REPLACE FUNCTION check_test( TEXT, BOOLEAN, TEXT, TEXT, TEXT, BOOLEAN ) +RETURNS SETOF TEXT AS $$ +DECLARE + tnumb INTEGER; + aok BOOLEAN; + adescr TEXT; + res BOOLEAN; + descr TEXT; + adiag TEXT; + have ALIAS FOR $1; + eok ALIAS FOR $2; + name ALIAS FOR $3; + edescr ALIAS FOR $4; + ediag ALIAS FOR $5; + matchit ALIAS FOR $6; +BEGIN + -- What test was it that just ran? + tnumb := currval('__tresults___numb_seq'); + + -- Fetch the results. + EXECUTE 'SELECT aok, descr FROM __tresults__ WHERE numb = ' || tnumb + INTO aok, adescr; + + -- Now delete those results. + EXECUTE 'DELETE FROM __tresults__ WHERE numb = ' || tnumb; + EXECUTE 'ALTER SEQUENCE __tresults___numb_seq RESTART WITH ' || tnumb; + + -- Set up the description. + descr := coalesce( name || ' ', 'Test ' ) || 'should '; + + -- So, did the test pass? + RETURN NEXT is( + aok, + eok, + descr || CASE eok WHEN true then 'pass' ELSE 'fail' END + ); + + -- Was the description as expected? + IF edescr IS NOT NULL THEN + RETURN NEXT is( + adescr, + edescr, + descr || 'have the proper description' + ); + END IF; + + -- Were the diagnostics as expected? + IF ediag IS NOT NULL THEN + -- Remove ok and the test number. + adiag := substring( + have + FROM CASE WHEN aok THEN 4 ELSE 9 END + char_length(tnumb::text) + ); + + -- Remove the description, if there is one. + IF adescr <> '' THEN + adiag := substring( adiag FROM 3 + char_length( diag( adescr ) ) ); + END IF; + + -- Remove failure message from ok(). + IF NOT aok THEN + adiag := substring( + adiag + FROM 14 + char_length(tnumb::text) + + CASE adescr WHEN '' THEN 3 ELSE 3 + char_length( diag( adescr ) ) END + ); + END IF; + + -- Remove the #s. + adiag := replace( substring(adiag from 3), E'\n# ', E'\n' ); + + -- Now compare the diagnostics. + IF matchit THEN + RETURN NEXT matches( + adiag, + ediag, + descr || 'have the proper diagnostics' + ); + ELSE + RETURN NEXT is( + adiag, + ediag, + descr || 'have the proper diagnostics' + ); + END IF; + END IF; + + -- And we're done + RETURN; +END; +$$ LANGUAGE plpgsql; + +-- check_test( test_output, pass, name, description, diag ) +CREATE OR REPLACE FUNCTION check_test( TEXT, BOOLEAN, TEXT, TEXT, TEXT ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM check_test( $1, $2, $3, $4, $5, FALSE ); +$$ LANGUAGE sql; + +-- check_test( test_output, pass, name, description ) +CREATE OR REPLACE FUNCTION check_test( TEXT, BOOLEAN, TEXT, TEXT ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM check_test( $1, $2, $3, $4, NULL, FALSE ); +$$ LANGUAGE sql; + +-- check_test( test_output, pass, name ) +CREATE OR REPLACE FUNCTION check_test( TEXT, BOOLEAN, TEXT ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM check_test( $1, $2, $3, NULL, NULL, FALSE ); +$$ LANGUAGE sql; + +-- check_test( test_output, pass ) +CREATE OR REPLACE FUNCTION check_test( TEXT, BOOLEAN ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM check_test( $1, $2, NULL, NULL, NULL, FALSE ); +$$ LANGUAGE sql; + + +CREATE OR REPLACE FUNCTION findfuncs( NAME, TEXT ) +RETURNS TEXT[] AS $$ + SELECT ARRAY( + SELECT DISTINCT quote_ident(n.nspname) || '.' || quote_ident(p.proname) AS pname + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid + WHERE n.nspname = $1 + AND p.proname ~ $2 + ORDER BY pname + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION findfuncs( TEXT ) +RETURNS TEXT[] AS $$ + SELECT ARRAY( + SELECT DISTINCT quote_ident(n.nspname) || '.' || quote_ident(p.proname) AS pname + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_namespace n ON p.pronamespace = n.oid + WHERE pg_catalog.pg_function_is_visible(p.oid) + AND p.proname ~ $1 + ORDER BY pname + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _runem( text[], boolean ) +RETURNS SETOF TEXT AS $$ +DECLARE + tap text; + lbound int := array_lower($1, 1); +BEGIN + IF lbound IS NULL THEN RETURN; END IF; + FOR i IN lbound..array_upper($1, 1) LOOP + -- Send the name of the function to diag if warranted. + IF $2 THEN RETURN NEXT diag( $1[i] || '()' ); END IF; + -- Execute the tap function and return its results. + FOR tap IN EXECUTE 'SELECT * FROM ' || $1[i] || '()' LOOP + RETURN NEXT tap; + END LOOP; + END LOOP; + RETURN; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _is_verbose() +RETURNS BOOLEAN AS $$ + SELECT current_setting('client_min_messages') NOT IN ( + 'warning', 'error', 'fatal', 'panic' + ); +$$ LANGUAGE sql STABLE; + +-- do_tap( schema, pattern ) +CREATE OR REPLACE FUNCTION do_tap( name, text ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM _runem( findfuncs($1, $2), _is_verbose() ); +$$ LANGUAGE sql; + +-- do_tap( schema ) +CREATE OR REPLACE FUNCTION do_tap( name ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM _runem( findfuncs($1, '^test'), _is_verbose() ); +$$ LANGUAGE sql; + +-- do_tap( pattern ) +CREATE OR REPLACE FUNCTION do_tap( text ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM _runem( findfuncs($1), _is_verbose() ); +$$ LANGUAGE sql; + +-- do_tap() +CREATE OR REPLACE FUNCTION do_tap( ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM _runem( findfuncs('^test'), _is_verbose()); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _currtest() +RETURNS INTEGER AS $$ +BEGIN + RETURN currval('__tresults___numb_seq'); +EXCEPTION + WHEN object_not_in_prerequisite_state THEN RETURN 0; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _cleanup() +RETURNS boolean AS $$ + DROP TABLE __tresults__; + DROP SEQUENCE __tresults___numb_seq; + DROP TABLE __tcache__; + DROP SEQUENCE __tcache___id_seq; + SELECT TRUE; +$$ LANGUAGE sql; + +-- diag_test_name ( test_name ) +CREATE OR REPLACE FUNCTION diag_test_name(TEXT) +RETURNS TEXT AS $$ + SELECT diag($1 || '()'); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _runner( text[], text[], text[], text[], text[] ) +RETURNS SETOF TEXT AS $$ +DECLARE + startup ALIAS FOR $1; + shutdown ALIAS FOR $2; + setup ALIAS FOR $3; + teardown ALIAS FOR $4; + tests ALIAS FOR $5; + tap text; + verbos boolean := _is_verbose(); -- verbose is a reserved word in 8.5. + num_faild INTEGER := 0; +BEGIN + BEGIN + -- No plan support. + PERFORM * FROM no_plan(); + FOR tap IN SELECT * FROM _runem(startup, false) LOOP RETURN NEXT tap; END LOOP; + EXCEPTION + -- Catch all exceptions and simply rethrow custom exceptions. This + -- will roll back everything in the above block. + WHEN raise_exception THEN + RAISE EXCEPTION '%', SQLERRM; + END; + + BEGIN + FOR i IN 1..array_upper(tests, 1) LOOP + BEGIN + -- What test are we running? + IF verbos THEN RETURN NEXT diag_test_name(tests[i]); END IF; + + -- Run the setup functions. + FOR tap IN SELECT * FROM _runem(setup, false) LOOP RETURN NEXT tap; END LOOP; + + -- Run the actual test function. + FOR tap IN EXECUTE 'SELECT * FROM ' || tests[i] || '()' LOOP + RETURN NEXT tap; + END LOOP; + + -- Run the teardown functions. + FOR tap IN SELECT * FROM _runem(teardown, false) LOOP RETURN NEXT tap; END LOOP; + + -- Remember how many failed and then roll back. + num_faild := num_faild + num_failed(); + RAISE EXCEPTION '__TAP_ROLLBACK__'; + + EXCEPTION WHEN raise_exception THEN + IF SQLERRM <> '__TAP_ROLLBACK__' THEN + -- We didn't raise it, so propagate it. + RAISE EXCEPTION '%', SQLERRM; + END IF; + END; + END LOOP; + + -- Run the shutdown functions. + FOR tap IN SELECT * FROM _runem(shutdown, false) LOOP RETURN NEXT tap; END LOOP; + + -- Raise an exception to rollback any changes. + RAISE EXCEPTION '__TAP_ROLLBACK__'; + EXCEPTION WHEN raise_exception THEN + IF SQLERRM <> '__TAP_ROLLBACK__' THEN + -- We didn't raise it, so propagate it. + RAISE EXCEPTION '%', SQLERRM; + END IF; + END; + -- Finish up. + FOR tap IN SELECT * FROM _finish( currval('__tresults___numb_seq')::integer, 0, num_faild ) LOOP + RETURN NEXT tap; + END LOOP; + + -- Clean up and return. + PERFORM _cleanup(); + RETURN; +END; +$$ LANGUAGE plpgsql; + +-- runtests( schema, match ) +CREATE OR REPLACE FUNCTION runtests( NAME, TEXT ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM _runner( + findfuncs( $1, '^startup' ), + findfuncs( $1, '^shutdown' ), + findfuncs( $1, '^setup' ), + findfuncs( $1, '^teardown' ), + findfuncs( $1, $2 ) + ); +$$ LANGUAGE sql; + +-- runtests( schema ) +CREATE OR REPLACE FUNCTION runtests( NAME ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM runtests( $1, '^test' ); +$$ LANGUAGE sql; + +-- runtests( match ) +CREATE OR REPLACE FUNCTION runtests( TEXT ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM _runner( + findfuncs( '^startup' ), + findfuncs( '^shutdown' ), + findfuncs( '^setup' ), + findfuncs( '^teardown' ), + findfuncs( $1 ) + ); +$$ LANGUAGE sql; + +-- runtests( ) +CREATE OR REPLACE FUNCTION runtests( ) +RETURNS SETOF TEXT AS $$ + SELECT * FROM runtests( '^test' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _temptable ( TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + EXECUTE 'CREATE TEMP TABLE ' || $2 || ' AS ' || _query($1); + return $2; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _temptable ( anyarray, TEXT ) +RETURNS TEXT AS $$ +BEGIN + CREATE TEMP TABLE _____coltmp___ AS + SELECT $1[i] + FROM generate_series(array_lower($1, 1), array_upper($1, 1)) s(i); + EXECUTE 'ALTER TABLE _____coltmp___ RENAME TO ' || $2; + return $2; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _temptypes( TEXT ) +RETURNS TEXT AS $$ + SELECT array_to_string(ARRAY( + SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) + FROM pg_catalog.pg_attribute a + JOIN pg_catalog.pg_class c ON a.attrelid = c.oid + WHERE c.oid = ('pg_temp.' || $1)::pg_catalog.regclass + AND attnum > 0 + AND NOT attisdropped + ORDER BY attnum + ), ','); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _docomp( TEXT, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have ALIAS FOR $1; + want ALIAS FOR $2; + extras TEXT[] := '{}'; + missing TEXT[] := '{}'; + res BOOLEAN := TRUE; + msg TEXT := ''; + rec RECORD; +BEGIN + BEGIN + -- Find extra records. + FOR rec in EXECUTE 'SELECT * FROM ' || have || ' EXCEPT ' || $4 + || 'SELECT * FROM ' || want LOOP + extras := extras || rec::text; + END LOOP; + + -- Find missing records. + FOR rec in EXECUTE 'SELECT * FROM ' || want || ' EXCEPT ' || $4 + || 'SELECT * FROM ' || have LOOP + missing := missing || rec::text; + END LOOP; + + -- Drop the temporary tables. + EXECUTE 'DROP TABLE ' || have; + EXECUTE 'DROP TABLE ' || want; + EXCEPTION WHEN syntax_error OR datatype_mismatch THEN + msg := E'\n' || diag( + E' Columns differ between queries:\n' + || ' have: (' || _temptypes(have) || E')\n' + || ' want: (' || _temptypes(want) || ')' + ); + EXECUTE 'DROP TABLE ' || have; + EXECUTE 'DROP TABLE ' || want; + RETURN ok(FALSE, $3) || msg; + END; + + -- What extra records do we have? + IF extras[1] IS NOT NULL THEN + res := FALSE; + msg := E'\n' || diag( + E' Extra records:\n ' + || array_to_string( extras, E'\n ' ) + ); + END IF; + + -- What missing records do we have? + IF missing[1] IS NOT NULL THEN + res := FALSE; + msg := msg || E'\n' || diag( + E' Missing records:\n ' + || array_to_string( missing, E'\n ' ) + ); + END IF; + + RETURN ok(res, $3) || msg; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _relcomp( TEXT, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _docomp( + _temptable( $1, '__taphave__' ), + _temptable( $2, '__tapwant__' ), + $3, $4 + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _relcomp( TEXT, anyarray, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _docomp( + _temptable( $1, '__taphave__' ), + _temptable( $2, '__tapwant__' ), + $3, $4 + ); +$$ LANGUAGE sql; + +-- set_eq( sql, sql, description ) +CREATE OR REPLACE FUNCTION set_eq( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, '' ); +$$ LANGUAGE sql; + +-- set_eq( sql, sql ) +CREATE OR REPLACE FUNCTION set_eq( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::text, '' ); +$$ LANGUAGE sql; + +-- set_eq( sql, array, description ) +CREATE OR REPLACE FUNCTION set_eq( TEXT, anyarray, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, '' ); +$$ LANGUAGE sql; + +-- set_eq( sql, array ) +CREATE OR REPLACE FUNCTION set_eq( TEXT, anyarray ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::text, '' ); +$$ LANGUAGE sql; + +-- bag_eq( sql, sql, description ) +CREATE OR REPLACE FUNCTION bag_eq( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, 'ALL ' ); +$$ LANGUAGE sql; + +-- bag_eq( sql, sql ) +CREATE OR REPLACE FUNCTION bag_eq( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::text, 'ALL ' ); +$$ LANGUAGE sql; + +-- bag_eq( sql, array, description ) +CREATE OR REPLACE FUNCTION bag_eq( TEXT, anyarray, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, 'ALL ' ); +$$ LANGUAGE sql; + +-- bag_eq( sql, array ) +CREATE OR REPLACE FUNCTION bag_eq( TEXT, anyarray ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::text, 'ALL ' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _do_ne( TEXT, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have ALIAS FOR $1; + want ALIAS FOR $2; + extras TEXT[] := '{}'; + missing TEXT[] := '{}'; + res BOOLEAN := TRUE; + msg TEXT := ''; +BEGIN + BEGIN + -- Find extra records. + EXECUTE 'SELECT EXISTS ( ' + || '( SELECT * FROM ' || have || ' EXCEPT ' || $4 + || ' SELECT * FROM ' || want + || ' ) UNION ( ' + || ' SELECT * FROM ' || want || ' EXCEPT ' || $4 + || ' SELECT * FROM ' || have + || ' ) LIMIT 1 )' INTO res; + + -- Drop the temporary tables. + EXECUTE 'DROP TABLE ' || have; + EXECUTE 'DROP TABLE ' || want; + EXCEPTION WHEN syntax_error OR datatype_mismatch THEN + msg := E'\n' || diag( + E' Columns differ between queries:\n' + || ' have: (' || _temptypes(have) || E')\n' + || ' want: (' || _temptypes(want) || ')' + ); + EXECUTE 'DROP TABLE ' || have; + EXECUTE 'DROP TABLE ' || want; + RETURN ok(FALSE, $3) || msg; + END; + + -- Return the value from the query. + RETURN ok(res, $3); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION _relne( TEXT, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _do_ne( + _temptable( $1, '__taphave__' ), + _temptable( $2, '__tapwant__' ), + $3, $4 + ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _relne( TEXT, anyarray, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _do_ne( + _temptable( $1, '__taphave__' ), + _temptable( $2, '__tapwant__' ), + $3, $4 + ); +$$ LANGUAGE sql; + +-- set_ne( sql, sql, description ) +CREATE OR REPLACE FUNCTION set_ne( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, $3, '' ); +$$ LANGUAGE sql; + +-- set_ne( sql, sql ) +CREATE OR REPLACE FUNCTION set_ne( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, NULL::text, '' ); +$$ LANGUAGE sql; + +-- set_ne( sql, array, description ) +CREATE OR REPLACE FUNCTION set_ne( TEXT, anyarray, TEXT ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, $3, '' ); +$$ LANGUAGE sql; + +-- set_ne( sql, array ) +CREATE OR REPLACE FUNCTION set_ne( TEXT, anyarray ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, NULL::text, '' ); +$$ LANGUAGE sql; + +-- bag_ne( sql, sql, description ) +CREATE OR REPLACE FUNCTION bag_ne( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, $3, 'ALL ' ); +$$ LANGUAGE sql; + +-- bag_ne( sql, sql ) +CREATE OR REPLACE FUNCTION bag_ne( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, NULL::text, 'ALL ' ); +$$ LANGUAGE sql; + +-- bag_ne( sql, array, description ) +CREATE OR REPLACE FUNCTION bag_ne( TEXT, anyarray, TEXT ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, $3, 'ALL ' ); +$$ LANGUAGE sql; + +-- bag_ne( sql, array ) +CREATE OR REPLACE FUNCTION bag_ne( TEXT, anyarray ) +RETURNS TEXT AS $$ + SELECT _relne( $1, $2, NULL::text, 'ALL ' ); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _relcomp( TEXT, TEXT, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have TEXT := _temptable( $1, '__taphave__' ); + want TEXT := _temptable( $2, '__tapwant__' ); + results TEXT[] := '{}'; + res BOOLEAN := TRUE; + msg TEXT := ''; + rec RECORD; +BEGIN + BEGIN + -- Find relevant records. + FOR rec in EXECUTE 'SELECT * FROM ' || want || ' ' || $4 + || ' SELECT * FROM ' || have LOOP + results := results || rec::text; + END LOOP; + + -- Drop the temporary tables. + EXECUTE 'DROP TABLE ' || have; + EXECUTE 'DROP TABLE ' || want; + EXCEPTION WHEN syntax_error OR datatype_mismatch THEN + msg := E'\n' || diag( + E' Columns differ between queries:\n' + || ' have: (' || _temptypes(have) || E')\n' + || ' want: (' || _temptypes(want) || ')' + ); + EXECUTE 'DROP TABLE ' || have; + EXECUTE 'DROP TABLE ' || want; + RETURN ok(FALSE, $3) || msg; + END; + + -- What records do we have? + IF results[1] IS NOT NULL THEN + res := FALSE; + msg := msg || E'\n' || diag( + ' ' || $5 || E' records:\n ' + || array_to_string( results, E'\n ' ) + ); + END IF; + + RETURN ok(res, $3) || msg; +END; +$$ LANGUAGE plpgsql; + +-- set_has( sql, sql, description ) +CREATE OR REPLACE FUNCTION set_has( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, 'EXCEPT', 'Missing' ); +$$ LANGUAGE sql; + +-- set_has( sql, sql ) +CREATE OR REPLACE FUNCTION set_has( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::TEXT, 'EXCEPT', 'Missing' ); +$$ LANGUAGE sql; + +-- bag_has( sql, sql, description ) +CREATE OR REPLACE FUNCTION bag_has( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, 'EXCEPT ALL', 'Missing' ); +$$ LANGUAGE sql; + +-- bag_has( sql, sql ) +CREATE OR REPLACE FUNCTION bag_has( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::TEXT, 'EXCEPT ALL', 'Missing' ); +$$ LANGUAGE sql; + +-- set_hasnt( sql, sql, description ) +CREATE OR REPLACE FUNCTION set_hasnt( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, 'INTERSECT', 'Extra' ); +$$ LANGUAGE sql; + +-- set_hasnt( sql, sql ) +CREATE OR REPLACE FUNCTION set_hasnt( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::TEXT, 'INTERSECT', 'Extra' ); +$$ LANGUAGE sql; + +-- bag_hasnt( sql, sql, description ) +CREATE OR REPLACE FUNCTION bag_hasnt( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, 'INTERSECT ALL', 'Extra' ); +$$ LANGUAGE sql; + +-- bag_hasnt( sql, sql ) +CREATE OR REPLACE FUNCTION bag_hasnt( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, NULL::TEXT, 'INTERSECT ALL', 'Extra' ); +$$ LANGUAGE sql; + +-- results_eq( cursor, cursor, description ) +CREATE OR REPLACE FUNCTION results_eq( refcursor, refcursor, text ) +RETURNS TEXT AS $$ +DECLARE + have ALIAS FOR $1; + want ALIAS FOR $2; + have_rec RECORD; + want_rec RECORD; + have_found BOOLEAN; + want_found BOOLEAN; + rownum INTEGER := 1; +BEGIN + FETCH have INTO have_rec; + have_found := FOUND; + FETCH want INTO want_rec; + want_found := FOUND; + WHILE have_found OR want_found LOOP + IF have_rec IS DISTINCT FROM want_rec OR have_found <> want_found THEN + RETURN ok( false, $3 ) || E'\n' || diag( + ' Results differ beginning at row ' || rownum || E':\n' || + ' have: ' || CASE WHEN have_found THEN have_rec::text ELSE 'NULL' END || E'\n' || + ' want: ' || CASE WHEN want_found THEN want_rec::text ELSE 'NULL' END + ); + END IF; + rownum = rownum + 1; + FETCH have INTO have_rec; + have_found := FOUND; + FETCH want INTO want_rec; + want_found := FOUND; + END LOOP; + + RETURN ok( true, $3 ); +EXCEPTION + WHEN datatype_mismatch THEN + RETURN ok( false, $3 ) || E'\n' || diag( + E' Columns differ between queries:\n' || + ' have: ' || CASE WHEN have_found THEN have_rec::text ELSE 'NULL' END || E'\n' || + ' want: ' || CASE WHEN want_found THEN want_rec::text ELSE 'NULL' END + ); +END; +$$ LANGUAGE plpgsql; + +-- results_eq( cursor, cursor ) +CREATE OR REPLACE FUNCTION results_eq( refcursor, refcursor ) +RETURNS TEXT AS $$ + SELECT results_eq( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_eq( sql, sql, description ) +CREATE OR REPLACE FUNCTION results_eq( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have REFCURSOR; + want REFCURSOR; + res TEXT; +BEGIN + OPEN have FOR EXECUTE _query($1); + OPEN want FOR EXECUTE _query($2); + res := results_eq(have, want, $3); + CLOSE have; + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_eq( sql, sql ) +CREATE OR REPLACE FUNCTION results_eq( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT results_eq( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_eq( sql, array, description ) +CREATE OR REPLACE FUNCTION results_eq( TEXT, anyarray, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have REFCURSOR; + want REFCURSOR; + res TEXT; +BEGIN + OPEN have FOR EXECUTE _query($1); + OPEN want FOR SELECT $2[i] + FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); + res := results_eq(have, want, $3); + CLOSE have; + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_eq( sql, array ) +CREATE OR REPLACE FUNCTION results_eq( TEXT, anyarray ) +RETURNS TEXT AS $$ + SELECT results_eq( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_eq( sql, cursor, description ) +CREATE OR REPLACE FUNCTION results_eq( TEXT, refcursor, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have REFCURSOR; + res TEXT; +BEGIN + OPEN have FOR EXECUTE _query($1); + res := results_eq(have, $2, $3); + CLOSE have; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_eq( sql, cursor ) +CREATE OR REPLACE FUNCTION results_eq( TEXT, refcursor ) +RETURNS TEXT AS $$ + SELECT results_eq( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_eq( cursor, sql, description ) +CREATE OR REPLACE FUNCTION results_eq( refcursor, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want REFCURSOR; + res TEXT; +BEGIN + OPEN want FOR EXECUTE _query($2); + res := results_eq($1, want, $3); + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_eq( cursor, sql ) +CREATE OR REPLACE FUNCTION results_eq( refcursor, TEXT ) +RETURNS TEXT AS $$ + SELECT results_eq( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_eq( cursor, array, description ) +CREATE OR REPLACE FUNCTION results_eq( refcursor, anyarray, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want REFCURSOR; + res TEXT; +BEGIN + OPEN want FOR SELECT $2[i] + FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); + res := results_eq($1, want, $3); + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_eq( cursor, array ) +CREATE OR REPLACE FUNCTION results_eq( refcursor, anyarray ) +RETURNS TEXT AS $$ + SELECT results_eq( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_ne( cursor, cursor, description ) +CREATE OR REPLACE FUNCTION results_ne( refcursor, refcursor, text ) +RETURNS TEXT AS $$ +DECLARE + have ALIAS FOR $1; + want ALIAS FOR $2; + have_rec RECORD; + want_rec RECORD; + have_found BOOLEAN; + want_found BOOLEAN; +BEGIN + FETCH have INTO have_rec; + have_found := FOUND; + FETCH want INTO want_rec; + want_found := FOUND; + WHILE have_found OR want_found LOOP + IF have_rec IS DISTINCT FROM want_rec OR have_found <> want_found THEN + RETURN ok( true, $3 ); + ELSE + FETCH have INTO have_rec; + have_found := FOUND; + FETCH want INTO want_rec; + want_found := FOUND; + END IF; + END LOOP; + RETURN ok( false, $3 ); +EXCEPTION + WHEN datatype_mismatch THEN + RETURN ok( false, $3 ) || E'\n' || diag( + E' Columns differ between queries:\n' || + ' have: ' || CASE WHEN have_found THEN have_rec::text ELSE 'NULL' END || E'\n' || + ' want: ' || CASE WHEN want_found THEN want_rec::text ELSE 'NULL' END + ); +END; +$$ LANGUAGE plpgsql; + +-- results_ne( cursor, cursor ) +CREATE OR REPLACE FUNCTION results_ne( refcursor, refcursor ) +RETURNS TEXT AS $$ + SELECT results_ne( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_ne( sql, sql, description ) +CREATE OR REPLACE FUNCTION results_ne( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have REFCURSOR; + want REFCURSOR; + res TEXT; +BEGIN + OPEN have FOR EXECUTE _query($1); + OPEN want FOR EXECUTE _query($2); + res := results_ne(have, want, $3); + CLOSE have; + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_ne( sql, sql ) +CREATE OR REPLACE FUNCTION results_ne( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT results_ne( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_ne( sql, array, description ) +CREATE OR REPLACE FUNCTION results_ne( TEXT, anyarray, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have REFCURSOR; + want REFCURSOR; + res TEXT; +BEGIN + OPEN have FOR EXECUTE _query($1); + OPEN want FOR SELECT $2[i] + FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); + res := results_ne(have, want, $3); + CLOSE have; + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_ne( sql, array ) +CREATE OR REPLACE FUNCTION results_ne( TEXT, anyarray ) +RETURNS TEXT AS $$ + SELECT results_ne( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_ne( sql, cursor, description ) +CREATE OR REPLACE FUNCTION results_ne( TEXT, refcursor, TEXT ) +RETURNS TEXT AS $$ +DECLARE + have REFCURSOR; + res TEXT; +BEGIN + OPEN have FOR EXECUTE _query($1); + res := results_ne(have, $2, $3); + CLOSE have; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_ne( sql, cursor ) +CREATE OR REPLACE FUNCTION results_ne( TEXT, refcursor ) +RETURNS TEXT AS $$ + SELECT results_ne( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_ne( cursor, sql, description ) +CREATE OR REPLACE FUNCTION results_ne( refcursor, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want REFCURSOR; + res TEXT; +BEGIN + OPEN want FOR EXECUTE _query($2); + res := results_ne($1, want, $3); + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_ne( cursor, sql ) +CREATE OR REPLACE FUNCTION results_ne( refcursor, TEXT ) +RETURNS TEXT AS $$ + SELECT results_ne( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- results_ne( cursor, array, description ) +CREATE OR REPLACE FUNCTION results_ne( refcursor, anyarray, TEXT ) +RETURNS TEXT AS $$ +DECLARE + want REFCURSOR; + res TEXT; +BEGIN + OPEN want FOR SELECT $2[i] + FROM generate_series(array_lower($2, 1), array_upper($2, 1)) s(i); + res := results_ne($1, want, $3); + CLOSE want; + RETURN res; +END; +$$ LANGUAGE plpgsql; + +-- results_ne( cursor, array ) +CREATE OR REPLACE FUNCTION results_ne( refcursor, anyarray ) +RETURNS TEXT AS $$ + SELECT results_ne( $1, $2, NULL::text ); +$$ LANGUAGE sql; + +-- isa_ok( value, regtype, description ) +CREATE OR REPLACE FUNCTION isa_ok( anyelement, regtype, TEXT ) +RETURNS TEXT AS $$ +DECLARE + typeof regtype := pg_typeof($1); +BEGIN + IF typeof = $2 THEN RETURN ok(true, $3 || ' isa ' || $2 ); END IF; + RETURN ok(false, $3 || ' isa ' || $2 ) || E'\n' || + diag(' ' || $3 || ' isn''t a "' || $2 || '" it''s a "' || typeof || '"'); +END; +$$ LANGUAGE plpgsql; + +-- isa_ok( value, regtype ) +CREATE OR REPLACE FUNCTION isa_ok( anyelement, regtype ) +RETURNS TEXT AS $$ + SELECT isa_ok($1, $2, 'the value'); +$$ LANGUAGE sql; + +-- is_empty( sql, description ) +CREATE OR REPLACE FUNCTION is_empty( TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + extras TEXT[] := '{}'; + res BOOLEAN := TRUE; + msg TEXT := ''; + rec RECORD; +BEGIN + -- Find extra records. + FOR rec in EXECUTE _query($1) LOOP + extras := extras || rec::text; + END LOOP; + + -- What extra records do we have? + IF extras[1] IS NOT NULL THEN + res := FALSE; + msg := E'\n' || diag( + E' Unexpected records:\n ' + || array_to_string( extras, E'\n ' ) + ); + END IF; + + RETURN ok(res, $2) || msg; +END; +$$ LANGUAGE plpgsql; + +-- is_empty( sql ) +CREATE OR REPLACE FUNCTION is_empty( TEXT ) +RETURNS TEXT AS $$ + SELECT is_empty( $1, NULL ); +$$ LANGUAGE sql; + +-- collect_tap( tap, tap, tap ) +CREATE OR REPLACE FUNCTION collect_tap( VARIADIC text[] ) +RETURNS TEXT AS $$ + SELECT array_to_string($1, E'\n'); +$$ LANGUAGE sql; + +-- collect_tap( tap[] ) +CREATE OR REPLACE FUNCTION collect_tap( VARCHAR[] ) +RETURNS TEXT AS $$ + SELECT array_to_string($1, E'\n'); +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _tlike ( BOOLEAN, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT ok( $1, $4 ) || CASE WHEN $1 THEN '' ELSE E'\n' || diag( + ' error message: ' || COALESCE( quote_literal($2), 'NULL' ) || + E'\n doesn''t match: ' || COALESCE( quote_literal($3), 'NULL' ) + ) END; +$$ LANGUAGE sql; + +-- throws_like ( sql, pattern, description ) +CREATE OR REPLACE FUNCTION throws_like ( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + EXECUTE _query($1); + RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); +EXCEPTION WHEN OTHERS THEN + return _tlike( SQLERRM ~~ $2, SQLERRM, $2, $3 ); +END; +$$ LANGUAGE plpgsql; + +-- throws_like ( sql, pattern ) +CREATE OR REPLACE FUNCTION throws_like ( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT throws_like($1, $2, 'Should throw exception like ' || quote_literal($2) ); +$$ LANGUAGE sql; + +-- throws_ilike ( sql, pattern, description ) +CREATE OR REPLACE FUNCTION throws_ilike ( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + EXECUTE _query($1); + RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); +EXCEPTION WHEN OTHERS THEN + return _tlike( SQLERRM ~~* $2, SQLERRM, $2, $3 ); +END; +$$ LANGUAGE plpgsql; + +-- throws_ilike ( sql, pattern ) +CREATE OR REPLACE FUNCTION throws_ilike ( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT throws_ilike($1, $2, 'Should throw exception like ' || quote_literal($2) ); +$$ LANGUAGE sql; + +-- throws_matching ( sql, pattern, description ) +CREATE OR REPLACE FUNCTION throws_matching ( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + EXECUTE _query($1); + RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); +EXCEPTION WHEN OTHERS THEN + return _tlike( SQLERRM ~ $2, SQLERRM, $2, $3 ); +END; +$$ LANGUAGE plpgsql; + +-- throws_matching ( sql, pattern ) +CREATE OR REPLACE FUNCTION throws_matching ( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT throws_matching($1, $2, 'Should throw exception matching ' || quote_literal($2) ); +$$ LANGUAGE sql; + +-- throws_imatching ( sql, pattern, description ) +CREATE OR REPLACE FUNCTION throws_imatching ( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +BEGIN + EXECUTE _query($1); + RETURN ok( FALSE, $3 ) || E'\n' || diag( ' no exception thrown' ); +EXCEPTION WHEN OTHERS THEN + return _tlike( SQLERRM ~* $2, SQLERRM, $2, $3 ); +END; +$$ LANGUAGE plpgsql; + +-- throws_imatching ( sql, pattern ) +CREATE OR REPLACE FUNCTION throws_imatching ( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT throws_imatching($1, $2, 'Should throw exception matching ' || quote_literal($2) ); +$$ LANGUAGE sql; + +-- roles_are( roles[], description ) +CREATE OR REPLACE FUNCTION roles_are( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'roles', + ARRAY( + SELECT rolname + FROM pg_catalog.pg_roles + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT rolname + FROM pg_catalog.pg_roles + ), + $2 + ); +$$ LANGUAGE SQL; + +-- roles_are( roles[] ) +CREATE OR REPLACE FUNCTION roles_are( NAME[] ) +RETURNS TEXT AS $$ + SELECT roles_are( $1, 'There should be the correct roles' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _types_are ( NAME, NAME[], TEXT, CHAR[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'types', + ARRAY( + SELECT t.typname + FROM pg_catalog.pg_type t + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE ( + t.typrelid = 0 + OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid) + ) + AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid) + AND n.nspname = $1 + AND t.typtype = ANY( COALESCE($4, ARRAY['b', 'c', 'd', 'p', 'e']) ) + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT t.typname + FROM pg_catalog.pg_type t + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE ( + t.typrelid = 0 + OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid) + ) + AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid) + AND n.nspname = $1 + AND t.typtype = ANY( COALESCE($4, ARRAY['b', 'c', 'd', 'p', 'e']) ) + ), + $3 + ); +$$ LANGUAGE SQL; + +-- types_are( schema, types[], description ) +CREATE OR REPLACE FUNCTION types_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, $3, NULL ); +$$ LANGUAGE SQL; + +-- types_are( schema, types[] ) +CREATE OR REPLACE FUNCTION types_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, 'Schema ' || quote_ident($1) || ' should have the correct types', NULL ); +$$ LANGUAGE SQL; + +-- types_are( types[], description ) +CREATE OR REPLACE FUNCTION _types_are ( NAME[], TEXT, CHAR[] ) +RETURNS TEXT AS $$ + SELECT _are( + 'types', + ARRAY( + SELECT t.typname + FROM pg_catalog.pg_type t + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE ( + t.typrelid = 0 + OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid) + ) + AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_type_is_visible(t.oid) + AND t.typtype = ANY( COALESCE($3, ARRAY['b', 'c', 'd', 'p', 'e']) ) + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT t.typname + FROM pg_catalog.pg_type t + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE ( + t.typrelid = 0 + OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid) + ) + AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_type_is_visible(t.oid) + AND t.typtype = ANY( COALESCE($3, ARRAY['b', 'c', 'd', 'p', 'e']) ) + ), + $2 + ); +$$ LANGUAGE SQL; + + +-- types_are( types[], description ) +CREATE OR REPLACE FUNCTION types_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, NULL ); +$$ LANGUAGE SQL; + +-- types_are( types[] ) +CREATE OR REPLACE FUNCTION types_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct types', NULL ); +$$ LANGUAGE SQL; + +-- domains_are( schema, domains[], description ) +CREATE OR REPLACE FUNCTION domains_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, $3, ARRAY['d'] ); +$$ LANGUAGE SQL; + +-- domains_are( schema, domains[] ) +CREATE OR REPLACE FUNCTION domains_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, 'Schema ' || quote_ident($1) || ' should have the correct domains', ARRAY['d'] ); +$$ LANGUAGE SQL; + +-- domains_are( domains[], description ) +CREATE OR REPLACE FUNCTION domains_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, ARRAY['d'] ); +$$ LANGUAGE SQL; + +-- domains_are( domains[] ) +CREATE OR REPLACE FUNCTION domains_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct domains', ARRAY['d'] ); +$$ LANGUAGE SQL; + +-- enums_are( schema, enums[], description ) +CREATE OR REPLACE FUNCTION enums_are ( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, $3, ARRAY['e'] ); +$$ LANGUAGE SQL; + +-- enums_are( schema, enums[] ) +CREATE OR REPLACE FUNCTION enums_are ( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, 'Schema ' || quote_ident($1) || ' should have the correct enums', ARRAY['e'] ); +$$ LANGUAGE SQL; + +-- enums_are( enums[], description ) +CREATE OR REPLACE FUNCTION enums_are ( NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, $2, ARRAY['e'] ); +$$ LANGUAGE SQL; + +-- enums_are( enums[] ) +CREATE OR REPLACE FUNCTION enums_are ( NAME[] ) +RETURNS TEXT AS $$ + SELECT _types_are( $1, 'Search path ' || pg_catalog.current_setting('search_path') || ' should have the correct enums', ARRAY['e'] ); +$$ LANGUAGE SQL; + +-- _dexists( schema, domain ) +CREATE OR REPLACE FUNCTION _dexists ( NAME, NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_type t on n.oid = t.typnamespace + WHERE n.nspname = $1 + AND t.typname = $2 + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _dexists ( NAME ) +RETURNS BOOLEAN AS $$ + SELECT EXISTS( + SELECT true + FROM pg_catalog.pg_type t + WHERE t.typname = $1 + AND pg_catalog.pg_type_is_visible(t.oid) + ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _get_dtype( NAME, TEXT, BOOLEAN ) +RETURNS TEXT AS $$ + SELECT display_type(CASE WHEN $3 THEN tn.nspname ELSE NULL END, t.oid, t.typtypmod) + FROM pg_catalog.pg_type d + JOIN pg_catalog.pg_namespace dn ON d.typnamespace = dn.oid + JOIN pg_catalog.pg_type t ON d.typbasetype = t.oid + JOIN pg_catalog.pg_namespace tn ON d.typnamespace = tn.oid + WHERE d.typisdefined + AND dn.nspname = $1 + AND d.typname = LOWER($2) + AND d.typtype = 'd' +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION _get_dtype( NAME ) +RETURNS TEXT AS $$ + SELECT display_type(t.oid, t.typtypmod) + FROM pg_catalog.pg_type d + JOIN pg_catalog.pg_type t ON d.typbasetype = t.oid + WHERE d.typisdefined + AND d.typname = LOWER($1) + AND d.typtype = 'd' +$$ LANGUAGE sql; + +-- domain_type_is( schema, domain, schema, type, description ) +CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + actual_type TEXT := _get_dtype($1, $2, true); +BEGIN + IF actual_type IS NULL THEN + RETURN fail( $5 ) || E'\n' || diag ( + ' Domain ' || quote_ident($1) || '.' || $2 + || ' does not exist' + ); + END IF; + + RETURN is( actual_type, quote_ident($3) || '.' || _quote_ident_like($4, actual_type), $5 ); +END; +$$ LANGUAGE plpgsql; + +-- domain_type_is( schema, domain, schema, type ) +CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT domain_type_is( + $1, $2, $3, $4, + 'Domain ' || quote_ident($1) || '.' || $2 + || ' should extend type ' || quote_ident($3) || '.' || $4 + ); +$$ LANGUAGE SQL; + +-- domain_type_is( schema, domain, type, description ) +CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + actual_type TEXT := _get_dtype($1, $2, false); +BEGIN + IF actual_type IS NULL THEN + RETURN fail( $4 ) || E'\n' || diag ( + ' Domain ' || quote_ident($1) || '.' || $2 + || ' does not exist' + ); + END IF; + + RETURN is( actual_type, _quote_ident_like($3, actual_type), $4 ); +END; +$$ LANGUAGE plpgsql; + +-- domain_type_is( schema, domain, type ) +CREATE OR REPLACE FUNCTION domain_type_is( NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT domain_type_is( + $1, $2, $3, + 'Domain ' || quote_ident($1) || '.' || $2 + || ' should extend type ' || $3 + ); +$$ LANGUAGE SQL; + +-- domain_type_is( domain, type, description ) +CREATE OR REPLACE FUNCTION domain_type_is( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + actual_type TEXT := _get_dtype($1); +BEGIN + IF actual_type IS NULL THEN + RETURN fail( $3 ) || E'\n' || diag ( + ' Domain ' || $1 || ' does not exist' + ); + END IF; + + RETURN is( actual_type, _quote_ident_like($2, actual_type), $3 ); +END; +$$ LANGUAGE plpgsql; + +-- domain_type_is( domain, type ) +CREATE OR REPLACE FUNCTION domain_type_is( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT domain_type_is( + $1, $2, + 'Domain ' || $1 || ' should extend type ' || $2 + ); +$$ LANGUAGE SQL; + +-- domain_type_isnt( schema, domain, schema, type, description ) +CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + actual_type TEXT := _get_dtype($1, $2, true); +BEGIN + IF actual_type IS NULL THEN + RETURN fail( $5 ) || E'\n' || diag ( + ' Domain ' || quote_ident($1) || '.' || $2 + || ' does not exist' + ); + END IF; + + RETURN isnt( actual_type, quote_ident($3) || '.' || _quote_ident_like($4, actual_type), $5 ); +END; +$$ LANGUAGE plpgsql; + +-- domain_type_isnt( schema, domain, schema, type ) +CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, NAME, TEXT ) +RETURNS TEXT AS $$ + SELECT domain_type_isnt( + $1, $2, $3, $4, + 'Domain ' || quote_ident($1) || '.' || $2 + || ' should not extend type ' || quote_ident($3) || '.' || $4 + ); +$$ LANGUAGE SQL; + +-- domain_type_isnt( schema, domain, type, description ) +CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + actual_type TEXT := _get_dtype($1, $2, false); +BEGIN + IF actual_type IS NULL THEN + RETURN fail( $4 ) || E'\n' || diag ( + ' Domain ' || quote_ident($1) || '.' || $2 + || ' does not exist' + ); + END IF; + + RETURN isnt( actual_type, _quote_ident_like($3, actual_type), $4 ); +END; +$$ LANGUAGE plpgsql; + +-- domain_type_isnt( schema, domain, type ) +CREATE OR REPLACE FUNCTION domain_type_isnt( NAME, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT domain_type_isnt( + $1, $2, $3, + 'Domain ' || quote_ident($1) || '.' || $2 + || ' should not extend type ' || $3 + ); +$$ LANGUAGE SQL; + +-- domain_type_isnt( domain, type, description ) +CREATE OR REPLACE FUNCTION domain_type_isnt( TEXT, TEXT, TEXT ) +RETURNS TEXT AS $$ +DECLARE + actual_type TEXT := _get_dtype($1); +BEGIN + IF actual_type IS NULL THEN + RETURN fail( $3 ) || E'\n' || diag ( + ' Domain ' || $1 || ' does not exist' + ); + END IF; + + RETURN isnt( actual_type, _quote_ident_like($2, actual_type), $3 ); +END; +$$ LANGUAGE plpgsql; + +-- domain_type_isnt( domain, type ) +CREATE OR REPLACE FUNCTION domain_type_isnt( TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT domain_type_isnt( + $1, $2, + 'Domain ' || $1 || ' should not extend type ' || $2 + ); +$$ LANGUAGE SQL; + +-- row_eq( sql, record, description ) +CREATE OR REPLACE FUNCTION row_eq( TEXT, anyelement, TEXT ) +RETURNS TEXT AS $$ +DECLARE + rec RECORD; +BEGIN + EXECUTE _query($1) INTO rec; + IF NOT rec IS DISTINCT FROM $2 THEN RETURN ok(true, $3); END IF; + RETURN ok(false, $3 ) || E'\n' || diag( + ' have: ' || CASE WHEN rec IS NULL THEN 'NULL' ELSE rec::text END || + E'\n want: ' || CASE WHEN $2 IS NULL THEN 'NULL' ELSE $2::text END + ); +END; +$$ LANGUAGE plpgsql; + +-- row_eq( sql, record ) +CREATE OR REPLACE FUNCTION row_eq( TEXT, anyelement ) +RETURNS TEXT AS $$ + SELECT row_eq($1, $2, NULL ); +$$ LANGUAGE sql; + +-- triggers_are( schema, table, triggers[], description ) +CREATE OR REPLACE FUNCTION triggers_are( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'triggers', + ARRAY( + SELECT t.tgname + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = $1 + AND c.relname = $2 + EXCEPT + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + ), + ARRAY( + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + EXCEPT + SELECT t.tgname + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = $1 + AND c.relname = $2 + ), + $4 + ); +$$ LANGUAGE SQL; + +-- triggers_are( schema, table, triggers[] ) +CREATE OR REPLACE FUNCTION triggers_are( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT triggers_are( $1, $2, $3, 'Table ' || quote_ident($1) || '.' || quote_ident($2) || ' should have the correct triggers' ); +$$ LANGUAGE SQL; + +-- triggers_are( table, triggers[], description ) +CREATE OR REPLACE FUNCTION triggers_are( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'triggers', + ARRAY( + SELECT t.tgname + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = $1 + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT t.tgname + FROM pg_catalog.pg_trigger t + JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid + JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + ), + $3 + ); +$$ LANGUAGE SQL; + +-- triggers_are( table, triggers[] ) +CREATE OR REPLACE FUNCTION triggers_are( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT triggers_are( $1, $2, 'Table ' || quote_ident($1) || ' should have the correct triggers' ); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION _areni ( text, text[], text[], TEXT ) +RETURNS TEXT AS $$ +DECLARE + what ALIAS FOR $1; + extras ALIAS FOR $2; + missing ALIAS FOR $3; + descr ALIAS FOR $4; + msg TEXT := ''; + res BOOLEAN := TRUE; +BEGIN + IF extras[1] IS NOT NULL THEN + res = FALSE; + msg := E'\n' || diag( + ' Extra ' || what || E':\n ' + || array_to_string( extras, E'\n ' ) + ); + END IF; + IF missing[1] IS NOT NULL THEN + res = FALSE; + msg := msg || E'\n' || diag( + ' Missing ' || what || E':\n ' + || array_to_string( missing, E'\n ' ) + ); + END IF; + + RETURN ok(res, descr) || msg; +END; +$$ LANGUAGE plpgsql; + + +-- casts_are( casts[], description ) +CREATE OR REPLACE FUNCTION casts_are ( TEXT[], TEXT ) +RETURNS TEXT AS $$ + SELECT _areni( + 'casts', + ARRAY( + SELECT display_type(castsource, NULL) || ' AS ' || display_type(casttarget, NULL) + FROM pg_catalog.pg_cast c + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT display_type(castsource, NULL) || ' AS ' || display_type(casttarget, NULL) + FROM pg_catalog.pg_cast c + ), + $2 + ); +$$ LANGUAGE sql; + +-- casts_are( casts[] ) +CREATE OR REPLACE FUNCTION casts_are ( TEXT[] ) +RETURNS TEXT AS $$ + SELECT casts_are( $1, 'There should be the correct casts'); +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION display_oper ( NAME, OID ) +RETURNS TEXT AS $$ + SELECT $1 || substring($2::regoperator::text, '[(][^)]+[)]$') +$$ LANGUAGE SQL; + +-- operators_are( schema, operators[], description ) +CREATE OR REPLACE FUNCTION operators_are( NAME, TEXT[], TEXT ) +RETURNS TEXT AS $$ + SELECT _areni( + 'operators', + ARRAY( + SELECT display_oper(o.oprname, o.oid) || ' RETURNS ' || o.oprresult::regtype + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_namespace n ON o.oprnamespace = n.oid + WHERE n.nspname = $1 + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT display_oper(o.oprname, o.oid) || ' RETURNS ' || o.oprresult::regtype + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_namespace n ON o.oprnamespace = n.oid + WHERE n.nspname = $1 + ), + $3 + ); +$$ LANGUAGE SQL; + +-- operators_are( schema, operators[] ) +CREATE OR REPLACE FUNCTION operators_are ( NAME, TEXT[] ) +RETURNS TEXT AS $$ + SELECT operators_are($1, $2, 'Schema ' || quote_ident($1) || ' should have the correct operators' ); +$$ LANGUAGE SQL; + +-- operators_are( operators[], description ) +CREATE OR REPLACE FUNCTION operators_are( TEXT[], TEXT ) +RETURNS TEXT AS $$ + SELECT _areni( + 'operators', + ARRAY( + SELECT display_oper(o.oprname, o.oid) || ' RETURNS ' || o.oprresult::regtype + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_namespace n ON o.oprnamespace = n.oid + WHERE pg_catalog.pg_operator_is_visible(o.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + EXCEPT + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + ), + ARRAY( + SELECT $1[i] + FROM generate_series(1, array_upper($1, 1)) s(i) + EXCEPT + SELECT display_oper(o.oprname, o.oid) || ' RETURNS ' || o.oprresult::regtype + FROM pg_catalog.pg_operator o + JOIN pg_catalog.pg_namespace n ON o.oprnamespace = n.oid + WHERE pg_catalog.pg_operator_is_visible(o.oid) + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + ), + $2 + ); +$$ LANGUAGE SQL; + +-- operators_are( operators[] ) +CREATE OR REPLACE FUNCTION operators_are ( TEXT[] ) +RETURNS TEXT AS $$ + SELECT operators_are($1, 'There should be the correct operators') +$$ LANGUAGE SQL; + +-- columns_are( schema, table, columns[], description ) +CREATE OR REPLACE FUNCTION columns_are( NAME, NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'columns', + ARRAY( + SELECT a.attname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + EXCEPT + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + ), + ARRAY( + SELECT $3[i] + FROM generate_series(1, array_upper($3, 1)) s(i) + EXCEPT + SELECT a.attname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname = $1 + AND c.relname = $2 + AND a.attnum > 0 + AND NOT a.attisdropped + ), + $4 + ); +$$ LANGUAGE SQL; + +-- columns_are( schema, table, columns[] ) +CREATE OR REPLACE FUNCTION columns_are( NAME, NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT columns_are( $1, $2, $3, 'Table ' || quote_ident($1) || '.' || quote_ident($2) || ' should have the correct columns' ); +$$ LANGUAGE SQL; + +-- columns_are( table, columns[], description ) +CREATE OR REPLACE FUNCTION columns_are( NAME, NAME[], TEXT ) +RETURNS TEXT AS $$ + SELECT _are( + 'columns', + ARRAY( + SELECT a.attname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_table_is_visible(c.oid) + AND c.relname = $1 + AND a.attnum > 0 + AND NOT a.attisdropped + EXCEPT + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + ), + ARRAY( + SELECT $2[i] + FROM generate_series(1, array_upper($2, 1)) s(i) + EXCEPT + SELECT a.attname + FROM pg_catalog.pg_namespace n + JOIN pg_catalog.pg_class c ON n.oid = c.relnamespace + JOIN pg_catalog.pg_attribute a ON c.oid = a.attrelid + WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') + AND pg_catalog.pg_table_is_visible(c.oid) + AND c.relname = $1 + AND a.attnum > 0 + AND NOT a.attisdropped + ), + $3 + ); +$$ LANGUAGE SQL; + +-- columns_are( table, columns[] ) +CREATE OR REPLACE FUNCTION columns_are( NAME, NAME[] ) +RETURNS TEXT AS $$ + SELECT columns_are( $1, $2, 'Table ' || quote_ident($1) || ' should have the correct columns' ); +$$ LANGUAGE SQL; + +-- _get_db_owner( dbname ) +CREATE OR REPLACE FUNCTION _get_db_owner( NAME ) +RETURNS NAME AS $$ + SELECT pg_catalog.pg_get_userbyid(datdba) + FROM pg_catalog.pg_database + WHERE datname = $1; +$$ LANGUAGE SQL; + +-- db_owner_is ( dbname, user, description ) +CREATE OR REPLACE FUNCTION db_owner_is ( NAME, NAME, TEXT ) +RETURNS TEXT AS $$ +DECLARE + dbowner NAME := _get_db_owner($1); +BEGIN + -- Make sure the database exists. + IF dbowner IS NULL THEN + RETURN ok(FALSE, $3) || E'\n' || diag( + E' Database ' || quote_ident($1) || ' does not exist' + ); + END IF; + + RETURN is(dbowner, $2, $3); +END; +$$ LANGUAGE plpgsql; + +-- db_owner_is ( dbname, user ) +CREATE OR REPLACE FUNCTION db_owner_is ( NAME, NAME ) +RETURNS TEXT AS $$ + SELECT db_owner_is( + $1, $2, + 'Database ' || quote_ident($1) || ' should be owned by ' || quote_ident($2) + ); +$$ LANGUAGE sql; + diff --git a/legacyworlds-server-data/db-structure/tests/user/priv/sys/logs.sql b/legacyworlds-server-data/db-structure/tests/user/priv/sys/logs.sql new file mode 100644 index 0000000..e7c7948 --- /dev/null +++ b/legacyworlds-server-data/db-structure/tests/user/priv/sys/logs.sql @@ -0,0 +1,39 @@ +BEGIN; + SELECT plan( 3 ); + + -- + -- Insertion through sys.write_log() + -- + CREATE OR REPLACE FUNCTION _test_this( ) + RETURNS BIGINT + AS $$ + SELECT sys.write_log( 'test' , 'WARNING'::log_level , 'test' ); + $$ LANGUAGE SQL; + SELECT lives_ok( 'SELECT _test_this()' ); + DROP FUNCTION _test_this( ); + + -- + -- Direct insertion must fail + -- + CREATE FUNCTION _test_this( ) + RETURNS VOID + AS $$ + INSERT INTO sys.logs( component , level , message ) + VALUES ( 'test' , 'WARNING'::log_level , 'test' ); + $$ LANGUAGE SQL; + SELECT throws_ok( 'SELECT _test_this()' , 42501 ); + DROP FUNCTION _test_this( ); + + -- + -- Updates must fail + -- + CREATE OR REPLACE FUNCTION _test_this( ) + RETURNS VOID + AS $$ + UPDATE sys.logs SET component = 'random' WHERE component = 'test'; + $$ LANGUAGE SQL; + SELECT throws_ok( 'SELECT _test_this()' , 42501 ); + DROP FUNCTION _test_this( ); + + SELECT * FROM finish( ); +ROLLBACK; diff --git a/legacyworlds-server-main/data-source.xml b/legacyworlds-server-main/data-source.sample.xml similarity index 100% rename from legacyworlds-server-main/data-source.xml rename to legacyworlds-server-main/data-source.sample.xml diff --git a/legacyworlds/doc/TODO.txt b/legacyworlds/doc/TODO.txt index 771e724..f1fc671 100644 --- a/legacyworlds/doc/TODO.txt +++ b/legacyworlds/doc/TODO.txt @@ -12,19 +12,14 @@ PROJECT: SERVER & DATABASE: - - ! Migrate to PostgreSQL 9.1 - -> add logging to some of the bigger stored procedures through an - external connection ! Add some form of database version control to allow easier updates - -> once migrated to Pg9.1, there are some interesting extensions that - may be satisfactory - + -> existing options were investigated, they are unsatisfactory + * Replace all single-precision reals with double precision reals * Add a tool to initialise the database - + * I18N loader: improve text file loading (use relative paths) * Replace current authentication information (pair of hashes) with a @@ -41,6 +36,6 @@ GENERAL: -> that would be "everywhere" * Write unit tests - ? Check out PostgreSQL extensions to test stored procedures - * Write unit tests for new code + * Write unit tests for all new Java code + * Write unit tests for all new SQL code ? add more tests if possible diff --git a/legacyworlds/doc/database.txt b/legacyworlds/doc/database.txt new file mode 100644 index 0000000..b2765e1 --- /dev/null +++ b/legacyworlds/doc/database.txt @@ -0,0 +1,91 @@ +Database structure and development +=================================== + +The database used by the Legacy Worlds server can be found in the db-structure +directory of the legacyworlds-server-data package. + + +Database configuration +----------------------- + +The configuration used by the various SQL scripts must be placed in the +db-config.txt file, at the root of the directory. This file is ignored by Git, +which allows each developer to set up her or his own copy of the database +without hassle. A sample is included in the repository in the +db-config.sample.txt file. + +The configuration file contains a few fields, which are defined using a simple +"=" syntax (warning: there should be no spaces before or after the +'=' sign). + +The following fields must be defined: + +* admin - the name of the administrative user, +* db - the name of the database, +* user - the name of the user through which the server connects to the +database, +* password - the password used by the server to authenticate when accessing +the database. + + +Code structure +--------------- + +The root directory includes a "database.sql" script, which can be launched in +psql to create the database and associated user. + +The parts/ sub-directory contains the various elements of the database's +definition. It contains a few scripts, which will initialise the schemas and +load other scripts from the sub-directories. + + * parts/data/ contains all data structure definitions (types, tables, + indexes, constraints and some of the views). + * parts/functions/ contains all general function definitions; this includes + both functions that are called internally and functions which are called + by the server. It also includes view definitions that depend on functions. + * parts/updates/ contains all functions that implement the game's updates. + + The tests/ sub-directory contains the SQL source code for the pgTAP testing + framework as well as the tests themselves. See below for more information. + + + Unit tests + ---------- + + There may be up to two sub-directories in the tests/ directory. The user/ + sub-directory would contain unit tests that must be executed as the standard + user, while the admin/ directory would contain tests that required + administrative permissions on the database. + + In order to run the database unit tests, the following steps must be taken: + + 1) pg_prove must be installed. This can be achieved by running the following + command as root: + + cpan TAP::Parser::SourceHandler::pgTAP + +2) It must be possible to log on to the database through the local socket as + both the administrative and the standard user, without password. + +3) The database itself must be loaded using the aforementioned database.sql + script. + +4) The tests/pgtap.sql script must be loaded into the database as the + administrative user. + +At this point, it becomes possible to launch the test suites by issuing a +command similar to: + + pg_prove -d $DATABASE -U $USER `find $DIR/ -type f -name '*.sql'` + +where $DATABASE is the name of the database, $USER the name of the user that +will execute the tests and $DIR being either admin or user. + + +Build system +------------- + +The build system will attempt to create the database using the scripts. It will +stop at the first unsuccessful command. On success, it will proceed to loading +pgTAP, then run all available unit tests. A failure will cause the build to be +aborted. \ No newline at end of file