diff --git a/doc/pgtap.mmd b/doc/pgtap.mmd index 8917d9e0..f69d580e 100644 --- a/doc/pgtap.mmd +++ b/doc/pgtap.mmd @@ -1378,10 +1378,12 @@ same! ### `set_eq()` ### - SELECT set_eq( :sql, :sql, :description ); - SELECT set_eq( :sql, :sql ); - SELECT set_eq( :sql, :array, :description ); - SELECT set_eq( :sql, :array ); + SELECT set_eq( :sql, :sql, :description ); + SELECT set_eq( :sql, :sql ); + SELECT set_eq( :sql, :array, :description ); + SELECT set_eq( :sql, :array ); + SELECT set_eq( :array, :array, :description ); + SELECT set_eq( :array, :array ); **Parameters** @@ -5068,6 +5070,30 @@ that do have check constraints, if any: Just like `col_is_pk()`, except that it test that the column or array of columns have a check constraint on them. +### `col_has_exclusion()` ### + + SELECT col_has_check( :schema, :table, :columns, :description ); + +**Parameters** + +`:schema` +: Schema in which to find the table. + +`:table` +: Name of a table containing the exclusion constraint. + +`:columns` +: Array of the names of the exclusion constraint columns. + +`:column` +: Name of the exclusion constraint column. + +`:description` +: A short description of the test. + +Just like `col_is_check()`, except that it tests that the column array has +an exclusion constraint on them. + ### `index_is_unique()` ### SELECT index_is_unique( :schema, :table, :index, :description ); @@ -7960,6 +7986,93 @@ missing policy command, like so: # have: INSERT # want: ALL +### `rls_is_enabled()` ### + + SELECT rls_is_enabled( :schema, :table, :desired_value ); + +Test whether [row-level security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) +is enabled (`:desired_value` true) or disabled (`:desired_value` false). + +**Parameters** + +`:schema` +: Name of a schema in which to find the `:table`. + +`:table` +: Name of a table which has RLS enabled or not. + +`:desired_value` +: `true` to assert that the table has RLS enabled, `false` to assert it's disabled. + +Commentary +---------- + +### `table_comment_has()` ### + + SELECT table_comment_has( :schema, :table, :comment ); + SELECT table_comment_has( :schema, :table, :comment, :description ); + +Assert that a table comment contains a full line of text (delimited by `\n`). + +**Parameters** + +`:schema` +: Name of a schema in which to find the `:table`. + +`:table` +: Name of a table which has a comment set. + +`:comment` +: The line the table comment should include. + +`:description` +: A short description of the test. + +### `column_comment_has()` ### + + SELECT column_comment_has( :schema, :table, :column, :comment ); + SELECT column_comment_has( :schema, :table, :column, :comment, :description ); + +Assert that a comment comment contains a full line of text (delimited by `\n`). + +**Parameters** + +`:schema` +: Name of a schema in which to find the `:table`. + +`:table` +: Name of a table including `:column`. + +`:column` +: Name of a column which has a comment set. + +`:comment` +: The line the column comment should include. + +`:description` +: A short description of the test. + +### `function_comment_has()` ### + + SELECT function_comment_has( :schema, :function, :comment ); + SELECT function_comment_has( :schema, :function, :comment, :description ); + +Assert that a function comment contains a full line of text (delimited by `\n`). + +**Parameters** + +`:schema` +: Name of a schema in which to find the `:function`. + +`:function` +: Name of a function which has RLS enabled or not. + +`:comment` +: The line the function comment should include. + +`:description` +: A short description of the test. + No Test for the Wicked ====================== diff --git a/sql/pgtap--1.2.0--1.2.1.sql b/sql/pgtap--1.2.0--1.2.1.sql index 26bd6a6d..012ada66 100644 --- a/sql/pgtap--1.2.0--1.2.1.sql +++ b/sql/pgtap--1.2.0--1.2.1.sql @@ -155,3 +155,99 @@ CREATE OR REPLACE FUNCTION col_is_pk ( NAME, NAME, NAME ) RETURNS TEXT AS $$ SELECT col_is_pk( $1, $2, $3, 'Column ' || quote_ident($1) || '.' || quote_ident($2) || '(' || quote_ident($3) || ') should be a primary key' ); $$ LANGUAGE sql; + +-- col_has_exclusion(schema, table, columns, description) +CREATE OR REPLACE FUNCTION col_has_exclusion(TEXT, TEXT, TEXT[], TEXT) +RETURNS TEXT AS $$ + SELECT ok(array_agg(attr.attname)::TEXT[] @> $3 AND $3 @> array_agg(attr.attname)::TEXT[]) + FROM pg_constraint AS con + JOIN LATERAL unnest(con.conkey) AS attnums (num) ON TRUE + JOIN pg_attribute AS attr ON attr.attrelid = con.conrelid + AND attr.attnum = attnums.num + WHERE conrelid = format('%1$I.%2$I', $1, $2)::regclass + AND contype = 'x'; +$$ LANGUAGE sql; + +-- _relcomp array-to-array +CREATE OR REPLACE FUNCTION _relcomp( anyarray, anyarray, TEXT, TEXT ) +RETURNS TEXT AS $$ + SELECT _docomp( + _temptable( $1, '__taphave__' ), + _temptable( $2, '__tapwant__' ), + $3, $4 + ); +$$ LANGUAGE sql; + +-- set_eq( array, array, description ) +CREATE OR REPLACE FUNCTION set_eq(anyarray, anyarray, TEXT) +RETURNS TEXT AS $$ + SELECT _relcomp($1, $2, $3); +$$ LANGUAGE sql; + +-- set_eq( array, array ) +CREATE OR REPLACE FUNCTION set_eq(anyarray, anyarray) +RETURNS TEXT AS $$ + SELECT _relcomp($1, $2, '') +$$ LANGUAGE sql; + +-- table_comment_has(schema, table, comment, description) +CREATE OR REPLACE FUNCTION table_comment_has(TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT ok(COUNT(*) >= 1, $4) + FROM pg_description + JOIN LATERAL regexp_split_to_table(description, '\n') AS lines (line) ON TRUE + WHERE objoid = format('%1$I.%2$I', $1, $2)::regclass + AND objsubid = 0 + AND trim(line) ILIKE $3 +$$ LANGUAGE sql; + +-- table_comment_has(schema, table, comment) +CREATE OR REPLACE FUNCTION table_comment_has(TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT table_comment_has($1, $2, $3, 'table comment contains expected line'); +$$ LANGUAGE sql; + +-- column_comment_has(schema, table, column, comment, description) +CREATE OR REPLACE FUNCTION column_comment_has(TEXT, TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT ok(COUNT(*) >= 1, $5) + FROM pg_description + JOIN pg_attribute AS attr + ON attr.attrelid = pg_description.objoid + AND attr.attnum = pg_description.objsubid + JOIN LATERAL regexp_split_to_table(description, '\n') AS lines (line) ON TRUE + WHERE objoid = format('%1$I.%2$I', $1, $2)::regclass + AND attr.attname = $3::name + AND trim(line) ILIKE $4 +$$ LANGUAGE sql; + +-- column_comment_has(schema, table, column, comment) +CREATE OR REPLACE FUNCTION column_comment_has(TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT column_comment_has($1, $2, $3, $4, 'column comment contains expected line'); +$$ LANGUAGE sql; + +-- function_comment_has(schema, function, comment, description) +CREATE OR REPLACE FUNCTION function_comment_has(TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT ok(COUNT(*) >= 1, $4) + FROM pg_description + JOIN LATERAL regexp_split_to_table(description, '\n') AS lines (line) ON TRUE + WHERE objoid = format('%1$I.%2$I', $1, $2)::regproc + AND objsubid = 0 + AND trim(line) ILIKE $3 +$$ LANGUAGE sql; + +-- function_comment_has(schema, function, comment) +CREATE OR REPLACE FUNCTION function_comment_has(TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT function_comment_has($1, $2, $3, 'function comment contains expected line'); +$$ LANGUAGE sql; + +-- rls_is_enabled(schema, table, desired_value) +CREATE OR REPLACE FUNCTION rls_is_enabled(TEXT, TEXT, BOOLEAN) +RETURNS TEXT AS $$ + SELECT ok(relrowsecurity IS NOT DISTINCT FROM $3) + FROM pg_class + WHERE oid = format('%1$I.%2$I', $1, $2)::regclass +$$ LANGUAGE sql; diff --git a/sql/pgtap.sql.in b/sql/pgtap.sql.in index 9370c511..e4754a41 100644 --- a/sql/pgtap.sql.in +++ b/sql/pgtap.sql.in @@ -2418,6 +2418,18 @@ RETURNS TEXT AS $$ SELECT col_has_check( $1, $2, 'Column ' || quote_ident($1) || '(' || quote_ident($2) || ') should have a check constraint' ); $$ LANGUAGE sql; +-- col_has_exclusion(schema, table, columns, description) +CREATE OR REPLACE FUNCTION col_has_exclusion(TEXT, TEXT, TEXT[], TEXT) +RETURNS TEXT AS $$ + SELECT ok(array_agg(attr.attname)::TEXT[] @> $3 AND $3 @> array_agg(attr.attname)::TEXT[]) + FROM pg_constraint AS con + JOIN LATERAL unnest(con.conkey) AS attnums (num) ON TRUE + JOIN pg_attribute AS attr ON attr.attrelid = con.conrelid + AND attr.attnum = attnums.num + WHERE conrelid = format('%1$I.%2$I', $1, $2)::regclass + AND contype = 'x'; +$$ 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 $$ @@ -6840,6 +6852,15 @@ RETURNS TEXT AS $$ ); $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION _relcomp( anyarray, 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 $$ @@ -6864,6 +6885,18 @@ RETURNS TEXT AS $$ SELECT _relcomp( $1, $2, NULL::text, '' ); $$ LANGUAGE sql; +-- set_eq( array, array, description ) +CREATE OR REPLACE FUNCTION set_eq(anyarray, anyarray, TEXT) +RETURNS TEXT AS $$ + SELECT _relcomp( $1, $2, $3, '' ); +$$ LANGUAGE sql; + +-- set_eq( array, array ) +CREATE OR REPLACE FUNCTION set_eq(anyarray, 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 $$ @@ -11366,3 +11399,65 @@ RETURNS TEXT AS $$ 'Function ' || quote_ident($1) || '() should not be a procedure' ); $$ LANGUAGE sql; + +-- table_comment_has(schema, table, comment, description) +CREATE OR REPLACE FUNCTION table_comment_has(TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT ok(COUNT(*) >= 1, $4) + FROM pg_description + JOIN LATERAL regexp_split_to_table(description, '\n') AS lines (line) ON TRUE + WHERE objoid = format('%1$I.%2$I', $1, $2)::regclass + AND objsubid = 0 + AND trim(line) ILIKE $3 +$$ LANGUAGE sql; + +-- table_comment_has(schema, table, comment) +CREATE OR REPLACE FUNCTION table_comment_has(TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT table_comment_has($1, $2, $3, 'table comment contains expected line'); +$$ LANGUAGE sql; + +-- column_comment_has(schema, table, column, comment, description) +CREATE OR REPLACE FUNCTION column_comment_has(TEXT, TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT ok(COUNT(*) >= 1, $5) + FROM pg_description + JOIN pg_attribute AS attr + ON attr.attrelid = pg_description.objoid + AND attr.attnum = pg_description.objsubid + JOIN LATERAL regexp_split_to_table(description, '\n') AS lines (line) ON TRUE + WHERE objoid = format('%1$I.%2$I', $1, $2)::regclass + AND attr.attname = $3::name + AND trim(line) ILIKE $4 +$$ LANGUAGE sql; + +-- column_comment_has(schema, table, column, comment) +CREATE OR REPLACE FUNCTION column_comment_has(TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT column_comment_has($1, $2, $3, $4, 'column comment contains expected line'); +$$ LANGUAGE sql; + +-- function_comment_has(schema, function, comment, description) +CREATE OR REPLACE FUNCTION function_comment_has(TEXT, TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT ok(COUNT(*) >= 1, $4) + FROM pg_description + JOIN LATERAL regexp_split_to_table(description, '\n') AS lines (line) ON TRUE + WHERE objoid = format('%1$I.%2$I', $1, $2)::regproc + AND objsubid = 0 + AND trim(line) ILIKE $3 +$$ LANGUAGE sql; + +-- function_comment_has(schema, function, comment) +CREATE OR REPLACE FUNCTION function_comment_has(TEXT, TEXT, TEXT) +RETURNS TEXT AS $$ + SELECT function_comment_has($1, $2, $3, 'function comment contains expected line'); +$$ LANGUAGE sql; + +-- rls_is_enabled(schema, table, desired_value) +CREATE OR REPLACE FUNCTION rls_is_enabled(TEXT, TEXT, BOOLEAN) +RETURNS TEXT AS $$ + SELECT ok(relrowsecurity IS NOT DISTINCT FROM $3) + FROM pg_class + WHERE oid = format('%1$I.%2$I', $1, $2)::regclass +$$ LANGUAGE sql; diff --git a/test/sql/resultset.sql b/test/sql/resultset.sql index 7a570ff8..1e30a3bf 100644 --- a/test/sql/resultset.sql +++ b/test/sql/resultset.sql @@ -1,7 +1,7 @@ \unset ECHO \i test/setup.sql -SELECT plan(545); +SELECT plan(560); --SELECT * FROM no_plan(); -- This will be rolled back. :-) @@ -1672,6 +1672,68 @@ SELECT * FROM check_test( want: (text)' ); +/****************************************************************************/ +-- Test set_eq() with dual arrays. +SELECT * FROM check_test( + set_eq( + ARRAY['Angel', 'Andrea', 'Angelina', 'Antonio', 'Anthony', 'Anna', 'Andrew' ], + ARRAY['Andrew', 'Anna', 'Anthony', 'Antonio', 'Angelina', 'Andrea', 'Angel' ], + 'whatever' + ), + true, + 'set_eq(array, array, desc)', + 'whatever', + '' +); + +SELECT * FROM check_test( + set_eq( + ARRAY['Angel', 'Andrea', 'Angelina', 'Antonio', 'Anthony', 'Anna', 'Andrew' ], + ARRAY['Andrew', 'Anna', 'Anthony', 'Antonio', 'Angelina', 'Andrea', 'Angel' ] + ), + true, + 'set_eq(array, array)', + '', + '' +); + +SELECT * FROM check_test( + set_eq( + ARRAY['Angel', 'Andrea', 'Angelina', 'Antonio', 'Anthony', 'Anna', 'Andrew' ], + ARRAY['Andrew', 'Anna', 'Anthony', 'Antonio', 'Angelina', 'Andrea', 'Angel', 'Andrew', 'Anna' ] + ), + true, + 'set_eq(array, dupe array)', + '', + '' +); + +-- Fail with an extra record. +SELECT * FROM check_test( + set_eq( + ARRAY['Angel', 'Andrea', 'Angelina', 'Antonio', 'Anthony', 'Anna', 'Andrew' ], + ARRAY['Andrew', 'Anna', 'Antonio', 'Angelina', 'Andrea', 'Angel' ] + ), + false, + 'set_eq(array, array) extra record', + '', + ' Extra records: + (Anthony)' +); + +-- Fail with a missing record. +SELECT * FROM check_test( + set_eq( + ARRAY['Angel', 'Andrea', 'Angelina', 'Antonio', 'Anthony', 'Anna', 'Andrew' ], + ARRAY['Andrew', 'Anna', 'Anthony', 'Alan', 'Antonio', 'Angelina', 'Andrea', 'Angel' ] + ), + false, + 'set_eq(array, array) missing record', + '', + ' Missing records: + (Alan)' +); + /****************************************************************************/ -- Test bag_eq() with an array argument. SELECT * FROM check_test(