diff --git a/docs/userguide/advanced_data_comparison.md b/docs/userguide/advanced_data_comparison.md index e1e5ee36c..c776f8596 100644 --- a/docs/userguide/advanced_data_comparison.md +++ b/docs/userguide/advanced_data_comparison.md @@ -7,7 +7,7 @@ utPLSQL expectations incorporates advanced data comparison options when comparin - nested table and varray Advanced data-comparison options are available for the [`equal`](expectations.md#equal) matcher. - + ## Syntax ``` @@ -23,6 +23,9 @@ Advanced data-comparison options are available for the [`equal`](expectations.md - `exclude(a_items varchar2)` - item or comma separated list of items to exclude - `include(a_items ut_varchar2_list)` - table of items to include - `exclude(a_items ut_varchar2_list)` - table of items to exclude + - `unordered` - perform compare on unordered set of data, return only missing or actual + - `join_by(a_columns varchar2)` - columns or comma seperated list of columns to join two cursors by + - `join_by(a_columns ut_varchar2_list)` - table of columns to join two cursors by Each item in the comma separated list can be: - a column name of cursor to be compared @@ -55,7 +58,7 @@ Columns 'ignore_me' and "ADate" will get excluded from cursor comparison. The cursor data is equal, when those columns are excluded. This option is useful in scenarios, when you need to exclude incomparable/unpredictable column data like CREATE_DATE of a record that is maintained by default value on a table column. - + ## Selecting columns for data comparison Consider the following example @@ -92,6 +95,86 @@ Only the columns 'RN', "A_Column" will be compared. Column 'SOME_COL' is exclude This option can be useful in scenarios where you need to narrow-down the scope of test so that the test is only focused on very specific data. +##Unordered + +Unordered option allows for quick comparison of two cursors without need of ordering them in any way. + +Result of such comparison will be limited to only information about row existing or not existing in given set without actual information about exact differences. + + + +```sql +procedure unordered_tst is + l_actual sys_refcursor; + l_expected sys_refcursor; +begin + open l_expected for select username, user_id from all_users + union all + select 'TEST' username, -600 user_id from dual + order by 1 desc; + open l_actual for select username, user_id from all_users + union all + select 'TEST' username, -610 user_id from dual + order by 1 asc; + ut.expect( l_actual ).to_equal( l_expected ).unordered; +end; +``` + + + +Above test will result in two differences of one row extra and one row missing. + + + +```sql + Diff: + Rows: [ 2 differences ] + Missing: TEST-600 + Extra: TEST-610 +``` + + + + + +## Join By option + +You can now join two cursors by defining a primary key or composite key that will be used to uniquely identify and compare rows. This option allows us to exactly show which rows are missing, extra and which are different without ordering clause. In the situation where the join key is not unique, join will partition set over rows with a same key and join on row number as well as given join key. The extra rows or missing will be presented to user as well as not matching rows. + +Join by option can be used in conjunction with include or exclude options. However if any of the join keys is part of exclude set, comparison will fail and report to user that sets could not be joined on specific key (excluded). + +Join by options currently doesn't support nested table inside cursor. + +```sql +procedure join_by_username is + l_actual sys_refcursor; + l_expected sys_refcursor; +begin + open l_expected for select username, user_id from all_users + union all + select 'TEST' username, -600 user_id from dual + order by 1 desc; + open l_actual for select username, user_id from all_users + union all + select 'TEST' username, -610 user_id from dual + order by 1 asc; + ut.expect( l_actual ).to_equal( l_expected ).join_by('USERNAME'); +end; +``` +This will show you difference in row 'TEST' regardless of order. + +```sql + Rows: [ 1 differences ] + PK TEST - Expected: -600 + PK TEST - Actual: -610 +``` + +Assumption is that join by is made by column name so that what will be displayed as part of results. + + + +**Please note that .join_by option will take longer to process due to need of parsing via primary keys.** + ## Defining item as XPath When using XPath expression, keep in mind the following: @@ -109,4 +192,4 @@ begin open l_actual for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL, a.* from all_objects a where rownum < 4; ut.expect( l_actual ).to_equal( l_expected ).include( '/ROW/RN|/ROW/A_Column|/ROW/SOME_COL' ); end; -``` +``` diff --git a/source/core/types/ut_key_value_pair.tps b/source/core/types/ut_key_value_pair.tps index 45bc53157..a23df4a7b 100644 --- a/source/core/types/ut_key_value_pair.tps +++ b/source/core/types/ut_key_value_pair.tps @@ -1,4 +1,4 @@ -create or replace type ut_key_value_pair as object( +create or replace type ut_key_value_pair force as object( /* utPLSQL - Version 3 Copyright 2016 - 2017 utPLSQL Project diff --git a/source/create_synonyms_and_grants_for_public.sql b/source/create_synonyms_and_grants_for_public.sql index 235c6806a..e34df182e 100644 --- a/source/create_synonyms_and_grants_for_public.sql +++ b/source/create_synonyms_and_grants_for_public.sql @@ -142,8 +142,6 @@ create public synonym ut_file_mapping for &&ut3_owner..ut_file_mapping; create public synonym ut_file_mapper for &&ut3_owner..ut_file_mapper; create public synonym ut_key_value_pairs for &&ut3_owner..ut_key_value_pairs; create public synonym ut_key_value_pair for &&ut3_owner..ut_key_value_pair; -create public synonym ut_compound_data_tmp for &&ut3_owner..ut_compound_data_tmp; -create public synonym ut_compound_data_diff_tmp for &&ut3_owner..ut_compound_data_diff_tmp; create public synonym ut_sonar_test_reporter for &&ut3_owner..ut_sonar_test_reporter; begin $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then diff --git a/source/create_user_synonyms.sql b/source/create_user_synonyms.sql index 729ba4002..5e15c7ded 100644 --- a/source/create_user_synonyms.sql +++ b/source/create_user_synonyms.sql @@ -95,7 +95,6 @@ create or replace synonym &ut3_user..ut_file_mapper for &&ut3_owner..ut_file_map create or replace synonym &ut3_user..ut_key_value_pairs for &&ut3_owner..ut_key_value_pairs; create or replace synonym &ut3_user..ut_key_value_pair for &&ut3_owner..ut_key_value_pair; create or replace synonym &ut3_user..ut_compound_data_tmp for &&ut3_owner..ut_cursor_data; -create or replace synonym &ut3_user..ut_compound_data_diff_tmp for &&ut3_owner..ut_compound_data_diff_tmp; create or replace synonym &ut3_user..ut_sonar_test_reporter for &&ut3_owner..ut_sonar_test_reporter; begin $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then diff --git a/source/expectations/data_values/ut_compound_data_diff_tmp.sql b/source/expectations/data_values/ut_compound_data_diff_tmp.sql index 881cc8082..27f95907b 100644 --- a/source/expectations/data_values/ut_compound_data_diff_tmp.sql +++ b/source/expectations/data_values/ut_compound_data_diff_tmp.sql @@ -14,5 +14,13 @@ create global temporary table ut_compound_data_diff_tmp( */ diff_id raw(128), item_no integer, - constraint ut_compound_data_diff_tmp_pk primary key(diff_id,item_no) + pk_hash raw(128), + item_hash raw(128), + duplicate_no integer, + constraint ut_compound_data_diff_tmp_uk1 unique (diff_id,duplicate_no,item_no,item_hash, pk_hash), + constraint ut_compound_data_diff_tmp_chk check( + item_no is not null and pk_hash is null and duplicate_no is null + or item_no is null and item_hash is not null and duplicate_no is not null + or item_no is null and pk_hash is not null and duplicate_no is not null + ) ) on commit preserve rows; diff --git a/source/expectations/data_values/ut_compound_data_helper.pkb b/source/expectations/data_values/ut_compound_data_helper.pkb index a69741aa8..0e8eba809 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pkb +++ b/source/expectations/data_values/ut_compound_data_helper.pkb @@ -16,55 +16,25 @@ create or replace package body ut_compound_data_helper is limitations under the License. */ - type t_type_name_map is table of varchar2(100) index by binary_integer; - g_type_name_map t_type_name_map; - - function get_column_type(a_desc_rec dbms_sql.desc_rec3) return varchar2 is - l_result varchar2(500) := 'unknown datatype'; - begin - if g_type_name_map.exists(a_desc_rec.col_type) then - l_result := g_type_name_map(a_desc_rec.col_type); - elsif a_desc_rec.col_schema_name is not null and a_desc_rec.col_type_name is not null then - l_result := a_desc_rec.col_schema_name||'.'||a_desc_rec.col_type_name; - end if; - return l_result; - end; - - function get_columns_info(a_columns_tab dbms_sql.desc_tab3, a_columns_count integer) return ut_key_value_pairs is - l_result ut_key_value_pairs := ut_key_value_pairs(); - begin - for i in 1 .. a_columns_count loop - l_result.extend; - l_result(l_result.last) := ut_key_value_pair(a_columns_tab(i).col_name, get_column_type(a_columns_tab(i))); - end loop; - return l_result; - end; - - function get_columns_info(a_cursor in out nocopy sys_refcursor) return xmltype is - l_cursor_number integer; - l_columns_count pls_integer; - l_columns_desc dbms_sql.desc_tab3; - l_result xmltype; - l_columns_tab ut_key_value_pairs; - begin - if a_cursor is null or not a_cursor%isopen then - return null; - end if; - l_cursor_number := dbms_sql.to_cursor_number( a_cursor ); - dbms_sql.describe_columns3( l_cursor_number, l_columns_count, l_columns_desc ); - a_cursor := dbms_sql.to_refcursor( l_cursor_number ); - l_columns_tab := get_columns_info( l_columns_desc, l_columns_count); - - select - XMLELEMENT("ROW", xmlagg(xmlelement(evalname ut_utils.xmlgen_escaped_string(key), - XMLATTRIBUTES(key AS "xml_valid_name"), - value))) - into l_result - from table(l_columns_tab ); - - return l_result; - end; - + g_user_defined_type pls_integer := dbms_sql.user_defined_type; + + function get_column_info_xml(a_column_details ut_key_anyval_pair) return xmltype is + l_result varchar2(4000); + l_res xmltype; + l_data ut_data_value := a_column_details.value; + l_key varchar2(4000) := ut_utils.xmlgen_escaped_string(a_column_details.KEY); + begin + l_result := '<'||l_key||' xml_valid_name="'||l_key||'">'; + if l_data is of(ut_data_value_xmltype) then + l_result := l_result || (treat(l_data as ut_data_value_xmltype).to_string); + else + l_result := l_result || ut_utils.xmlgen_escaped_string((treat(l_data as ut_data_value_varchar2).data_value)); + end if; + + l_result := l_result ||''; + return xmltype(l_result); + end; + function get_columns_filter( a_exclude_xpath varchar2, a_include_xpath varchar2, a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data' @@ -86,6 +56,32 @@ create or replace package body ut_compound_data_helper is end if; return l_filter; end; + + /** + * Current get column filter shaving off ROW tag during extract, this not working well with include and XMLTABLE option + * so when there is extract we artificially inject removed tag + **/ + function get_columns_row_filter( + a_exclude_xpath varchar2, a_include_xpath varchar2, + a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data' + ) return varchar2 is + l_filter varchar2(32767); + l_source_column varchar2(500) := a_table_alias||'.'||a_column_alias; + begin + -- this SQL statement is constructed in a way that we always get the same number and ordering of substitution variables + -- That is, we always get: l_exclude_xpath, l_include_xpath + -- regardless if the variables are NULL (not to be used) or NOT NULL and will be used for filtering + if a_exclude_xpath is null and a_include_xpath is null then + l_filter := ':l_exclude_xpath, :l_include_xpath, '||l_source_column||' as '||a_column_alias; + elsif a_exclude_xpath is not null and a_include_xpath is null then + l_filter := 'deletexml( '||l_source_column||', :l_exclude_xpath ) as '||a_column_alias||', :l_include_xpath'; + elsif a_exclude_xpath is null and a_include_xpath is not null then + l_filter := ':l_exclude_xpath, xmlelement("ROW",extract( '||l_source_column||', :l_include_xpath )) as '||a_column_alias; + elsif a_exclude_xpath is not null and a_include_xpath is not null then + l_filter := 'xmlelement("ROW",extract( deletexml( '||l_source_column||', :l_exclude_xpath ), :l_include_xpath )) as '||a_column_alias; + end if; + return l_filter; + end; function get_columns_diff( a_expected xmltype, a_actual xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2 @@ -94,7 +90,7 @@ create or replace package body ut_compound_data_helper is l_sql varchar2(32767); l_results tt_column_diffs; begin - l_column_filter := get_columns_filter(a_exclude_xpath, a_include_xpath); + l_column_filter := get_columns_row_filter(a_exclude_xpath, a_include_xpath); l_sql := q'[ with expected_cols as ( select :a_expected as item_data from dual ), @@ -157,6 +153,135 @@ create or replace package body ut_compound_data_helper is execute immediate l_sql bulk collect into l_results using a_expected, a_actual, a_exclude_xpath, a_include_xpath, a_exclude_xpath, a_include_xpath; + + return l_results; + end; + + function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob is + l_pk_value clob; + begin + select replace((extract(a_item_data,a_join_by_xpath).getclobval()),chr(10)) into l_pk_value from dual; + return l_pk_value; + exception when no_data_found then + return 'null'; + end; + + function get_rows_diff( + a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, + a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2, + a_join_by_xpath varchar2 + ) return tt_row_diffs is + l_column_filter varchar2(32767); + l_results tt_row_diffs; + begin + l_column_filter := get_columns_row_filter(a_exclude_xpath,a_include_xpath); + + /** + * Since its unordered search we cannot select max rows from diffs as we miss some comparision records + * We will restrict output on higher level of select + * NO_MERGE hint was introduced to prevent optimizer from merging views and rewriting query which in some cases + * lead to second value being null depend on execution plan that been chosen + **/ + execute immediate q'[ + with diff_info as (select item_hash,pk_hash,duplicate_no from ut_compound_data_diff_tmp ucdc where diff_id = :diff_guid) + select rn,diff_type,diffed_row,pk_value from + ( + select + diff_type, diffed_row, + dense_rank() over (order by case when diff_type in ('Extra','Missing') then diff_type end, + case when diff_type in ('Actual','Expected') then pk_hash end, + case when diff_type in ('Extra','Missing') then pk_hash end, + case when diff_type in ('Actual','Expected') then diff_type end) rn, + pk_value, pk_hash + from + ( + select diff_type,diffed_row,pk_hash,pk_value from + (select diff_type,data_item diffed_row,pk_hash,pk_value + from + (select /*+NO_MERGE*/ nvl(exp.pk_hash, act.pk_hash) pk_hash,nvl(exp.pk_value, act.pk_value) pk_value, + xmlserialize(content exp.row_data no indent) exp_item, + xmlserialize(content act.row_data no indent) act_item + from + (select ucd.* + from + (select ucd.column_value row_data, + r.item_hash row_hash, + r.pk_hash , + r.duplicate_no, + ucd.column_value.getclobval() col_val, + ucd.column_value.getRootElement() col_name, + ut_compound_data_helper.get_pk_value(:join_xpath,r.item_data) pk_value + from + (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_hash, i.pk_hash, i.duplicate_no + from ut_compound_data_tmp ucd, + diff_info i + where ucd.data_id = :self_guid + and ucd.item_hash = i.item_hash + ) r, + table( xmlsequence( extract(r.item_data,'/*/*') ) ) ucd + ) ucd + ) exp + join ( + select ucd.* + from + (select ucd.column_value row_data, + r.item_hash row_hash, + r.pk_hash , + r.duplicate_no, + ucd.column_value.getclobval() col_val, + ucd.column_value.getRootElement() col_name, + ut_compound_data_helper.get_pk_value(:join_xpath,r.item_data) pk_value + from + (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_hash, i.pk_hash, i.duplicate_no + from ut_compound_data_tmp ucd, + diff_info i + where ucd.data_id = :other_guid + and ucd.item_hash = i.item_hash + ) r, + table( xmlsequence( extract(r.item_data,'/*/*') ) ) ucd + ) ucd + ) act + on exp.pk_hash = act.pk_hash and exp.col_name = act.col_name + and exp.duplicate_no = act.duplicate_no + where dbms_lob.compare(exp.col_val, act.col_val) != 0 + ) + unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') ) + ) + union all + select case when exp.pk_hash is null then 'Extra' else 'Missing' end as diff_type, + xmlserialize(content nvl(exp.item_data, act.item_data) no indent) diffed_row, + coalesce(exp.pk_hash,act.pk_hash) pk_hash, + coalesce(exp.pk_value,act.pk_value) pk_value + from (select extract(deletexml(ucd.item_data, :join_by),'/*/*') item_data,i.pk_hash, + ut_compound_data_helper.get_pk_value(:join_by,item_data) pk_value + from ut_compound_data_tmp ucd, + diff_info i + where ucd.data_id = :self_guid + and ucd.item_hash = i.item_hash + ) exp + full outer join ( + select extract(deletexml(ucd.item_data, :join_by),'/*/*') item_data,i.pk_hash, + ut_compound_data_helper.get_pk_value(:join_by,item_data) pk_value + from ut_compound_data_tmp ucd, + diff_info i + where ucd.data_id = :other_guid + and ucd.item_hash = i.item_hash + )act + on exp.pk_hash = act.pk_hash + where exp.pk_hash is null or act.pk_hash is null + ) + ) where rn <= :max_rows + order by rn, pk_hash, diff_type + ]' + bulk collect into l_results + using a_diff_id, + a_join_by_xpath, + a_exclude_xpath, a_include_xpath, a_expected_dataset_guid, + a_join_by_xpath, + a_exclude_xpath, a_include_xpath, a_actual_dataset_guid, + a_join_by_xpath,a_join_by_xpath,a_expected_dataset_guid,a_join_by_xpath,a_join_by_xpath, a_actual_dataset_guid, + a_max_rows; + return l_results; end; @@ -170,9 +295,12 @@ create or replace package body ut_compound_data_helper is l_column_filter := get_columns_filter(a_exclude_xpath,a_include_xpath); execute immediate q'[ with - diff_info as (select item_no from ut_compound_data_diff_tmp ucdc where diff_id = :diff_guid and rownum <= :max_rows) + diff_info as ( select item_no + from + (select item_no from ut_compound_data_diff_tmp ucdc where diff_id = :diff_guid order by item_no asc) + where rownum <= :max_rows) select * - from (select rn, diff_type, xmlserialize(content data_item no indent) diffed_row + from (select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value from (select nvl(exp.rn, act.rn) rn, xmlagg(exp.col order by exp.col_no) exp_item, xmlagg(act.col order by act.col_no) act_item @@ -180,9 +308,10 @@ create or replace package body ut_compound_data_helper is s.column_value.getRootElement() col_name, s.column_value.getclobval() col_val from (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_data item_data_no_filter - from ut_compound_data_tmp ucd + from ut_compound_data_tmp ucd, + diff_info i where ucd.data_id = :self_guid - and ucd.item_no in (select i.item_no from diff_info i) + and ucd.item_no = i.item_no ) r, table( xmlsequence( extract(r.item_data,'/*/*') ) ) s ) exp @@ -191,9 +320,10 @@ create or replace package body ut_compound_data_helper is s.column_value.getRootElement() col_name, s.column_value.getclobval() col_val from (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_data item_data_no_filter - from ut_compound_data_tmp ucd + from ut_compound_data_tmp ucd, + diff_info i where ucd.data_id = :other_guid - and ucd.item_no in (select i.item_no from diff_info i) + and ucd.item_no = i.item_no ) r, table( xmlsequence( extract(r.item_data,'/*/*') ) ) s ) act @@ -206,7 +336,8 @@ create or replace package body ut_compound_data_helper is union all select nvl(exp.item_no, act.item_no) rn, case when exp.item_no is null then 'Extra:' else 'Missing:' end as diff_type, - xmlserialize(content nvl(exp.item_data, act.item_data) no indent) diffed_row + xmlserialize(content nvl(exp.item_data, act.item_data) no indent) diffed_row, + null pk_value from (select ucd.item_no, extract(ucd.item_data,'/*/*') item_data from ut_compound_data_tmp ucd where ucd.data_id = :self_guid @@ -217,12 +348,10 @@ create or replace package body ut_compound_data_helper is from ut_compound_data_tmp ucd where ucd.data_id = :other_guid and ucd.item_no in (select i.item_no from diff_info i) - )act on exp.item_no = act.item_no where exp.item_no is null or act.item_no is null - order by 1, 2 -]' + order by 1, 2]' bulk collect into l_results using a_diff_id, a_max_rows, a_exclude_xpath, a_include_xpath, a_expected_dataset_guid, @@ -231,6 +360,116 @@ create or replace package body ut_compound_data_helper is return l_results; end; + function get_rows_diff_unordered( + a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, + a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2 + ) return tt_row_diffs is + l_column_filter varchar2(32767); + l_results tt_row_diffs; + begin + l_column_filter := get_columns_filter(a_exclude_xpath,a_include_xpath); + + /** + * Since its unordered search we cannot select max rows from diffs as we miss some comparision records + * We will restrict output on higher level of select + */ + + execute immediate q'[with + diff_info as (select item_hash,duplicate_no from ut_compound_data_diff_tmp ucdc where diff_id = :diff_guid) + select duplicate_no, + diffed_type, + diffed_row, + null pk_value + from + (select + coalesce(exp.duplicate_no,act.duplicate_no) duplicate_no, + case + when act.row_hash is null then + 'Missing:' + else 'Extra:' + end diffed_type, + case when exp.row_hash is null then + xmlserialize(content act.row_data no indent) + when act.row_hash is null then + xmlserialize(content exp.row_data no indent) + end diffed_row + from (select ucd.* + from (select ucd.column_value row_data, + r.item_hash row_hash, + r.duplicate_no + from (select ]'||l_column_filter||q'[, ucd.item_no, i.item_hash, i.duplicate_no + from ut_compound_data_tmp ucd, + diff_info i + where ucd.data_id = :self_guid + and ucd.item_hash = i.item_hash + ) r, + table( xmlsequence( extract(r.item_data,'/*') ) ) ucd + ) ucd + ) exp + full outer join + (select ucd.* + from (select ucd.column_value row_data, + r.item_hash row_hash, + r.duplicate_no + from (select ]'||l_column_filter||q'[, ucd.item_no, i.item_hash, i.duplicate_no + from ut_compound_data_tmp ucd, + diff_info i + where ucd.data_id = :other_guid + and ucd.item_hash = i.item_hash + ) r, + table( xmlsequence( extract(r.item_data,'/*') ) ) ucd + ) ucd + ) act + on exp.row_hash = act.row_hash + and exp.duplicate_no = act.duplicate_no + where exp.row_hash is null or act.row_hash is null + order by diffed_type, coalesce(exp.row_hash,act.row_hash), duplicate_no + ) + where rownum < :max_rows ]' + bulk collect into l_results + using a_diff_id, + a_exclude_xpath, a_include_xpath, a_expected_dataset_guid, + a_exclude_xpath, a_include_xpath, a_actual_dataset_guid, + a_max_rows; + + return l_results; + + end; + + function compare_type(a_join_by_xpath in varchar2,a_unordered boolean) return varchar2 is + begin + case + when a_join_by_xpath is not null then + return gc_compare_join_by; + when a_unordered then + return gc_compare_unordered; + else + return gc_compare_normal; + end case; + end; + + function get_rows_diff( + a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, + a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2, + a_join_by_xpath varchar2,a_unorderdered boolean + ) return tt_row_diffs is + l_results tt_row_diffs; + l_compare_type varchar2(10):= compare_type(a_join_by_xpath,a_unorderdered); + begin + case + when l_compare_type = gc_compare_join_by then + return get_rows_diff(a_expected_dataset_guid, a_actual_dataset_guid, a_diff_id, + a_max_rows, a_exclude_xpath, a_include_xpath ,a_join_by_xpath); + when l_compare_type = gc_compare_unordered then + return get_rows_diff_unordered(a_expected_dataset_guid, a_actual_dataset_guid, a_diff_id, + a_max_rows, a_exclude_xpath, a_include_xpath); + else + return get_rows_diff(a_expected_dataset_guid, a_actual_dataset_guid, a_diff_id, + a_max_rows, a_exclude_xpath, a_include_xpath); + end case; + + end; + function get_hash(a_data raw, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is begin return dbms_crypto.hash(a_data, a_hash_type); @@ -258,26 +497,47 @@ create or replace package body ut_compound_data_helper is return l_cols_hash; end; -begin - g_type_name_map( dbms_sql.binary_bouble_type ) := 'BINARY_DOUBLE'; - g_type_name_map( dbms_sql.bfile_type ) := 'BFILE'; - g_type_name_map( dbms_sql.binary_float_type ) := 'BINARY_FLOAT'; - g_type_name_map( dbms_sql.blob_type ) := 'BLOB'; - g_type_name_map( dbms_sql.long_raw_type ) := 'LONG RAW'; - g_type_name_map( dbms_sql.char_type ) := 'CHAR'; - g_type_name_map( dbms_sql.clob_type ) := 'CLOB'; - g_type_name_map( dbms_sql.long_type ) := 'LONG'; - g_type_name_map( dbms_sql.date_type ) := 'DATE'; - g_type_name_map( dbms_sql.interval_day_to_second_type ) := 'INTERVAL DAY TO SECOND'; - g_type_name_map( dbms_sql.interval_year_to_month_type ) := 'INTERVAL YEAR TO MONTH'; - g_type_name_map( dbms_sql.raw_type ) := 'RAW'; - g_type_name_map( dbms_sql.timestamp_type ) := 'TIMESTAMP'; - g_type_name_map( dbms_sql.timestamp_with_tz_type ) := 'TIMESTAMP WITH TIME ZONE'; - g_type_name_map( dbms_sql.timestamp_with_local_tz_type ) := 'TIMESTAMP WITH LOCAL TIME ZONE'; - g_type_name_map( dbms_sql.varchar2_type ) := 'VARCHAR2'; - g_type_name_map( dbms_sql.number_type ) := 'NUMBER'; - g_type_name_map( dbms_sql.rowid_type ) := 'ROWID'; - g_type_name_map( dbms_sql.urowid_type ) := 'UROWID'; - + function is_pk_exists(a_expected_cursor xmltype,a_actual_cursor xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2,a_join_by_xpath varchar2) + return tt_missing_pk is + l_pk_xpath_tabs ut_varchar2_list := ut_varchar2_list(); + l_column_filter varchar2(32767); + l_no_missing_keys tt_missing_pk := tt_missing_pk(); + + begin + if a_join_by_xpath is not null then + l_pk_xpath_tabs := ut_utils.string_to_table(a_join_by_xpath,'|'); + l_column_filter := get_columns_row_filter(a_exclude_xpath, a_include_xpath); + + execute immediate q'[ + with xpaths_tab as (select column_value xpath from table(:xpath_tabs)), + expected_column_info as ( select :expected as item_data from dual ), + actual_column_info as ( select :actual as item_data from dual ) + select REGEXP_SUBSTR (xpath,'[^(/\*/)](.+)$'),diif_type from + ( + (select xpath,'e' diif_type from xpaths_tab + minus + select xpath,'e' diif_type + from ( select ]'||l_column_filter||q'[ from expected_column_info ucd) x + ,xpaths_tab + where xmlexists (xpaths_tab.xpath passing x.item_data) + ) + union all + (select xpath,'a' diif_type from xpaths_tab + minus + select xpath,'a' diif_type + from ( select ]'||l_column_filter||q'[ from actual_column_info ucd) x + ,xpaths_tab + where xmlexists (xpaths_tab.xpath passing x.item_data) + ) + )]' bulk collect into l_no_missing_keys + using l_pk_xpath_tabs,a_expected_cursor,a_actual_cursor, + a_exclude_xpath, a_include_xpath, + a_exclude_xpath, a_include_xpath; + + end if; + + return l_no_missing_keys; + end; + end; -/ \ No newline at end of file +/ diff --git a/source/expectations/data_values/ut_compound_data_helper.pks b/source/expectations/data_values/ut_compound_data_helper.pks index d49db160c..da2863667 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pks +++ b/source/expectations/data_values/ut_compound_data_helper.pks @@ -16,6 +16,10 @@ create or replace package ut_compound_data_helper authid definer is limitations under the License. */ + gc_compare_join_by constant varchar2(10):='join_by'; + gc_compare_unordered constant varchar2(10):='unordered'; + gc_compare_normal constant varchar2(10):='normal'; + type t_column_diffs is record( diff_type varchar2(1), expected_name varchar2(250), @@ -28,15 +32,23 @@ create or replace package ut_compound_data_helper authid definer is type tt_column_diffs is table of t_column_diffs; + type t_missing_pk is record( + missingxpath varchar2(250), + diff_type varchar2(1) + ); + + type tt_missing_pk is table of t_missing_pk; + type t_row_diffs is record( rn integer, diff_type varchar2(250), - diffed_row clob + diffed_row clob, + pk_value varchar2(4000) ); type tt_row_diffs is table of t_row_diffs; - function get_columns_info(a_cursor in out nocopy sys_refcursor) return xmltype; + function get_column_info_xml(a_column_details ut_key_anyval_pair) return xmltype; function get_columns_filter( a_exclude_xpath varchar2, a_include_xpath varchar2, @@ -47,9 +59,14 @@ create or replace package ut_compound_data_helper authid definer is a_expected xmltype, a_actual xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return tt_column_diffs; - function get_rows_diff( + function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob; + + function compare_type(a_join_by_xpath in varchar2,a_unordered boolean) return varchar2; + + function get_rows_diff( a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, - a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2 + a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2, + a_join_by_xpath varchar2,a_unorderdered boolean ) return tt_row_diffs; subtype t_hash is raw(128); @@ -60,6 +77,9 @@ create or replace package ut_compound_data_helper authid definer is a_data_value_cursor ut_data_value_refcursor, a_exclude_xpath varchar2, a_include_xpath varchar2, a_hash_type binary_integer := dbms_crypto.hash_sh1 ) return t_hash; + + function is_pk_exists(a_expected_cursor xmltype, a_actual_cursor xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2,a_join_by_xpath varchar2) + return tt_missing_pk; end; / diff --git a/source/expectations/data_values/ut_compound_data_tmp.sql b/source/expectations/data_values/ut_compound_data_tmp.sql index 300b0fd38..827ab9066 100644 --- a/source/expectations/data_values/ut_compound_data_tmp.sql +++ b/source/expectations/data_values/ut_compound_data_tmp.sql @@ -15,5 +15,8 @@ create global temporary table ut_compound_data_tmp( data_id raw(32), item_no integer, item_data xmltype, - constraint ut_compound_data_tmp_pk primary key(data_id, item_no) + item_hash raw(128), + pk_hash raw(128), + duplicate_no integer, + constraint ut_cmp_data_tmp_hash_pk unique (data_id,item_no, item_hash , duplicate_no) ) on commit preserve rows; diff --git a/source/expectations/data_values/ut_compound_data_value.tpb b/source/expectations/data_values/ut_compound_data_value.tpb index 164bc1129..45841e5a9 100644 --- a/source/expectations/data_values/ut_compound_data_value.tpb +++ b/source/expectations/data_values/ut_compound_data_value.tpb @@ -71,17 +71,18 @@ create or replace type body ut_compound_data_value as return l_result_string; end; - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2 is + overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2 is l_result clob; - l_result_string varchar2(32767); + l_result_string varchar2(32767); begin - l_result := get_data_diff(a_other, a_exclude_xpath, a_include_xpath); + l_result := get_data_diff(a_other, a_exclude_xpath, a_include_xpath, a_join_by_xpath,a_unordered); l_result_string := ut_utils.to_string(l_result,null); dbms_lob.freetemporary(l_result); return l_result_string; end; - - member function get_data_diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return clob is + + member function get_data_diff(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, + a_join_by_xpath varchar2, a_unordered boolean) return clob is c_max_rows constant integer := 20; l_result clob; l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); @@ -91,6 +92,19 @@ create or replace type body ut_compound_data_value as l_actual ut_compound_data_value; l_diff_id ut_compound_data_helper.t_hash; l_row_diffs ut_compound_data_helper.tt_row_diffs; + l_compare_type varchar2(10); + + function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs,a_compare_type varchar2) return varchar2 is + begin + if a_compare_type = ut_compound_data_helper.gc_compare_join_by and a_row_diff.pk_value is not null then + return ' PK '||a_row_diff.pk_value||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + elsif a_compare_type = ut_compound_data_helper.gc_compare_join_by or a_compare_type = ut_compound_data_helper.gc_compare_normal then + return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + elsif a_compare_type = ut_compound_data_helper.gc_compare_unordered then + return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + end; + begin if not a_other is of (ut_compound_data_value) then raise value_error; @@ -101,13 +115,16 @@ create or replace type body ut_compound_data_value as --diff rows and row elements l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_actual.data_id); + -- First tell how many rows are different - execute immediate 'select count(*) from ' || l_ut_owner || '.ut_compound_data_diff_tmp where diff_id = :diff_id' into l_diff_row_count using l_diff_id; + execute immediate 'select count('||case when a_join_by_xpath is not null then 'distinct pk_hash' else '*' end||') from ' + || l_ut_owner || '.ut_compound_data_diff_tmp + where diff_id = :diff_id' into l_diff_row_count using l_diff_id; if l_diff_row_count > 0 then + l_compare_type := ut_compound_data_helper.compare_type(a_join_by_xpath,a_unordered); l_row_diffs := ut_compound_data_helper.get_rows_diff( - self.data_id, l_actual.data_id, l_diff_id, c_max_rows, a_exclude_xpath, a_include_xpath - ); + self.data_id, l_actual.data_id, l_diff_id, c_max_rows, a_exclude_xpath, a_include_xpath, a_join_by_xpath, a_unordered); l_message := chr(10) ||'Rows: [ ' || l_diff_row_count ||' differences' || case when l_diff_row_count > c_max_rows and l_row_diffs.count > 0 then ', showing first '||c_max_rows end @@ -117,13 +134,14 @@ create or replace type body ut_compound_data_value as ut_utils.append_to_clob( l_result, l_message ); for i in 1 .. l_row_diffs.count loop l_results.extend; - l_results(l_results.last) := ' Row No. '||l_row_diffs(i).rn||' - '||rpad(l_row_diffs(i).diff_type,10)||l_row_diffs(i).diffed_row; + l_results(l_results.last) := get_diff_message(l_row_diffs(i),l_compare_type); end loop; ut_utils.append_to_clob(l_result,l_results); end if; return l_result; end; + member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2) return integer is l_other ut_compound_data_value; l_ut_owner varchar2(250) := ut_utils.ut_owner; @@ -172,7 +190,103 @@ create or replace type body ut_compound_data_value as ') != 0' using in l_diff_id, a_exclude_xpath, a_include_xpath, self.data_id, a_exclude_xpath, a_include_xpath, l_other.data_id, l_xml_data_fmt, l_xml_data_fmt; + --result is OK only if both are same + if sql%rowcount = 0 and self.elements_count = l_other.elements_count then + l_result := 0; + else + l_result := 1; + end if; + return l_result; + end; + + member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean ) return integer is + l_other ut_compound_data_value; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_column_filter varchar2(32767); + l_diff_id ut_compound_data_helper.t_hash; + l_result integer; + l_row_diffs ut_compound_data_helper.tt_row_diffs; + c_max_rows constant integer := 20; + + function get_column_pk_hash(a_join_by_xpath varchar2) return varchar2 is + l_column varchar2(32767); + begin + /* due to possibility of key being to columns we cannot use xmlextractvalue + usage of xmlagg is possible however it greatly complicates code and performance is impacted. + xpath to be looked at or regex + */ + if a_join_by_xpath is not null then + l_column := l_ut_owner ||'.ut_compound_data_helper.get_hash(extract(ucd.item_data,:join_by_xpath).GetClobVal()) pk_hash'; + else + l_column := ':join_by_xpath pk_hash'; + end if; + return l_column; + end; + + begin + if not a_other is of (ut_compound_data_value) then + raise value_error; + end if; + + l_other := treat(a_other as ut_compound_data_value); + l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_other.data_id); + l_column_filter := ut_compound_data_helper.get_columns_filter(a_exclude_xpath, a_include_xpath); + + /** + * Due to incompatibility issues in XML between 11 and 12.2 and 12.1 versions we will prepopulate pk_hash upfront to + * avoid optimizer incorrectly rewrite and causing NULL error or ORA-600 + **/ + execute immediate 'merge into ' || l_ut_owner || '.ut_compound_data_tmp tgt + using ( + select '||l_ut_owner ||'.ut_compound_data_helper.get_hash(ucd.item_data.getclobval()) item_hash, + pk_hash, ucd.item_no, ucd.data_id + from + ( + select '||l_column_filter||','||get_column_pk_hash(a_join_by_xpath)||', item_no, data_id + from ' || l_ut_owner || q'[.ut_compound_data_tmp ucd + where data_id = :self_guid or data_id = :other_guid + ) ucd + ) src + on (tgt.item_no = src.item_no and tgt.data_id = src.data_id) + when matched then update + set tgt.item_hash = src.item_hash, + tgt.pk_hash = src.pk_hash ]' + using a_exclude_xpath, a_include_xpath,a_join_by_xpath,self.data_id, l_other.data_id; + + /* Peform minus on two sets two get diffrences that will be used later on to print results */ + execute immediate 'insert into ' || l_ut_owner || '.ut_compound_data_diff_tmp ( diff_id,item_hash,pk_hash,duplicate_no) + with source_data as + ( select t.data_id,t.item_hash,row_number() over (partition by t.pk_hash,t.item_hash,t.data_id order by 1,2) duplicate_no, + pk_hash + from ' || l_ut_owner || '.ut_compound_data_tmp t + where data_id = :self_guid or data_id = :other_guid + ) + select distinct :diff_id,tmp.item_hash,tmp.pk_hash,tmp.duplicate_no + from( + ( + select t.item_hash,t. duplicate_no,t.pk_hash + from source_data t + where t.data_id = :self_guid + minus + select t.item_hash,t. duplicate_no,t.pk_hash + from source_data t + where t.data_id = :other_guid + ) + union all + ( + select t.item_hash,t. duplicate_no,t.pk_hash + from source_data t + where t.data_id = :other_guid + minus + select t.item_hash,t. duplicate_no,t.pk_hash + from source_data t + where t.data_id = :self_guid + ))tmp' + using self.data_id, l_other.data_id, + l_diff_id, + self.data_id, l_other.data_id, + l_other.data_id,self.data_id; --result is OK only if both are same if sql%rowcount = 0 and self.elements_count = l_other.elements_count then l_result := 0; diff --git a/source/expectations/data_values/ut_compound_data_value.tps b/source/expectations/data_values/ut_compound_data_value.tps index e06848a1e..003373271 100644 --- a/source/expectations/data_values/ut_compound_data_value.tps +++ b/source/expectations/data_values/ut_compound_data_value.tps @@ -1,4 +1,4 @@ -create or replace type ut_compound_data_value under ut_data_value( +create or replace type ut_compound_data_value force under ut_data_value( /* utPLSQL - Version 3 Copyright 2016 - 2017 utPLSQL Project @@ -42,8 +42,9 @@ create or replace type ut_compound_data_value under ut_data_value( overriding member function to_string return varchar2, overriding member function is_multi_line return boolean, overriding member function compare_implementation(a_other ut_data_value) return integer, - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2, - member function get_data_diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return clob, - member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2) return integer + overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2, + member function get_data_diff(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean) return clob, + member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2) return integer, + member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean ) return integer ) not final not instantiable / diff --git a/source/expectations/data_values/ut_curr_usr_compound_helper.pkb b/source/expectations/data_values/ut_curr_usr_compound_helper.pkb new file mode 100644 index 000000000..7885b15fe --- /dev/null +++ b/source/expectations/data_values/ut_curr_usr_compound_helper.pkb @@ -0,0 +1,186 @@ +create or replace package body ut_curr_usr_compound_helper is + + type t_type_name_map is table of varchar2(100) index by binary_integer; + g_type_name_map t_type_name_map; + g_anytype_name_map t_type_name_map; + g_user_defined_type pls_integer := dbms_sql.user_defined_type; + + + function get_column_type(a_desc_rec dbms_sql.desc_rec3,a_desc_user_types boolean := false) return ut_key_anyval_pair is + l_data ut_data_value; + l_result ut_key_anyval_pair; + l_data_type varchar2(500) := 'unknown datatype'; + begin + if g_type_name_map.exists(a_desc_rec.col_type) then + l_data := ut_data_value_varchar2(g_type_name_map(a_desc_rec.col_type)); + elsif a_desc_rec.col_type = g_user_defined_type and a_desc_user_types then + l_data :=ut_data_value_xmltype(get_user_defined_type(a_desc_rec.col_schema_name,a_desc_rec.col_type_name)); + elsif a_desc_rec.col_schema_name is not null and a_desc_rec.col_type_name is not null then + l_data := ut_data_value_varchar2(a_desc_rec.col_schema_name||'.'||a_desc_rec.col_type_name); + end if; + return ut_key_anyval_pair(a_desc_rec.col_name,l_data); + end; + + function get_columns_info(a_columns_tab dbms_sql.desc_tab3, a_columns_count integer,a_desc_user_types boolean := false) return ut_key_anyval_pairs is + l_result ut_key_anyval_pairs := ut_key_anyval_pairs(); + begin + for i in 1 .. a_columns_count loop + l_result.extend; + l_result(l_result.last) := get_column_type(a_columns_tab(i),a_desc_user_types); + end loop; + return l_result; + end; + + function get_descr_cursor(a_cursor in out nocopy sys_refcursor,a_desc_user_types boolean := false) return ut_key_anyval_pairs is + l_cursor_number integer; + l_columns_count pls_integer; + l_columns_desc dbms_sql.desc_tab3; + l_columns_tab ut_key_anyval_pairs; + begin + if a_cursor is null or not a_cursor%isopen then + return null; + end if; + l_cursor_number := dbms_sql.to_cursor_number( a_cursor ); + dbms_sql.describe_columns3( l_cursor_number, l_columns_count, l_columns_desc ); + a_cursor := dbms_sql.to_refcursor( l_cursor_number ); + l_columns_tab := get_columns_info( l_columns_desc, l_columns_count,a_desc_user_types); + return l_columns_tab; + end; + + function get_columns_info(a_cursor in out nocopy sys_refcursor,a_desc_user_types boolean := false) return xmltype is + l_result xmltype; + l_result_tmp xmltype; + l_columns_tab ut_key_anyval_pairs; + begin + l_columns_tab := get_descr_cursor(a_cursor,a_desc_user_types); + + for i in 1..l_columns_tab.COUNT + loop + l_result_tmp := ut_compound_data_helper.get_column_info_xml(l_columns_tab(i)); + select xmlconcat(l_result,l_result_tmp) into l_result from dual; + end loop; + + select XMLELEMENT("ROW",l_result ) + into l_result from dual; + + return l_result; + end; + + function get_anytype_attribute_count (a_anytype anytype) return pls_integer is + l_attribute_typecode pls_integer; + l_schema_name varchar2(32767); + l_version varchar2(32767); + l_type_name varchar2(32767); + l_attributes pls_integer; + l_prec pls_integer; + l_scale pls_integer; + l_len pls_integer; + l_csid pls_integer; + l_csfrm pls_integer; + begin + l_attribute_typecode := a_anytype.getinfo( + prec => l_prec, + scale => l_scale, + len => l_len, + csid => l_csid, + csfrm => l_csfrm, + schema_name => l_schema_name, + type_name => l_type_name, + version => l_version, + numelems => l_attributes); + return l_attributes; + end; + + function get_anytype_attributes_info (a_anytype anytype) return ut_key_value_pairs is + l_result ut_key_value_pairs := ut_key_value_pairs(); + l_attribute_typecode pls_integer; + l_aname varchar2(32767); + l_prec pls_integer; + l_scale pls_integer; + l_len pls_integer; + l_csid pls_integer; + l_csfrm pls_integer; + l_attr_elt_type anytype; + begin + for i in 1..get_anytype_attribute_count(a_anytype) loop + l_attribute_typecode := a_anytype.getAttrElemInfo( + pos => i, --First attribute + prec => l_prec, + scale => l_scale, + len => l_len, + csid => l_csid, + csfrm => l_csfrm, + attr_elt_type => l_attr_elt_type, + aname => l_aname); + + l_result.extend; + l_result(l_result.last) := ut_key_value_pair(l_aname, g_anytype_name_map(l_attribute_typecode)); + + end loop; + return l_result; + end; + + function get_user_defined_type(a_owner varchar2,a_type_name varchar2) return xmltype is + l_anydata anydata; + l_anytype anytype; + l_typecode pls_integer; + l_result xmltype; + l_columns_tab ut_key_value_pairs := ut_key_value_pairs(); + begin + execute immediate 'declare + l_v '||a_owner||'.'||a_type_name||'; + begin + :anydata := anydata.convertobject(l_v); + end;' USING IN OUT l_anydata; + + l_typecode := l_anydata.gettype(l_anytype); + l_columns_tab := get_anytype_attributes_info(l_anytype); + + select xmlagg(xmlelement(evalname key,value)) + into l_result from table(l_columns_tab); + + return l_result; + + end; + + begin + g_anytype_name_map(dbms_types.typecode_date) :=' DATE'; + g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; + g_anytype_name_map(dbms_types.typecode_raw) := 'RAW'; + g_anytype_name_map(dbms_types.typecode_char) := 'CHAR'; + g_anytype_name_map(dbms_types.typecode_varchar2) := 'VARCHAR2'; + g_anytype_name_map(dbms_types.typecode_varchar) := 'VARCHAR'; + g_anytype_name_map(dbms_types.typecode_blob) := 'BLOB'; + g_anytype_name_map(dbms_types.typecode_bfile) := 'BFILE'; + g_anytype_name_map(dbms_types.typecode_clob) := 'CLOB'; + g_anytype_name_map(dbms_types.typecode_timestamp) := 'TIMESTAMP'; + g_anytype_name_map(dbms_types.typecode_timestamp_tz) := 'TIMESTAMP WITH TIME ZONE'; + g_anytype_name_map(dbms_types.typecode_timestamp_ltz) := 'TIMESTAMP WITH LOCAL TIME ZONE'; + g_anytype_name_map(dbms_types.typecode_interval_ym) := 'INTERVAL YEAR TO MONTH'; + g_anytype_name_map(dbms_types.typecode_interval_ds) := 'INTERVAL DAY TO SECOND'; + g_anytype_name_map(dbms_types.typecode_bfloat) := 'BINARY_FLOAT'; + g_anytype_name_map(dbms_types.typecode_bdouble) := 'BINARY_DOUBLE'; + g_anytype_name_map(dbms_types.typecode_urowid) := 'UROWID'; + + g_type_name_map( dbms_sql.binary_bouble_type ) := 'BINARY_DOUBLE'; + g_type_name_map( dbms_sql.bfile_type ) := 'BFILE'; + g_type_name_map( dbms_sql.binary_float_type ) := 'BINARY_FLOAT'; + g_type_name_map( dbms_sql.blob_type ) := 'BLOB'; + g_type_name_map( dbms_sql.long_raw_type ) := 'LONG RAW'; + g_type_name_map( dbms_sql.char_type ) := 'CHAR'; + g_type_name_map( dbms_sql.clob_type ) := 'CLOB'; + g_type_name_map( dbms_sql.long_type ) := 'LONG'; + g_type_name_map( dbms_sql.date_type ) := 'DATE'; + g_type_name_map( dbms_sql.interval_day_to_second_type ) := 'INTERVAL DAY TO SECOND'; + g_type_name_map( dbms_sql.interval_year_to_month_type ) := 'INTERVAL YEAR TO MONTH'; + g_type_name_map( dbms_sql.raw_type ) := 'RAW'; + g_type_name_map( dbms_sql.timestamp_type ) := 'TIMESTAMP'; + g_type_name_map( dbms_sql.timestamp_with_tz_type ) := 'TIMESTAMP WITH TIME ZONE'; + g_type_name_map( dbms_sql.timestamp_with_local_tz_type ) := 'TIMESTAMP WITH LOCAL TIME ZONE'; + g_type_name_map( dbms_sql.varchar2_type ) := 'VARCHAR2'; + g_type_name_map( dbms_sql.number_type ) := 'NUMBER'; + g_type_name_map( dbms_sql.rowid_type ) := 'ROWID'; + g_type_name_map( dbms_sql.urowid_type ) := 'UROWID'; + +end; +/ diff --git a/source/expectations/data_values/ut_curr_usr_compound_helper.pks b/source/expectations/data_values/ut_curr_usr_compound_helper.pks new file mode 100644 index 000000000..3955f49c8 --- /dev/null +++ b/source/expectations/data_values/ut_curr_usr_compound_helper.pks @@ -0,0 +1,8 @@ +create or replace package ut_curr_usr_compound_helper authid current_user is + + function get_columns_info(a_cursor in out nocopy sys_refcursor,a_desc_user_types boolean := false) return xmltype; + + function get_user_defined_type(a_owner varchar2,a_type_name varchar2) return xmltype; + +end; +/ diff --git a/source/expectations/data_values/ut_data_value.tpb b/source/expectations/data_values/ut_data_value.tpb index 605f492f3..d5ec11092 100644 --- a/source/expectations/data_values/ut_data_value.tpb +++ b/source/expectations/data_values/ut_data_value.tpb @@ -24,8 +24,8 @@ create or replace type body ut_data_value as begin return false; end; - - member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2 is + + member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean :=false ) return varchar2 is begin return null; end; diff --git a/source/expectations/data_values/ut_data_value.tps b/source/expectations/data_values/ut_data_value.tps index fba0ed3c4..79ac4eef9 100644 --- a/source/expectations/data_values/ut_data_value.tps +++ b/source/expectations/data_values/ut_data_value.tps @@ -1,4 +1,4 @@ -create or replace type ut_data_value authid current_user as object ( +create or replace type ut_data_value force authid current_user as object ( /* utPLSQL - Version 3 Copyright 2016 - 2017 utPLSQL Project @@ -24,7 +24,7 @@ create or replace type ut_data_value authid current_user as object ( final member function to_string_report(a_add_new_line_for_multi_line boolean := false, a_with_object_info boolean := true) return varchar2, order member function compare( a_other ut_data_value ) return integer, member function is_diffable return boolean, - member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2, + member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2, not instantiable member function compare_implementation( a_other ut_data_value ) return integer ) not final not instantiable / diff --git a/source/expectations/data_values/ut_data_value_refcursor.tpb b/source/expectations/data_values/ut_data_value_refcursor.tpb index ccebdb99e..5b8ab8463 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tpb +++ b/source/expectations/data_values/ut_data_value_refcursor.tpb @@ -15,7 +15,7 @@ create or replace type body ut_data_value_refcursor as See the License for the specific language governing permissions and limitations under the License. */ - + constructor function ut_data_value_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) return self as result is begin init(a_value); @@ -28,8 +28,8 @@ create or replace type body ut_data_value_refcursor as l_ctx number; l_xml xmltype; l_current_date_format varchar2(4000); - l_ut_owner varchar2(250) := ut_utils.ut_owner; cursor_not_open exception; + l_ut_owner varchar2(250) := ut_utils.ut_owner; begin self.is_data_null := ut_utils.boolean_to_int(a_value is null); self.self_type := $$plsql_unit; @@ -37,7 +37,8 @@ create or replace type body ut_data_value_refcursor as self.data_type := 'refcursor'; if l_cursor is not null then if l_cursor%isopen then - self.columns_info := ut_compound_data_helper.get_columns_info(l_cursor); + self.columns_info := ut_curr_usr_compound_helper.get_columns_info(l_cursor); + self.key_info := ut_curr_usr_compound_helper.get_columns_info(l_cursor,true); self.elements_count := 0; -- We use DBMS_XMLGEN in order to: -- 1) be able to process data in bulks (set of rows) @@ -58,8 +59,8 @@ create or replace type body ut_data_value_refcursor as loop l_xml := dbms_xmlgen.getxmltype(l_ctx); - - execute immediate + + execute immediate 'insert into ' || l_ut_owner || '.ut_compound_data_tmp(data_id, item_no, item_data) ' || 'select :self_guid, :self_row_count + rownum, value(a) ' || ' from table( xmlsequence( extract(:l_xml,''ROWSET/*'') ) ) a' @@ -69,7 +70,7 @@ create or replace type body ut_data_value_refcursor as self.elements_count := self.elements_count + sql%rowcount; end loop; - + ut_expectation_processor.reset_nls_params(); if l_cursor%isopen then close l_cursor; @@ -108,14 +109,15 @@ create or replace type body ut_data_value_refcursor as return l_result_string; end; - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2 is + overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2 is l_result clob; l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); l_result_string varchar2(32767); l_actual ut_data_value_refcursor; l_column_diffs ut_compound_data_helper.tt_column_diffs := ut_compound_data_helper.tt_column_diffs(); l_exclude_xpath varchar2(32767) := a_exclude_xpath; - + l_missing_pk ut_compound_data_helper.tt_missing_pk := ut_compound_data_helper.tt_missing_pk(); + function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is begin return @@ -130,7 +132,18 @@ create or replace type body ut_data_value_refcursor as ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' end; end; - + + function get_missing_key_message(a_missing_keys ut_compound_data_helper.t_missing_pk) return varchar2 is + begin + return + case a_missing_keys.diff_type + when 'a' then + ' Join key '||a_missing_keys.missingxpath||' does not exists in actual' + when 'e' then + ' Join key '||a_missing_keys.missingxpath||' does not exists in expected' + end; + end; + function add_incomparable_cols_to_xpath( a_column_diffs ut_compound_data_helper.tt_column_diffs, a_exclude_xpath varchar2 ) return varchar2 is @@ -151,6 +164,7 @@ create or replace type body ut_data_value_refcursor as end if; return l_result; end; + begin if not a_other is of (ut_data_value_refcursor) then raise value_error; @@ -174,34 +188,65 @@ create or replace type body ut_data_value_refcursor as ut_utils.append_to_clob(l_result, l_results); l_exclude_xpath := add_incomparable_cols_to_xpath(l_column_diffs, a_exclude_xpath); end if; - - --diff rows and row elements - ut_utils.append_to_clob(l_result, self.get_data_diff(a_other, l_exclude_xpath, a_include_xpath)); - + + --check for missing pk + if (a_join_by_xpath is not null) then + l_missing_pk := ut_compound_data_helper.is_pk_exists(self.key_info, l_actual.key_info, a_exclude_xpath, a_include_xpath,a_join_by_xpath); + end if; + + --diff rows and row elements if the pk is not missing + if l_missing_pk.count = 0 then + ut_utils.append_to_clob(l_result, self.get_data_diff(a_other, a_exclude_xpath, a_include_xpath, a_join_by_xpath, a_unordered)); + else + ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); + for i in 1 .. l_missing_pk.count loop + l_results.extend; + ut_utils.append_to_clob(l_result, get_missing_key_message(l_missing_pk(i))|| chr(10)); + end loop; + end if; + l_result_string := ut_utils.to_string(l_result,null); dbms_lob.freetemporary(l_result); return l_result_string; end; - overriding member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2) return integer is + overriding member function compare_implementation (a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean) return integer is l_result integer := 0; l_other ut_data_value_refcursor; + function is_pk_missing (a_pk_missing_tab ut_compound_data_helper.tt_missing_pk) return integer is + begin + return case when a_pk_missing_tab.count > 0 then 1 else 0 end; + end; begin if not a_other is of (ut_data_value_refcursor) then raise value_error; end if; l_other := treat(a_other as ut_data_value_refcursor); - - --if column names/types are not equal - build a diff of column names and types - if ut_compound_data_helper.columns_hash( self, a_exclude_xpath, a_include_xpath ) - != ut_compound_data_helper.columns_hash( l_other, a_exclude_xpath, a_include_xpath ) - then - l_result := 1; + + --if we join by key and key is missing fail and report error + if a_join_by_xpath is not null then + l_result := is_pk_missing(ut_compound_data_helper.is_pk_exists(self.key_info, l_other.key_info, a_exclude_xpath, a_include_xpath,a_join_by_xpath)); end if; - l_result := l_result + (self as ut_compound_data_value).compare_implementation(a_other, a_exclude_xpath, a_include_xpath); + + if l_result = 0 then + --if column names/types are not equal - build a diff of column names and types + if ut_compound_data_helper.columns_hash( self, a_exclude_xpath, a_include_xpath ) + != ut_compound_data_helper.columns_hash( l_other, a_exclude_xpath, a_include_xpath ) + then + l_result := 1; + end if; + + if a_unordered then + l_result := l_result + (self as ut_compound_data_value).compare_implementation(a_other, a_exclude_xpath, a_include_xpath, a_join_by_xpath, a_unordered); + else + l_result := l_result + (self as ut_compound_data_value).compare_implementation(a_other, a_exclude_xpath, a_include_xpath); + end if; + end if; + return l_result; end; + end; / diff --git a/source/expectations/data_values/ut_data_value_refcursor.tps b/source/expectations/data_values/ut_data_value_refcursor.tps index c59bf2662..4e175ce6b 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tps +++ b/source/expectations/data_values/ut_data_value_refcursor.tps @@ -29,11 +29,16 @@ create or replace type ut_data_value_refcursor under ut_compound_data_value( * Holds information about column names and column data-types */ columns_info xmltype, - + + /** + * Holds more detailed information regarding the pk joins + */ + key_info xmltype, + constructor function ut_data_value_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) return self as result, member procedure init(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor), overriding member function to_string return varchar2, - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2, - overriding member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2) return integer + overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2, + overriding member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean) return integer ) / diff --git a/source/expectations/data_values/ut_data_value_xmltype.tpb b/source/expectations/data_values/ut_data_value_xmltype.tpb new file mode 100644 index 000000000..237a7e592 --- /dev/null +++ b/source/expectations/data_values/ut_data_value_xmltype.tpb @@ -0,0 +1,50 @@ +create or replace type body ut_data_value_xmltype as + /* + utPLSQL - Version 3 + Copyright 2016 - 2017 utPLSQL Project + + Licensed under the Apache License, Version 2.0 (the "License"): + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + + constructor function ut_data_value_xmltype(self in out nocopy ut_data_value_xmltype, a_value xmltype) return self as result is + begin + self.data_value := a_value; + self.self_type := $$plsql_unit; + self.data_type := 'xmltype'; + return; + end; + + overriding member function is_null return boolean is + begin + return (self.data_value is null); + end; + + overriding member function to_string return varchar2 is + begin + return ut_utils.to_string(self.data_value.getClobVal()); + end; + + overriding member function compare_implementation(a_other ut_data_value) return integer is + l_result integer; + l_other ut_data_value_xmltype; + begin + if a_other is of (ut_data_value_xmltype) then + l_other := treat(a_other as ut_data_value_xmltype); + l_result := dbms_lob.compare(self.data_value.getClobVal(),l_other.data_value.getClobVal()); + end if; + + return l_result; + end; + +end; +/ diff --git a/source/expectations/data_values/ut_data_value_xmltype.tps b/source/expectations/data_values/ut_data_value_xmltype.tps new file mode 100644 index 000000000..2bc4ae985 --- /dev/null +++ b/source/expectations/data_values/ut_data_value_xmltype.tps @@ -0,0 +1,24 @@ +create or replace type ut_data_value_xmltype under ut_data_value( + /* + utPLSQL - Version 3 + Copyright 2016 - 2017 utPLSQL Project + + Licensed under the Apache License, Version 2.0 (the "License"): + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + data_value xmltype, + constructor function ut_data_value_xmltype(self in out nocopy ut_data_value_xmltype, a_value xmltype) return self as result, + overriding member function is_null return boolean, + overriding member function to_string return varchar2, + overriding member function compare_implementation(a_other ut_data_value) return integer +) +/ diff --git a/source/expectations/data_values/ut_key_anyval_pair.tps b/source/expectations/data_values/ut_key_anyval_pair.tps new file mode 100644 index 000000000..8fb4831c0 --- /dev/null +++ b/source/expectations/data_values/ut_key_anyval_pair.tps @@ -0,0 +1,21 @@ +create or replace type ut_key_anyval_pair force as object( + /* + utPLSQL - Version 3 + Copyright 2016 - 2017 utPLSQL Project + + Licensed under the Apache License, Version 2.0 (the "License"): + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + key varchar2(4000), + value ut_data_value +) not final +/ diff --git a/source/expectations/data_values/ut_key_anyval_pairs.tps b/source/expectations/data_values/ut_key_anyval_pairs.tps new file mode 100644 index 000000000..ccf4ce047 --- /dev/null +++ b/source/expectations/data_values/ut_key_anyval_pairs.tps @@ -0,0 +1,19 @@ +create or replace type ut_key_anyval_pairs as + /* + utPLSQL - Version 3 + Copyright 2016 - 2017 utPLSQL Project + + Licensed under the Apache License, Version 2.0 (the "License"): + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + table of ut_key_anyval_pair +/ diff --git a/source/expectations/matchers/ut_equal.tpb b/source/expectations/matchers/ut_equal.tpb index 29b8f511d..3a163e9c9 100644 --- a/source/expectations/matchers/ut_equal.tpb +++ b/source/expectations/matchers/ut_equal.tpb @@ -23,6 +23,7 @@ create or replace type body ut_equal as self.expected := a_expected; self.include_list := ut_varchar2_list(); self.exclude_list := ut_varchar2_list(); + self.join_columns := ut_varchar2_list(); end; member function equal_with_nulls(a_assert_result boolean, a_actual ut_data_value) return boolean is @@ -182,6 +183,29 @@ create or replace type body ut_equal as return l_result; end; + member function unordered return ut_equal is + l_result ut_equal := self; + begin + l_result.is_unordered := ut_utils.boolean_to_int(true); + return l_result; + end; + + member function join_by(a_columns varchar2) return ut_equal is + l_result ut_equal := self; + begin + l_result.is_unordered := ut_utils.boolean_to_int(true); + ut_utils.append_to_list(l_result.join_columns, a_columns); + return l_result; + end; + + member function join_by(a_columns ut_varchar2_list) return ut_equal is + l_result ut_equal := self; + begin + l_result.is_unordered := ut_utils.boolean_to_int(true); + l_result.join_columns := l_result.join_columns multiset union all coalesce(a_columns,ut_varchar2_list()); + return l_result; + end; + member function get_include_xpath return varchar2 is begin return ut_utils.to_xpath( coalesce(include_list, ut_varchar2_list()) ); @@ -192,6 +216,16 @@ create or replace type body ut_equal as return ut_utils.to_xpath( coalesce(exclude_list, ut_varchar2_list()) ); end; + member function get_unordered return boolean is + begin + return ut_utils.int_to_boolean(nvl(is_unordered,0)); + end; + + member function get_join_by_xpath return varchar2 is + begin + return ut_utils.to_xpath( coalesce(join_columns, ut_varchar2_list()) ); + end; + overriding member function run_matcher(self in out nocopy ut_equal, a_actual ut_data_value) return boolean is l_result boolean; begin @@ -199,7 +233,7 @@ create or replace type body ut_equal as if self.expected is of (ut_data_value_anydata) then l_result := 0 = treat(self.expected as ut_data_value_anydata).compare_implementation(a_actual, get_exclude_xpath(), get_include_xpath()); elsif self.expected is of (ut_data_value_refcursor) then - l_result := 0 = treat(self.expected as ut_data_value_refcursor).compare_implementation(a_actual, get_exclude_xpath(), get_include_xpath()); + l_result := 0 = treat(self.expected as ut_data_value_refcursor).compare_implementation(a_actual, get_exclude_xpath(), get_include_xpath(), get_join_by_xpath(), get_unordered()); else l_result := equal_with_nulls((self.expected = a_actual), a_actual); end if; @@ -216,7 +250,7 @@ create or replace type body ut_equal as if self.expected.data_type = a_actual.data_type and self.expected.is_diffable then l_result := 'Actual: '||a_actual.get_object_info()||' '||self.description()||': '||self.expected.get_object_info() - || chr(10) || 'Diff:' || expected.diff(a_actual, get_exclude_xpath(), get_include_xpath()); + || chr(10) || 'Diff:' || expected.diff(a_actual, get_exclude_xpath(), get_include_xpath(), get_join_by_xpath(), get_unordered()); else l_result := (self as ut_matcher).failure_message(a_actual) || ': '|| self.expected.to_string_report(); end if; diff --git a/source/expectations/matchers/ut_equal.tps b/source/expectations/matchers/ut_equal.tps index 710b23f7f..01b1d2e1e 100644 --- a/source/expectations/matchers/ut_equal.tps +++ b/source/expectations/matchers/ut_equal.tps @@ -25,7 +25,17 @@ create or replace type ut_equal under ut_comparison_matcher( * Holds (list of columns/attributes) to incude when comparing compound types */ include_list ut_varchar2_list, + + /** + * Holds value if comparision on refcursor to be performed as unordered set + */ + is_unordered number(1,0), + /** + * Holds list of columns to be used as a join PK on sys_refcursor comparision + */ + join_columns ut_varchar2_list, + member procedure init(self in out nocopy ut_equal, a_expected ut_data_value, a_nulls_are_equal boolean), member function equal_with_nulls( self in ut_equal, a_assert_result boolean, a_actual ut_data_value) return boolean, constructor function ut_equal(self in out nocopy ut_equal, a_expected anydata, a_nulls_are_equal boolean := null) return self as result, @@ -49,8 +59,13 @@ create or replace type ut_equal under ut_comparison_matcher( member function include(a_items ut_varchar2_list) return ut_equal, member function exclude(a_items varchar2) return ut_equal, member function exclude(a_items ut_varchar2_list) return ut_equal, + member function unordered return ut_equal, + member function join_by(a_columns varchar2) return ut_equal, + member function join_by(a_columns ut_varchar2_list) return ut_equal, member function get_include_xpath return varchar2, member function get_exclude_xpath return varchar2, + member function get_unordered return boolean, + member function get_join_by_xpath return varchar2, overriding member function run_matcher(self in out nocopy ut_equal, a_actual ut_data_value) return boolean, overriding member function failure_message(a_actual ut_data_value) return varchar2, overriding member function failure_message_when_negated(a_actual ut_data_value) return varchar2 diff --git a/source/expectations/ut_expectation_compound.tpb b/source/expectations/ut_expectation_compound.tpb index 8665af265..dea146bd2 100644 --- a/source/expectations/ut_expectation_compound.tpb +++ b/source/expectations/ut_expectation_compound.tpb @@ -146,6 +146,59 @@ create or replace type body ut_expectation_compound as self.to_( treat(matcher as ut_equal).exclude(a_items) ); end if; end; + + member function unordered return ut_expectation_compound is + l_result ut_expectation_compound; + begin + l_result := self; + l_result.matcher := treat(l_result.matcher as ut_equal).unordered; + return l_result; + end; + + member procedure unordered(self in ut_expectation_compound) is + begin + + if ut_utils.int_to_boolean(negated) then + self.not_to( treat(matcher as ut_equal).unordered ); + else + self.to_( treat(matcher as ut_equal).unordered ); + end if; + end; + + member function join_by(a_columns varchar2) return ut_expectation_compound is + l_result ut_expectation_compound; + begin + l_result := self; + l_result.matcher := treat(l_result.matcher as ut_equal).join_by(a_columns); + return l_result; + end; + + member function join_by(a_columns ut_varchar2_list) return ut_expectation_compound is + l_result ut_expectation_compound; + begin + l_result := self; + l_result.matcher := treat(l_result.matcher as ut_equal).join_by(a_columns); + return l_result; + end; + + member procedure join_by(self in ut_expectation_compound, a_columns varchar2) is + begin + if ut_utils.int_to_boolean(negated) then + self.not_to( treat(matcher as ut_equal).join_by(a_columns) ); + else + self.to_( treat(matcher as ut_equal).join_by(a_columns) ); + end if; + end; + + member procedure join_by(self in ut_expectation_compound, a_columns ut_varchar2_list) is + begin + + if ut_utils.int_to_boolean(negated) then + self.not_to( treat(matcher as ut_equal).join_by(a_columns) ); + else + self.to_( treat(matcher as ut_equal).join_by(a_columns) ); + end if; + end; end; / diff --git a/source/expectations/ut_expectation_compound.tps b/source/expectations/ut_expectation_compound.tps index 66dcb91af..967706a21 100644 --- a/source/expectations/ut_expectation_compound.tps +++ b/source/expectations/ut_expectation_compound.tps @@ -36,8 +36,13 @@ create or replace type ut_expectation_compound under ut_expectation( member function exclude(a_items varchar2) return ut_expectation_compound, member function exclude(a_items ut_varchar2_list) return ut_expectation_compound, member procedure exclude(self in ut_expectation_compound, a_items varchar2), - member procedure exclude(self in ut_expectation_compound, a_items ut_varchar2_list) - + member procedure exclude(self in ut_expectation_compound, a_items ut_varchar2_list), + member function unordered return ut_expectation_compound, + member procedure unordered(self in ut_expectation_compound), + member function join_by(a_columns varchar2) return ut_expectation_compound, + member function join_by(a_columns ut_varchar2_list) return ut_expectation_compound, + member procedure join_by(self in ut_expectation_compound, a_columns varchar2), + member procedure join_by(self in ut_expectation_compound, a_columns ut_varchar2_list) ) final -/ +/ \ No newline at end of file diff --git a/source/install.sql b/source/install.sql index 3050349e6..0bd30222c 100644 --- a/source/install.sql +++ b/source/install.sql @@ -162,6 +162,8 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'core/types/ut_console_reporter_base.tpb' --expectations and matchers +@@install_component.sql 'expectations/data_values/ut_key_anyval_pair.tps' +@@install_component.sql 'expectations/data_values/ut_key_anyval_pairs.tps' @@install_component.sql 'expectations/data_values/ut_compound_data_tmp.sql' @@install_component.sql 'expectations/data_values/ut_compound_data_diff_tmp.sql' @@install_component.sql 'expectations/data_values/ut_data_value.tps' @@ -181,7 +183,9 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'expectations/data_values/ut_data_value_timestamp_ltz.tps' @@install_component.sql 'expectations/data_values/ut_data_value_varchar2.tps' @@install_component.sql 'expectations/data_values/ut_data_value_yminterval.tps' +@@install_component.sql 'expectations/data_values/ut_data_value_xmltype.tps' @@install_component.sql 'expectations/data_values/ut_compound_data_helper.pks' +@@install_component.sql 'expectations/data_values/ut_curr_usr_compound_helper.pks' @@install_component.sql 'expectations/matchers/ut_matcher.tps' @@install_component.sql 'expectations/matchers/ut_comparison_matcher.tps' @@install_component.sql 'expectations/matchers/ut_be_false.tps' @@ -203,6 +207,7 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'expectations/data_values/ut_data_value.tpb' @@install_component.sql 'expectations/data_values/ut_compound_data_value.tpb' @@install_component.sql 'expectations/data_values/ut_compound_data_helper.pkb' +@@install_component.sql 'expectations/data_values/ut_curr_usr_compound_helper.pkb' @@install_component.sql 'expectations/data_values/ut_data_value_anydata.tpb' @@install_component.sql 'expectations/data_values/ut_data_value_object.tpb' @@install_component.sql 'expectations/data_values/ut_data_value_collection.tpb' @@ -218,6 +223,7 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'expectations/data_values/ut_data_value_timestamp_ltz.tpb' @@install_component.sql 'expectations/data_values/ut_data_value_varchar2.tpb' @@install_component.sql 'expectations/data_values/ut_data_value_yminterval.tpb' +@@install_component.sql 'expectations/data_values/ut_data_value_xmltype.tpb' @@install_component.sql 'expectations/matchers/ut_matcher.tpb' @@install_component.sql 'expectations/matchers/ut_comparison_matcher.tpb' @@install_component.sql 'expectations/matchers/ut_be_false.tpb' diff --git a/source/uninstall.sql b/source/uninstall.sql index 7b2ad4f66..0a8540592 100644 --- a/source/uninstall.sql +++ b/source/uninstall.sql @@ -152,6 +152,8 @@ drop type ut_data_value_collection force; drop type ut_data_value_anydata force; +drop type ut_data_value_xmltype force; + drop type ut_data_value force; drop table ut_compound_data_tmp; @@ -260,6 +262,10 @@ drop package ut_event_manager; drop type ut_event_item force; +drop type ut_key_anyval_pair force; + +drop type ut_key_anyval_pairs force; + drop type ut_key_value_pairs force; drop type ut_key_value_pair force; diff --git a/test/core/expectations/compound_data/test_expectations_cursor.pkb b/test/core/expectations/compound_data/test_expectations_cursor.pkb index b805105e6..efb1ca845 100644 --- a/test/core/expectations/compound_data/test_expectations_cursor.pkb +++ b/test/core/expectations/compound_data/test_expectations_cursor.pkb @@ -1041,5 +1041,695 @@ Rows: [ 2 differences ]% end; end; + procedure cursor_unorderd_compr_success is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select username , user_id from all_users order by username asc; + open l_expected for select username , user_id from all_users order by username desc; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_unordered_compare_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select 'test1' username,-100 user_id from dual + union all + select 'test' username,-666 user_id from dual + order by 1 asc; + + open l_expected for select 'test1' username,-100 user_id from dual + union all + select 'test' username,-667 user_id from dual + order by 1 desc; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% +%Diff:% +%Rows: [ 2 differences ]% +%Extra: test-666% +%Missing: test-667%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_joinby_compare is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select owner, object_name,object_type from all_objects where owner = user + order by 1,2,3 asc; + open l_expected for select owner, object_name,object_type from all_objects where owner = user + order by 1,2,3 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OWNER'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_joinby_compare_twocols is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select owner, object_name,object_type from all_objects where owner = user + order by 1,2,3 asc; + open l_expected for select owner, object_name,object_type from all_objects where owner = user + order by 1,2,3 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('OWNER,OBJECT_NAME')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_joinby_compare_nokey is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + open l_expected for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OWNER'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +%Unable to join sets:% +%Join key OWNER does not exists in expected% +%Join key OWNER does not exists in actual%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cur_joinby_comp_twocols_nokey is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + open l_expected for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('OWNER,USER_ID')); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +%Unable to join sets:% +%Join key OWNER does not exists in expected% +%Join key USER_ID does not exists in expected% +%Join key OWNER does not exists in actual% +%Join key USER_ID does not exists in actual%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_joinby_compare_exkey is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + open l_expected for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('SOME_COL').exclude('SOME_COL'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +%Unable to join sets:% +%Join key SOME_COL does not exists in expected% +%Join key SOME_COL does not exists in actual%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cur_joinby_comp_twocols_exkey is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + open l_expected for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('RN,SOME_COL')).exclude('RN'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +%Unable to join sets:% +%Join key RN does not exists in expected% +%Join key RN does not exists in actual%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_joinby_comp_nokey_ex is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rni, 'x' SOME_COL from dual a connect by level < 4; + open l_expected for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('RNI'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +%Unable to join sets:% +%Join key RNI does not exists in expected%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_joinby_comp_nokey_ac is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn, 'x' SOME_COL from dual a connect by level < 4; + open l_expected for select rownum as rni, 'x' SOME_COL from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('RNI'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +%Unable to join sets:% +%Join key RNI does not exists in actual%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_joinby_compare_1000 is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select object_name from all_objects where rownum <=1100; + open l_expected for select object_name from all_objects where rownum <=1100; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OBJECT_NAME'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_joinby_compare_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for select username, user_id from all_users union all + select 'TEST' username, -600 user_id from dual order by 1 desc; + + open l_actual for select username, user_id from all_users union all + select 'TEST' username, -610 user_id from dual order by 1 asc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('USERNAME'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = % ] was expected to equal: refcursor [ count = % ] +%Diff:% +%Rows: [ 1 differences ]% +%PK TEST - Actual:%-610% +%PK TEST - Expected:%-600%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_joinby_cmp_twocol_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for select username, user_id from all_users union all + select 'TEST' username, -600 user_id from dual order by 1 desc; + + open l_actual for select username, user_id from all_users union all + select 'TEST' username, -610 user_id from dual order by 1 asc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('USERNAME,USER_ID')); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = % ] was expected to equal: refcursor [ count = % ] +%Diff:% +%Rows: [ 2 differences ]% +%PK TEST-610 - Extra% +%PK TEST-600 - Missing%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cur_joinby_cmp_threcol_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for select username, user_id,'Y' is_valid from all_users union all + select 'TEST' username, -600 user_id,'Y' is_valid from dual order by 1 desc; + + open l_actual for select username, user_id,'Y' is_valid from all_users union all + select 'TEST' username, -610 user_id,'Y' is_valid from dual order by 1 asc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('USERNAME,IS_VALID')); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = % ] was expected to equal: refcursor [ count = % ] +%Diff:% +%Rows: [ 1 differences ]% +%PK TESTY - Actual:%-610% +%PK TESTY - Expected:%-600%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure unord_incl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).unordered; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure joinby_incl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('SOME_COL'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure joinby_excl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('SOME_COL'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure unord_excl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).exclude(ut3.ut_varchar2_list('A_COLUMN|//Some_Col')).unordered; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure excl_dif_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'TEST' as A_COLUMN from dual a connect by level < 4; + open l_expected for select rownum as rn, 1 as A_COLUMN from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).exclude(ut3.ut_varchar2_list('A_COLUMN')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure inlc_dif_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'TEST' as A_COLUMN from dual a connect by level < 4; + open l_expected for select rownum as rn, 1 as A_COLUMN from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).include(ut3.ut_varchar2_list('RN')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure inlc_exc_dif_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'TEST' as A_COLUMN from dual a connect by level < 4; + open l_expected for select rownum as rn, 1 as A_COLUMN from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).include(ut3.ut_varchar2_list('RN')).exclude(ut3.ut_varchar2_list('A_COLUMN')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure compare_obj_typ_col_un is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum asc; + + open l_expected for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure compare_obj_typ_col_jb is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum asc; + + open l_expected for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('COLVAL/ID'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure comp_obj_typ_col_un_fail is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum asc; + + open l_expected for select test_dummy_object( rownum, 'Somethings '||rownum, rownum) as colval + from dual connect by level <=3 order by rownum desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 3 ]% +Diff:% +Rows: [ 5 differences ] +%Extra: 2Something 22% +%Extra: 1Something 11% +%Missing: 1Somethings 11% +%Missing: 2Somethings 22% +%Missing: 3Somethings 33%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure comp_obj_typ_col_jb_fail is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum asc; + + open l_expected for select test_dummy_object( rownum, 'Somethings '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('COLVAL/ID'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_not_null(); + end; + + procedure comp_obj_typ_col_jb_multi is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn,test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum asc; + + open l_expected for select rownum as rn,test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('RN,COLVAL/ID')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure comp_obj_typ_col_jb_nokey is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum asc; + + open l_expected for select test_dummy_object( rownum, 'Something '||rownum, rownum) as colval + from dual connect by level <=2 order by rownum desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('COLVAL/IDS'); + + l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% +Diff:% +%Unable to join sets:% +%Join key COLVAL/IDS does not exists in expected% +%Join key COLVAL/IDS does not exists in actual%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure compare_nest_tab_col_jb is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_actual_tab + from dual connect by level <=2; + + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_expected_tab + from dual connect by level <=2; + + --Arrange + open l_actual for select key,value + from table(l_actual_tab) order by 1 asc; + + open l_expected for select key,value + from table(l_expected_tab) order by 1 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('KEY'); + + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure compare_nest_tab_col_jb_fail is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_actual_tab + from dual connect by level <=2; + + select ut3.ut_key_value_pair(rownum,'Somethings '||rownum) + bulk collect into l_expected_tab + from dual connect by level <=2; + + --Arrange + open l_actual for select key,value + from table(l_actual_tab) order by 1 asc; + + open l_expected for select key,value + from table(l_expected_tab) order by 1 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('KEY'); + l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% +%Diff:% +%Rows: [ 2 differences ]% +%PK % - Actual: %% +%PK % - Expected: %% +%PK % - Actual: %% +%PK % - Expected: %%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure compare_nest_tab_cols_jb is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_actual_tab + from dual connect by level <=2; + + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_expected_tab + from dual connect by level <=2; + + --Arrange + open l_actual for select key,value + from table(l_actual_tab) order by 1 asc; + + open l_expected for select key,value + from table(l_expected_tab) order by 1 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('KEY,VALUE')); + + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure compare_nest_tab_cols_jb_fail is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_actual_tab + from dual connect by level <=2; + + select ut3.ut_key_value_pair(rownum,'Somethings '||rownum) + bulk collect into l_expected_tab + from dual connect by level <=2; + + --Arrange + open l_actual for select key,value + from table(l_actual_tab) order by 1 asc; + + open l_expected for select key,value + from table(l_expected_tab) order by 1 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('KEY,VALUE')); + l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% +%Diff:% +%Rows: [ 4 differences ]% +%PK %% - Extra% +%PK %% - Extra% +%PK %% - Missing% +%PK %% - Missing%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure comparet_tabtype_as_cols_jb is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_tab ut3.ut_key_value_pairs := ut3.ut_key_value_pairs(); + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + select ut3.ut_key_value_pair(rownum,'Something '||rownum) + bulk collect into l_actual_tab + from dual connect by level <=2; + + select ut3.ut_key_value_pair(rownum,'Somethings '||rownum) + bulk collect into l_expected_tab + from dual connect by level <=2; + + --Arrange + open l_actual for select rownum rn, l_actual_tab + from dual connect by level <=2; + + open l_expected for select key,value + from table(l_expected_tab) order by 1 desc; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('RN,L_ACTUAL_TAB/KEY')); + ut3.ut.fail('Expected Exception'); + exception + when others then + l_expected_message := q'[%PLS-00306: wrong number or types of arguments in call to 'CONVERTOBJECT'%]'; + l_actual_message := SQLERRM; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + end; / diff --git a/test/core/expectations/compound_data/test_expectations_cursor.pks b/test/core/expectations/compound_data/test_expectations_cursor.pks index 4868b72ce..bd8cf45a0 100644 --- a/test/core/expectations/compound_data/test_expectations_cursor.pks +++ b/test/core/expectations/compound_data/test_expectations_cursor.pks @@ -26,7 +26,7 @@ create or replace package test_expectations_cursor is --%test(Gives success on to_be_null if cursor is null) procedure success_to_be_null; - --%test(Gives succes on not_to_be_not_null if cursor is null) + --%test(Gives success on not_to_be_not_null if cursor is null) procedure success_not_to_be_not_null; --%test(Gives success on not_to_be_null if cursor is not null) @@ -199,5 +199,101 @@ create or replace package test_expectations_cursor is --%test( Fail on passing implicit column name as exclude filter ) procedure exclude_col_name_implicit; + --%test( Compare cursors using unordered method success) + procedure cursor_unorderd_compr_success; + + --%test( Compare cursors using unordered method failure) + procedure cursor_unordered_compare_fail; + + --%test( Compare cursors join by single key ) + procedure cursor_joinby_compare; + + --%test( Compare cursors join by composite key) + procedure cursor_joinby_compare_twocols; + + --%test( Compare cursors join by single key - key doesnt exists ) + procedure cursor_joinby_compare_nokey; + + --%test( Compare cursors join by composite key - one part of key doesnt exists ) + procedure cur_joinby_comp_twocols_nokey; + + --%test( Compare cursors join by single key - key doesnt is excluded ) + procedure cursor_joinby_compare_exkey; + + --%test( Compare cursors join by composite key - one part of key is excluded exists ) + procedure cur_joinby_comp_twocols_exkey; + + --%test( Compare cursors join by single key - key doesnt exists in expected) + procedure cursor_joinby_comp_nokey_ex; + + --%test( Compare cursors join by single key - key doesnt exists in actual) + procedure cursor_joinby_comp_nokey_ac; + + --%test( Compare cursors join by single key more than 1000 rows) + procedure cursor_joinby_compare_1000; + + --%test( Compare two column cursors join by and fail to match ) + procedure cursor_joinby_compare_fail; + + --%test( Compare two column cursors join by two columns and fail to match ) + procedure cursor_joinby_cmp_twocol_fail; + + --%test( Compare three column cursors join by two columns and fail to match ) + procedure cur_joinby_cmp_threcol_fail; + + --%test(Unordered List of columns to include) + procedure unord_incl_cols_as_list; + + --%test(Join By List of columns to include) + procedure joinby_incl_cols_as_list; + + --%test(Unordered List of columns to exclude) + procedure unord_excl_cols_as_list; + + --%test(Join By List of columns to exclude) + procedure joinby_excl_cols_as_list; + + --%test(Exclude columns of different type) + procedure excl_dif_cols_as_list; + + --%test(Include column of same type leaving different type out) + procedure inlc_dif_cols_as_list; + + --%test(Include column of same type leaving different type out and exclude different type) + procedure inlc_exc_dif_cols_as_list; + + --%test(Compare object type unordered) + procedure compare_obj_typ_col_un; + + --%test(Compare object type join by) + procedure compare_obj_typ_col_jb; + + --%test(Compare nested table type unordered fail) + procedure comp_obj_typ_col_un_fail; + + --%test(Compare object type join by fail) + procedure comp_obj_typ_col_jb_fail; + + --%test(Compare object type join by multi key) + procedure comp_obj_typ_col_jb_multi; + + --%test(Compare object type join by missing nested key) + procedure comp_obj_typ_col_jb_nokey; + + --%test(Compare table type join by) + procedure compare_nest_tab_col_jb; + + --%test(Compare table type join by - Failure) + procedure compare_nest_tab_col_jb_fail; + + --%test(Compare table type join by mulitple columns) + procedure compare_nest_tab_cols_jb; + + --%test(Compare table type join by multiple columns- Failure) + procedure compare_nest_tab_cols_jb_fail; + + --%test(Compare table type as column join by multiple columns - PLS Failure) + procedure comparet_tabtype_as_cols_jb; + end; /