From ae982a69e536adb4a522a718eb3f05dee6e419ca Mon Sep 17 00:00:00 2001 From: lwasylow Date: Sat, 11 May 2019 09:32:17 +0100 Subject: [PATCH 1/8] Initial checkin --- source/core/ut_utils.pkb | 15 +++++ source/core/ut_utils.pks | 5 ++ .../data_values/ut_compound_data_helper.pkb | 17 ++++- .../data_values/ut_cursor_details.tpb | 63 +++++++++++++++++-- .../data_values/ut_cursor_details.tps | 13 ++-- .../data_values/ut_data_value_anydata.tpb | 2 + .../data_values/ut_data_value_refcursor.tpb | 16 +++-- .../expectations/test_expectation_anydata.pkb | 39 ++++++------ 8 files changed, 136 insertions(+), 34 deletions(-) diff --git a/source/core/ut_utils.pkb b/source/core/ut_utils.pkb index 7efbd6b06..12739d01b 100644 --- a/source/core/ut_utils.pkb +++ b/source/core/ut_utils.pkb @@ -798,5 +798,20 @@ create or replace package body ut_utils is return l_valid_name; end; + function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list is + l_result ut_varchar2_list := ut_varchar2_list(); + l_idx binary_integer; + begin + if a_prefix is not null then + l_idx := a_list.first; + while l_idx is not null loop + l_result.extend; + l_result(l_idx) := a_prefix||a_connector||trim(leading a_connector from a_list(l_idx)); + l_idx := a_list.next(l_idx); + end loop; + end if; + return l_result; + end; + end ut_utils; / diff --git a/source/core/ut_utils.pks b/source/core/ut_utils.pks index a2bb728e4..8a751a0d8 100644 --- a/source/core/ut_utils.pks +++ b/source/core/ut_utils.pks @@ -387,5 +387,10 @@ create or replace package ut_utils authid definer is */ function get_valid_xml_name(a_name varchar2) return varchar2; + /** + * Add prefix word to elements of list + */ + function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list; + end ut_utils; / diff --git a/source/expectations/data_values/ut_compound_data_helper.pkb b/source/expectations/data_values/ut_compound_data_helper.pkb index bd259b47d..fa736ecfb 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pkb +++ b/source/expectations/data_values/ut_compound_data_helper.pkb @@ -335,7 +335,8 @@ create or replace package body ut_compound_data_helper is l_not_equal_stmt clob; l_where_stmt clob; l_ut_owner varchar2(250) := ut_utils.ut_owner; - + l_join_by_list ut_varchar2_list; + function get_join_type(a_inclusion_compare in boolean,a_negated in boolean) return varchar2 is begin return @@ -356,12 +357,22 @@ create or replace package body ut_compound_data_helper is end; begin + /** + * We already estabilished cursor equality so now we add anydata root if we compare anydata + * to join by. + */ + l_join_by_list := + case + when a_other is of (ut_data_value_anydata) then ut_utils.add_prefix(a_join_by_list, a_other.cursor_details.get_root) + else a_join_by_list + end; + dbms_lob.createtemporary(l_compare_sql, true); --Initiate a SQL template with placeholders ut_utils.append_to_clob(l_compare_sql, g_compare_sql_template); --Generate a pieceso of dynamic SQL that will substitute placeholders gen_sql_pieces_out_of_cursor( - a_other.cursor_details.cursor_columns_info, a_join_by_list, a_unordered, + a_other.cursor_details.cursor_columns_info, l_join_by_list, a_unordered, l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_join_on_stmt, l_not_equal_stmt ); @@ -374,7 +385,7 @@ create or replace package body ut_compound_data_helper is l_compare_sql := replace(l_compare_sql,'{:join_type:}',get_join_type(a_inclusion_type,a_is_negated)); l_compare_sql := replace(l_compare_sql,'{:join_condition:}',l_join_on_stmt); - if l_not_equal_stmt is not null and ((a_join_by_list.count > 0 and not a_is_negated) or (not a_unordered)) then + if l_not_equal_stmt is not null and ((l_join_by_list.count > 0 and not a_is_negated) or (not a_unordered)) then ut_utils.append_to_clob(l_where_stmt,' ( '||l_not_equal_stmt||' ) or '); end if; --If its inclusion we expect a actual set to fully match and have no extra elements over expected diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb index e6e24eea2..d2bd56e14 100644 --- a/source/expectations/data_values/ut_cursor_details.tpb +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -146,13 +146,25 @@ create or replace type body ut_cursor_details as member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is l_result ut_varchar2_list; + l_prefix varchar2(125); begin + if self.is_anydata = 1 then + l_prefix := get_root; + end if; + --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') select fl.column_value bulk collect into l_result from table(a_expected_columns) fl where not exists ( select 1 from table(self.cursor_columns_info) c - where regexp_like(c.access_path, '^'||fl.column_value||'($|/.*)') + where regexp_like(c.access_path,'^/?'|| + case + when self.is_anydata = 1 then + l_prefix||'/'||trim (leading '/' from fl.column_value) + else + fl.column_value + end||'($|/.*)' + ) ) order by fl.column_value; return l_result; @@ -162,9 +174,14 @@ create or replace type body ut_cursor_details as l_result ut_cursor_details := self; l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); l_column ut_cursor_column; + l_prefix varchar2(125); c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; begin if l_result.cursor_columns_info is not null then + + if self.is_anydata = 1 then + l_prefix := get_root; + end if; --limit columns to those on the include items minus exclude items if a_match_options.include.items.count > 0 then @@ -181,8 +198,16 @@ create or replace type body ut_cursor_details as bulk collect into l_result.cursor_columns_info from table(self.cursor_columns_info) x where exists( - select 1 from included_columns f where regexp_like( x.access_path, '^/?'||f.col_names||'($|/.*)' ) - ); + select 1 from included_columns f where regexp_like(x.access_path,'^/?'|| + case + when self.is_anydata = 1 then + l_prefix||'/'||trim(leading '/' from f.col_names) + else + f.col_names + end||'($|/.*)' + ) + ) + or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; end if; elsif a_match_options.exclude.items.count > 0 then with excluded_columns as ( @@ -193,7 +218,13 @@ create or replace type body ut_cursor_details as bulk collect into l_result.cursor_columns_info from table(self.cursor_columns_info) x where not exists( - select 1 from excluded_columns f where regexp_like( '/'||x.access_path, '^/?'||f.col_names||'($|/.*)' ) + select 1 from excluded_columns f where regexp_like(x.access_path,'^/?'|| + case + when self.is_anydata = 1 then + l_prefix||'/'||trim(leading '/' from f.col_names) + else + f.col_names + end||'($|/.*)' ) ); end if; @@ -226,8 +257,30 @@ create or replace type body ut_cursor_details as from table(self.cursor_columns_info) t where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) having count(*) > 0; - return l_result; end; + + member procedure has_anydata(self in out nocopy ut_cursor_details, a_is_anydata in boolean :=false) is + begin + self.is_anydata := case when nvl(a_is_anydata,false) then 1 else 0 end; + end; + + member function has_anydata return boolean is + begin + return ut_utils.int_to_boolean(nvl(self.is_anydata,0)); + end; + + member function get_root return varchar2 is + l_root varchar2(250); + begin + if self.cursor_columns_info.count > 0 then + select x.access_path into l_root from table(self.cursor_columns_info) x + where x.hierarchy_level = 1; + else + l_root := null; + end if; + return l_root; + end; + end; / diff --git a/source/expectations/data_values/ut_cursor_details.tps b/source/expectations/data_values/ut_cursor_details.tps index c2aa98066..ce5aefbe7 100644 --- a/source/expectations/data_values/ut_cursor_details.tps +++ b/source/expectations/data_values/ut_cursor_details.tps @@ -16,7 +16,9 @@ create or replace type ut_cursor_details force authid current_user as object ( limitations under the License. */ cursor_columns_info ut_cursor_column_tab, - + + /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ + is_anydata number(1,0), constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, constructor function ut_cursor_details( self in out nocopy ut_cursor_details,a_cursor_number in number @@ -29,9 +31,12 @@ create or replace type ut_cursor_details force authid current_user as object ( a_level in integer, a_access_path in varchar2 ), - member function contains_collection return boolean, - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, + member function contains_collection return boolean, + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), - member function get_xml_children(a_parent_name varchar2 := null) return xmltype + member function get_xml_children(a_parent_name varchar2 := null) return xmltype, + member procedure has_anydata(self in out nocopy ut_cursor_details, a_is_anydata in boolean := false), + member function has_anydata return boolean, + member function get_root return varchar2 ) / diff --git a/source/expectations/data_values/ut_data_value_anydata.tpb b/source/expectations/data_values/ut_data_value_anydata.tpb index 19917633a..fa59ad67c 100644 --- a/source/expectations/data_values/ut_data_value_anydata.tpb +++ b/source/expectations/data_values/ut_data_value_anydata.tpb @@ -84,6 +84,7 @@ create or replace type body ut_data_value_anydata as self.extract_cursor(l_refcursor); l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); self.cursor_details := ut_cursor_details(l_cursor_number); + self.cursor_details.has_anydata(true); dbms_sql.close_cursor(l_cursor_number); elsif not l_refcursor%isopen then raise cursor_not_open; @@ -139,5 +140,6 @@ create or replace type body ut_data_value_anydata as raise value_error; end if; end; + end; / diff --git a/source/expectations/data_values/ut_data_value_refcursor.tpb b/source/expectations/data_values/ut_data_value_refcursor.tpb index ee929d097..2aac626e3 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tpb +++ b/source/expectations/data_values/ut_data_value_refcursor.tpb @@ -95,6 +95,7 @@ create or replace type body ut_data_value_refcursor as extract_cursor(l_cursor); l_cursor_number := dbms_sql.to_cursor_number(l_cursor); self.cursor_details := ut_cursor_details(l_cursor_number); + self.cursor_details.has_anydata(false); dbms_sql.close_cursor(l_cursor_number); elsif not l_cursor%isopen then raise cursor_not_open; @@ -250,8 +251,14 @@ create or replace type body ut_data_value_refcursor as if l_diff_row_count > 0 then l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, - l_diff_id, a_match_options.join_by.items, a_match_options.unordered, - a_match_options.ordered_columns(), self.extract_path + l_diff_id, + case + when + l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) + else + a_match_options.join_by.items + end, + a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path ); l_message := chr(10) ||'Rows: [ ' || l_diff_row_count ||' differences' @@ -359,17 +366,18 @@ create or replace type body ut_data_value_refcursor as l_other := treat(a_other as ut_data_value_refcursor); l_other.cursor_details.filter_columns( a_match_options ); l_self.cursor_details.filter_columns( a_match_options ); - + if a_match_options.join_by.items.count > 0 then l_result := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; end if; - + if l_result = 0 then if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then l_result := 1; end if; + l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( l_other, a_match_options.join_by.items, diff --git a/test/ut3_user/expectations/test_expectation_anydata.pkb b/test/ut3_user/expectations/test_expectation_anydata.pkb index dec64d30d..4838aabff 100644 --- a/test/ut3_user/expectations/test_expectation_anydata.pkb +++ b/test/ut3_user/expectations/test_expectation_anydata.pkb @@ -249,7 +249,7 @@ create or replace package body test_expectation_anydata is l_list ut3.ut_varchar2_list; begin --Arrange - l_list := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/Value','/TEST_DUMMY_OBJECT/ID'); + l_list := ut3.ut_varchar2_list('Value','/ID'); g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>3, "name"=>'A',"Value"=>'1') ); --Act @@ -262,7 +262,7 @@ create or replace package body test_expectation_anydata is l_list varchar2(100); begin --Arrange - l_list := 'TEST_DUMMY_OBJECT/Value,TEST_DUMMY_OBJECT/ID'; + l_list := 'Value,ID'; g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>2, "name"=>'A',"Value"=>'1') ); --Act @@ -275,7 +275,7 @@ create or replace package body test_expectation_anydata is l_xpath varchar2(100); begin --Arrange - l_xpath := '//TEST_DUMMY_OBJECT/Value|//TEST_DUMMY_OBJECT/ID'; + l_xpath := '//Value|//ID'; g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>2, "name"=>'A',"Value"=>'1') ); --Act @@ -301,7 +301,7 @@ create or replace package body test_expectation_anydata is l_list ut3.ut_varchar2_list; begin --Arrange - l_list := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/Value','TEST_DUMMY_OBJECT/ID'); + l_list := ut3.ut_varchar2_list('Value','ID'); g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'b',"Value"=>'0') ); --Act @@ -314,7 +314,7 @@ create or replace package body test_expectation_anydata is l_xpath varchar2(100); begin --Arrange - l_xpath := 'TEST_DUMMY_OBJECT/key,TEST_DUMMY_OBJECT/ID'; + l_xpath := 'key,ID'; g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'1') ); --Act @@ -327,7 +327,7 @@ create or replace package body test_expectation_anydata is l_xpath varchar2(100); begin --Arrange - l_xpath := '//TEST_DUMMY_OBJECT/key|//TEST_DUMMY_OBJECT/ID'; + l_xpath := '//key|//ID'; g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'1') ); --Act @@ -340,7 +340,7 @@ create or replace package body test_expectation_anydata is l_include varchar2(100); begin --Arrange - l_include := ' BadAttributeName, TEST_DUMMY_OBJECT/ID '; + l_include := ' BadAttributeName, ID '; g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'B',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'1') ); --Act @@ -354,8 +354,8 @@ create or replace package body test_expectation_anydata is l_include varchar2(100); begin --Arrange - l_include := 'TEST_DUMMY_OBJECT/key,TEST_DUMMY_OBJECT/ID,TEST_DUMMY_OBJECT/Value'; - l_exclude := '//TEST_DUMMY_OBJECT/key|//TEST_DUMMY_OBJECT/Value'; + l_include := 'key,ID,Value'; + l_exclude := '//key|//Value'; g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'B',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'1') ); --Act @@ -371,8 +371,8 @@ create or replace package body test_expectation_anydata is l_actual varchar2(32767); begin --Arrange - l_include := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/key','TEST_DUMMY_OBJECT/ID','TEST_DUMMY_OBJECT/Value'); - l_exclude := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/key','TEST_DUMMY_OBJECT/Value'); + l_include := ut3.ut_varchar2_list('key','ID','Value'); + l_exclude := ut3.ut_varchar2_list('key','Value'); g_test_expected := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'B',"Value"=>'0') ); g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(id=>1, "name"=>'A',"Value"=>'1') ); --Act @@ -548,7 +548,7 @@ Rows: [ 60 differences, showing first 20 ] l_expected ut3_tester_helper.test_dummy_object_list; l_list ut3.ut_varchar2_list; begin - l_list := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/Value','TEST_DUMMY_OBJECT/ID'); + l_list := ut3.ut_varchar2_list('Value','ID'); --Arrange select ut3_tester_helper.test_dummy_object( rownum, 'SomethingsDifferent '||rownum, rownum) bulk collect into l_actual @@ -567,7 +567,7 @@ Rows: [ 60 differences, showing first 20 ] l_expected ut3_tester_helper.test_dummy_object_list; l_list ut3.ut_varchar2_list; begin - l_list := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/Value','TEST_DUMMY_OBJECT/ID'); + l_list := ut3.ut_varchar2_list('Value','ID'); --Arrange select ut3_tester_helper.test_dummy_object( rownum*2, 'Something '||rownum, rownum*2) bulk collect into l_actual @@ -588,7 +588,7 @@ Rows: [ 60 differences, showing first 20 ] l_actual_message varchar2(32767); l_expected_message varchar2(32767); begin - l_list := ut3.ut_varchar2_list('TEST_DUMMY_OBJECT/name'); + l_list := ut3.ut_varchar2_list('name'); --Arrange select ut3_tester_helper.test_dummy_object( rownum, 'SomethingsDifferent '||rownum, rownum) bulk collect into l_actual @@ -602,7 +602,10 @@ Rows: [ 60 differences, showing first 20 ] l_expected_message := q'[%Actual: ut3_tester_helper.test_dummy_object_list [ count = 2 ] was expected to equal: ut3_tester_helper.test_dummy_object_list [ count = 2 ] %Diff: %Rows: [ 2 differences ] -%All rows are different as the columns are not matching.]'; +%Row No. 1 - Actual: SomethingsDifferent 1 +%Row No. 1 - Expected: Something 1 +%Row No. 2 - Actual: SomethingsDifferent 2 +%Row No. 2 - Expected: Something 2]'; l_actual_message := ut3_tester_helper.main_helper.get_failed_expectations(1); --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); @@ -841,7 +844,7 @@ Rows: [ 60 differences, showing first 20 ] from dual connect by level <=2 order by rownum desc; --Act - ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).join_by('TEST_DUMMY_OBJECT/ID'); + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).join_by('ID'); ut.expect(ut3_tester_helper.main_helper.get_failed_expectations_num).to_equal(0); end; @@ -860,7 +863,7 @@ Rows: [ 60 differences, showing first 20 ] from dual connect by level <=2 order by rownum desc; --Act - ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).join_by('TEST_DUMMY_OBJECT/ID'); + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).join_by('ID'); l_expected_message := q'[%Actual: ut3_tester_helper.test_dummy_object_list [ count = 2 ] was expected to equal: ut3_tester_helper.test_dummy_object_list [ count = 2 ] %Diff: %Rows: [ 3 differences ] @@ -928,7 +931,7 @@ Rows: [ 60 differences, showing first 20 ] g_test_actual := anydata.convertObject( ut3_tester_helper.test_dummy_object(1, 'A', '0') ); --Act - ut3.ut.expect(g_test_actual).to_equal(g_test_expected).join_by('TEST_DUMMY_OBJECT/ID'); + ut3.ut.expect(g_test_actual).to_equal(g_test_expected).join_by('ID'); ut.expect(ut3_tester_helper.main_helper.get_failed_expectations_num).to_equal(0); end; From 7106a0e880782e35b85a3cdd7e32bcc91db6205a Mon Sep 17 00:00:00 2001 From: lwasylow Date: Fri, 24 May 2019 08:50:23 +0100 Subject: [PATCH 2/8] Fixing PR comments. --- docs/userguide/advanced_data_comparison.md | 38 +++++++++++++++++++ .../data_values/ut_cursor_details.tpb | 14 +++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/userguide/advanced_data_comparison.md b/docs/userguide/advanced_data_comparison.md index f2f296755..646f77ebf 100644 --- a/docs/userguide/advanced_data_comparison.md +++ b/docs/userguide/advanced_data_comparison.md @@ -126,6 +126,44 @@ end; ``` +Example of `include / exclude` for anydata.convertCollection + +```plsql +create or replace package ut_anydata_inc_exc IS + + --%suite(Anydata) + + --%test(Anydata include and exclude) + procedure ut_anydata_test; + +end ut_anydata_inc_exc; +/ + +create or replace package body ut_anydata_inc_exc IS + + procedure ut_refcursors1 IS + l_actual ut3_tester_helper.test_dummy_object_list; + l_expected ut3_tester_helper.test_dummy_object_list; + begin + --Arrange + select ut3_tester_helper.test_dummy_object( rownum, 'Something Name'||rownum, rownum) + bulk collect into l_actual + from dual connect by level <=2; + select ut3_tester_helper.test_dummy_object( rownum, 'Something '||rownum, rownum) + bulk collect into l_expected + from dual connect by level <=2 + order by rownum desc; + --Act + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).include('ID'); +ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).exclude('name'); + end; + +end ut_sample_test; +/ +``` + + + Only the columns 'RN', "A_Column" will be compared. Column 'SOME_COL' is excluded. 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. diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb index d2bd56e14..99e1d4edc 100644 --- a/source/expectations/data_values/ut_cursor_details.tpb +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -146,10 +146,10 @@ create or replace type body ut_cursor_details as member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is l_result ut_varchar2_list; - l_prefix varchar2(125); + l_root varchar2(125); begin if self.is_anydata = 1 then - l_prefix := get_root; + l_root := get_root; end if; --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') select fl.column_value @@ -160,7 +160,7 @@ create or replace type body ut_cursor_details as where regexp_like(c.access_path,'^/?'|| case when self.is_anydata = 1 then - l_prefix||'/'||trim (leading '/' from fl.column_value) + l_root||'/'||trim (leading '/' from fl.column_value) else fl.column_value end||'($|/.*)' @@ -174,13 +174,13 @@ create or replace type body ut_cursor_details as l_result ut_cursor_details := self; l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); l_column ut_cursor_column; - l_prefix varchar2(125); + l_root varchar2(125); c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; begin if l_result.cursor_columns_info is not null then if self.is_anydata = 1 then - l_prefix := get_root; + l_root := get_root; end if; --limit columns to those on the include items minus exclude items @@ -201,7 +201,7 @@ create or replace type body ut_cursor_details as select 1 from included_columns f where regexp_like(x.access_path,'^/?'|| case when self.is_anydata = 1 then - l_prefix||'/'||trim(leading '/' from f.col_names) + l_root||'/'||trim(leading '/' from f.col_names) else f.col_names end||'($|/.*)' @@ -221,7 +221,7 @@ create or replace type body ut_cursor_details as select 1 from excluded_columns f where regexp_like(x.access_path,'^/?'|| case when self.is_anydata = 1 then - l_prefix||'/'||trim(leading '/' from f.col_names) + l_root||'/'||trim(leading '/' from f.col_names) else f.col_names end||'($|/.*)' ) From f66445b7f693668f04568e30773093193763d970 Mon Sep 17 00:00:00 2001 From: lwasylow Date: Fri, 24 May 2019 13:04:30 +0100 Subject: [PATCH 3/8] Update docs with sample --- docs/userguide/advanced_data_comparison.md | 50 ++++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/docs/userguide/advanced_data_comparison.md b/docs/userguide/advanced_data_comparison.md index 646f77ebf..6179806e5 100644 --- a/docs/userguide/advanced_data_comparison.md +++ b/docs/userguide/advanced_data_comparison.md @@ -133,36 +133,70 @@ create or replace package ut_anydata_inc_exc IS --%suite(Anydata) - --%test(Anydata include and exclude) - procedure ut_anydata_test; + --%test(Anydata include) + procedure ut_anydata_test_inc; + + --%test(Anydata exclude) + procedure ut_anydata_test_exc; end ut_anydata_inc_exc; / create or replace package body ut_anydata_inc_exc IS - procedure ut_refcursors1 IS + procedure ut_anydata_test_inc IS + l_actual ut3_tester_helper.test_dummy_object_list; + l_expected ut3_tester_helper.test_dummy_object_list; + begin + --Arrange + select ut3_tester_helper.test_dummy_object( rownum, 'Something Name'||rownum, rownum) + bulk collect into l_actual + from dual connect by level <=2 + order by rownum asc; + select ut3_tester_helper.test_dummy_object( rownum, 'Something '||rownum, rownum) + bulk collect into l_expected + from dual connect by level <=2 + order by rownum asc; + --Act + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).include('ID,Value'); + end; + + procedure ut_anydata_test_exc IS l_actual ut3_tester_helper.test_dummy_object_list; l_expected ut3_tester_helper.test_dummy_object_list; begin --Arrange select ut3_tester_helper.test_dummy_object( rownum, 'Something Name'||rownum, rownum) bulk collect into l_actual - from dual connect by level <=2; + from dual connect by level <=2 + order by rownum asc; select ut3_tester_helper.test_dummy_object( rownum, 'Something '||rownum, rownum) bulk collect into l_expected from dual connect by level <=2 - order by rownum desc; + order by rownum asc; --Act - ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).include('ID'); -ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).exclude('name'); + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).exclude('name'); end; -end ut_sample_test; +end ut_anydata_inc_exc; / + ``` +will result in : + +```sql +Anydata + Anydata include [.07 sec] + Anydata exclude [.058 sec] + +Finished in .131218 seconds +2 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s) +``` + + +Example of exclude Only the columns 'RN', "A_Column" will be compared. Column 'SOME_COL' is excluded. From fbc53254cc239f56742ee627cdf7ea004ee72f05 Mon Sep 17 00:00:00 2001 From: LUKASZ104 Date: Fri, 24 May 2019 14:43:58 +0100 Subject: [PATCH 4/8] TAG: Phase2 Adding a new attribute filterpath that is used for filtering cursor in anydata / refcursor. This value will be different from cursor in anydata as we skip root element. --- source/core/ut_utils.pkb | 1644 +++++++++-------- source/core/ut_utils.pks | 796 ++++---- .../data_values/ut_cursor_column.tpb | 139 +- .../data_values/ut_cursor_column.tps | 103 +- .../data_values/ut_cursor_details.tpb | 543 +++--- .../data_values/ut_cursor_details.tps | 83 +- .../data_values/ut_data_value_anydata.tpb | 288 ++- .../data_values/ut_data_value_refcursor.tpb | 797 ++++---- 8 files changed, 2188 insertions(+), 2205 deletions(-) diff --git a/source/core/ut_utils.pkb b/source/core/ut_utils.pkb index 2e35229e6..5b80e1f59 100644 --- a/source/core/ut_utils.pkb +++ b/source/core/ut_utils.pkb @@ -1,817 +1,827 @@ -create or replace package body ut_utils is - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - /** - * Constants regex used to validate XML name - */ - gc_invalid_first_xml_char constant varchar2(50) := '[^_a-zA-Z]'; - gc_invalid_xml_char constant varchar2(50) := '[^_a-zA-Z0-9\.-]'; - gc_full_valid_xml_name constant varchar2(50) := '^([_a-zA-Z])([_a-zA-Z0-9\.-])*$'; - - function surround_with(a_value varchar2, a_quote_char varchar2) return varchar2 is - begin - return case when a_quote_char is not null then a_quote_char||a_value||a_quote_char else a_value end; - end; - - function test_result_to_char(a_test_result integer) return varchar2 as - l_result varchar2(20); - begin - if a_test_result = gc_success then - l_result := gc_success_char; - elsif a_test_result = gc_failure then - l_result := gc_failure_char; - elsif a_test_result = gc_error then - l_result := gc_error_char; - elsif a_test_result = gc_disabled then - l_result := gc_disabled_char; - else - l_result := 'Unknown(' || coalesce(to_char(a_test_result),'NULL') || ')'; - end if ; - return l_result; - end test_result_to_char; - - - function to_test_result(a_test boolean) return integer is - l_result integer; - begin - if a_test then - l_result := gc_success; - else - l_result := gc_failure; - end if; - return l_result; - end; - - function gen_savepoint_name return varchar2 is - begin - return 's'||trim(to_char(ut_savepoint_seq.nextval,'0000000000000000000000000000')); - end; - - procedure debug_log(a_message varchar2) is - begin - $if $$ut_trace $then - dbms_output.put_line(a_message); - $else - null; - $end - end; - - procedure debug_log(a_message clob) is - l_varchars ut_varchar2_list; - begin - $if $$ut_trace $then - l_varchars := clob_to_table(a_message); - for i in 1..l_varchars.count loop - dbms_output.put_line(l_varchars(i)); - end loop; - $else - null; - $end - end; - - function to_string( - a_value varchar2, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2 is - l_result varchar2(32767); - c_length constant integer := coalesce( length( a_value ), 0 ); - c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); - c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; - begin - if c_length = 0 then - l_result := gc_null_string; - elsif c_length <= c_max_input_string_length then - l_result := surround_with(a_value, a_quote_char); - else - l_result := surround_with(substr(a_value, 1, c_overflow_substr_len ), a_quote_char) || gc_more_data_string; - end if ; - return l_result; - end; - - function to_string( - a_value clob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2 is - l_result varchar2(32767); - c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); - c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); - c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; - begin - if a_value is null then - l_result := gc_null_string; - elsif c_length = 0 then - l_result := gc_empty_string; - elsif c_length <= c_max_input_string_length then - l_result := surround_with(a_value,a_quote_char); - else - l_result := surround_with(dbms_lob.substr(a_value, c_overflow_substr_len), a_quote_char) || gc_more_data_string; - end if; - return l_result; - end; - - function to_string( - a_value blob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2 is - l_result varchar2(32767); - c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); - c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); - c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; - begin - if a_value is null then - l_result := gc_null_string; - elsif c_length = 0 then - l_result := gc_empty_string; - elsif c_length <= c_max_input_string_length then - l_result := surround_with(rawtohex(a_value),a_quote_char); - else - l_result := to_string( rawtohex(dbms_lob.substr(a_value, c_overflow_substr_len)) ); - end if ; - return l_result; - end; - - function to_string(a_value boolean) return varchar2 is - begin - return case a_value when true then 'TRUE' when false then 'FALSE' else gc_null_string end; - end; - - function to_string(a_value number) return varchar2 is - begin - return coalesce(to_char(a_value,gc_number_format), gc_null_string); - end; - - function to_string(a_value date) return varchar2 is - begin - return coalesce(to_char(a_value,gc_date_format), gc_null_string); - end; - - function to_string(a_value timestamp_unconstrained) return varchar2 is - begin - return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); - end; - - function to_string(a_value timestamp_tz_unconstrained) return varchar2 is - begin - return coalesce(to_char(a_value,gc_timestamp_tz_format), gc_null_string); - end; - - function to_string(a_value timestamp_ltz_unconstrained) return varchar2 is - begin - return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); - end; - - function to_string(a_value yminterval_unconstrained) return varchar2 IS - begin - return coalesce(to_char(a_value), gc_null_string); - end; - - function to_string(a_value dsinterval_unconstrained) return varchar2 IS - begin - return coalesce(to_char(a_value), gc_null_string); - end; - - - function boolean_to_int(a_value boolean) return integer is - begin - return case a_value when true then 1 when false then 0 end; - end; - - function int_to_boolean(a_value integer) return boolean is - begin - return case a_value when 1 then true when 0 then false end; - end; - - function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list is - l_offset integer := 1; - l_delimiter_position integer; - l_skip_leading_delimiter boolean := coalesce(a_skip_leading_delimiter = 'Y',false); - l_result ut_varchar2_list := ut_varchar2_list(); - begin - if a_string is null then - return l_result; - end if; - if a_delimiter is null then - return ut_varchar2_list(a_string); - end if; - - loop - l_delimiter_position := instr(a_string, a_delimiter, l_offset); - if not (l_delimiter_position = 1 and l_skip_leading_delimiter) then - l_result.extend; - if l_delimiter_position > 0 then - l_result(l_result.last) := substr(a_string, l_offset, l_delimiter_position - l_offset); - else - l_result(l_result.last) := substr(a_string, l_offset); - end if; - end if; - exit when l_delimiter_position = 0; - l_offset := l_delimiter_position + 1; - end loop; - return l_result; - end; - - function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list is - l_offset integer := 1; - l_length integer := dbms_lob.getlength(a_clob); - l_amount integer; - l_buffer varchar2(32767); - l_last_line varchar2(32767); - l_string_results ut_varchar2_list; - l_results ut_varchar2_list := ut_varchar2_list(); - l_has_last_line boolean; - l_skip_leading_delimiter varchar2(1) := 'N'; - begin - while l_offset <= l_length loop - l_amount := a_max_amount - coalesce( length(l_last_line), 0 ); - dbms_lob.read(a_clob, l_amount, l_offset, l_buffer); - l_offset := l_offset + l_amount; - - l_string_results := string_to_table( l_last_line || l_buffer, a_delimiter, l_skip_leading_delimiter ); - for i in 1 .. l_string_results.count loop - --if a split of lines was not done or not at the last line - if l_string_results.count = 1 or i < l_string_results.count then - l_results.extend; - l_results(l_results.last) := l_string_results(i); - end if; - end loop; - - --check if we need to append the last line to the next element - if l_string_results.count = 1 then - l_has_last_line := false; - l_last_line := null; - elsif l_string_results.count > 1 then - l_has_last_line := true; - l_last_line := l_string_results(l_string_results.count); - end if; - - l_skip_leading_delimiter := 'Y'; - end loop; - if l_has_last_line then - l_results.extend; - l_results(l_results.last) := l_last_line; - end if; - return l_results; - end; - - function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob is - l_result clob; - l_table_rows integer := coalesce(cardinality(a_text_table),0); - begin - for i in 1 .. l_table_rows loop - if i < l_table_rows then - append_to_clob(l_result, a_text_table(i)||a_delimiter); - else - append_to_clob(l_result, a_text_table(i)); - end if; - end loop; - return l_result; - end; - - function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob is - l_result clob; - l_table_rows integer := coalesce(cardinality(a_integer_table),0); - begin - for i in 1 .. l_table_rows loop - if i < l_table_rows then - append_to_clob(l_result, a_integer_table(i)||a_delimiter); - else - append_to_clob(l_result, a_integer_table(i)); - end if; - end loop; - return l_result; - end; - - function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number is - begin - return - extract(day from(a_end_time - a_start_time)) * 24 * 60 * 60 + - extract(hour from(a_end_time - a_start_time)) * 60 * 60 + - extract(minute from(a_end_time - a_start_time)) * 60 + - extract(second from(a_end_time - a_start_time)); - end; - - function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2 is - begin - if a_include_first_line then - return rtrim(lpad( ' ', a_indent_size ) || replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); - else - return rtrim(replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); - end if; - end; - - function get_utplsql_objects_list return ut_object_names is - l_result ut_object_names; - begin - select distinct ut_object_name(sys_context('userenv','current_user'), o.object_name) - bulk collect into l_result - from user_objects o - where o.object_name = 'UT' or object_name like 'UT\_%' escape '\' - and o.object_type <> 'SYNONYM'; - return l_result; - end; - - procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2) is - begin - if a_item is not null then - if a_list is null then - a_list := ut_varchar2_list(); - end if; - a_list.extend; - a_list(a_list.last) := a_item; - end if; - end append_to_list; - - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows) is - begin - if a_items is not null then - if a_list is null then - a_list := ut_varchar2_rows(); - end if; - for i in 1 .. a_items.count loop - a_list.extend; - a_list(a_list.last) := a_items(i); - end loop; - end if; - end; - - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob) is - begin - append_to_list( - a_list, - convert_collection( - clob_to_table( a_item, ut_utils.gc_max_storage_varchar2_len ) - ) - ); - end; - - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2) is - begin - if a_item is not null then - if a_list is null then - a_list := ut_varchar2_rows(); - end if; - if length(a_item) > gc_max_storage_varchar2_len then - append_to_list( - a_list, - ut_utils.convert_collection( - ut_utils.clob_to_table( a_item, gc_max_storage_varchar2_len ) - ) - ); - else - a_list.extend; - a_list(a_list.last) := a_item; - end if; - end if; - end append_to_list; - - procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2:= chr(10)) is - begin - if a_clob_table is not null and cardinality(a_clob_table) > 0 then - if a_src_clob is null then - dbms_lob.createtemporary(a_src_clob, true); - end if; - for i in 1 .. a_clob_table.count loop - dbms_lob.append(a_src_clob,a_clob_table(i)); - if i < a_clob_table.count then - append_to_clob(a_src_clob,a_delimiter); - end if; - end loop; - end if; - end; - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob) is - begin - if a_new_data is not null and dbms_lob.getlength(a_new_data) > 0 then - if a_src_clob is null then - dbms_lob.createtemporary(a_src_clob, true); - end if; - dbms_lob.append(a_src_clob, a_new_data); - end if; - end; - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2) is - begin - if a_new_data is not null then - if a_src_clob is null then - dbms_lob.createtemporary(a_src_clob, true); - end if; - dbms_lob.writeappend(a_src_clob, dbms_lob.getlength(a_new_data), a_new_data); - end if; - end; - - function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows is - l_result ut_varchar2_rows; - begin - if a_collection is not null then - l_result := ut_varchar2_rows(); - for i in 1 .. a_collection.count loop - l_result.extend(); - l_result(i) := substr(a_collection(i),1,gc_max_storage_varchar2_len); - end loop; - end if; - return l_result; - end; - - procedure set_action(a_text in varchar2) is - begin - dbms_application_info.set_module('utPLSQL', a_text); - end; - - procedure set_client_info(a_text in varchar2) is - begin - dbms_application_info.set_client_info(a_text); - end; - - function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2 is - l_xpath varchar2(32767) := a_list; - begin - l_xpath := to_xpath( clob_to_table(a_clob=>a_list, a_delimiter=>','), a_ancestors); - return l_xpath; - end; - - function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2 is - l_xpath varchar2(32767); - l_item varchar2(32767); - l_iter integer; - begin - if a_list is not null then - l_iter := a_list.first; - while l_iter is not null loop - l_item := trim(a_list(l_iter)); - if l_item is not null then - if l_item like '%,%' then - l_xpath := l_xpath || to_xpath( l_item, a_ancestors ) || '|'; - elsif l_item like '/%' then - l_xpath := l_xpath || l_item || '|'; - else - l_xpath := l_xpath || a_ancestors || l_item || '|'; - end if; - end if; - l_iter := a_list.next(l_iter); - end loop; - l_xpath := rtrim(l_xpath,',|'); - end if; - return l_xpath; - end; - - procedure cleanup_temp_tables is - begin - execute immediate 'delete from ut_compound_data_tmp'; - execute immediate 'delete from ut_compound_data_diff_tmp'; - end; - - function to_version(a_version_no varchar2) return t_version is - l_result t_version; - c_version_part_regex constant varchar2(20) := '[0-9]+'; - begin - - if regexp_like(a_version_no,'v?([0-9]+(\.|$)){1,4}') then - l_result.major := regexp_substr(a_version_no, c_version_part_regex, 1, 1); - l_result.minor := regexp_substr(a_version_no, c_version_part_regex, 1, 2); - l_result.bugfix := regexp_substr(a_version_no, c_version_part_regex, 1, 3); - l_result.build := regexp_substr(a_version_no, c_version_part_regex, 1, 4); - else - raise_application_error(gc_invalid_version_no, 'Version string "'||a_version_no||'" is not a valid version'); - end if; - return l_result; - end; - - procedure save_dbms_output_to_cache is - l_status number; - l_line varchar2(32767); - l_offset integer := 0; - l_lines ut_varchar2_rows := ut_varchar2_rows(); - c_lines_limit constant integer := 100; - pragma autonomous_transaction; - - procedure flush_lines(a_lines ut_varchar2_rows, a_offset integer) is - begin - insert into ut_dbms_output_cache (seq_no,text) - select rownum+a_offset, column_value - from table(a_lines); - end; - begin - loop - dbms_output.get_line(line => l_line, status => l_status); - exit when l_status = 1; - l_lines := l_lines multiset union all ut_utils.convert_collection(ut_utils.clob_to_table(l_line||chr(7),4000)); - if l_lines.count > c_lines_limit then - flush_lines(l_lines, l_offset); - l_offset := l_offset + l_lines.count; - l_lines.delete; - end if; - end loop; - flush_lines(l_lines, l_offset); - commit; - end; - - procedure read_cache_to_dbms_output is - l_lines_data sys_refcursor; - l_lines ut_varchar2_rows; - c_lines_limit constant integer := 100; - pragma autonomous_transaction; - begin - open l_lines_data for select text from ut_dbms_output_cache order by seq_no; - loop - fetch l_lines_data bulk collect into l_lines limit c_lines_limit; - for i in 1 .. l_lines.count loop - if substr(l_lines(i),-1) = chr(7) then - dbms_output.put_line(rtrim(l_lines(i),chr(7))); - else - dbms_output.put(l_lines(i)); - end if; - end loop; - exit when l_lines_data%notfound; - end loop; - delete from ut_dbms_output_cache; - commit; - end; - - function ut_owner return varchar2 is - begin - return sys_context('userenv','current_schema'); - end; - - function scale_cardinality(a_cardinality natural) return natural is - begin - return nvl(trunc(power(10,(floor(log(10,a_cardinality))+1))/3),0); - end; - - function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2 is - begin - return 'The syntax: "'||a_old_syntax||'" is deprecated.' ||chr(10)|| - 'Please use the new syntax: "'||a_new_syntax||'".' ||chr(10)|| - 'The deprecated syntax will not be supported in future releases.'; - end; - - function to_xml_number_format(a_value number) return varchar2 is - begin - return to_char(a_value, gc_number_format, 'NLS_NUMERIC_CHARACTERS=''. '''); - end; - - function get_xml_header(a_encoding varchar2) return varchar2 is - begin - return - ''; - end; - - function trim_list_elements(a_list ut_varchar2_list, a_regexp_to_trim varchar2 default '[:space:]') return ut_varchar2_list is - l_trimmed_list ut_varchar2_list; - l_index integer; - begin - if a_list is not null then - l_trimmed_list := ut_varchar2_list(); - l_index := a_list.first; - - while (l_index is not null) loop - l_trimmed_list.extend; - l_trimmed_list(l_trimmed_list.count) := regexp_replace(a_list(l_index), '(^['||a_regexp_to_trim||']*)|(['||a_regexp_to_trim||']*$)'); - l_index := a_list.next(l_index); - end loop; - end if; - - return l_trimmed_list; - end; - - function filter_list(a_list in ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list is - l_filtered_list ut_varchar2_list; - l_index integer; - begin - if a_list is not null then - l_filtered_list := ut_varchar2_list(); - l_index := a_list.first; - - while (l_index is not null) loop - if regexp_like(a_list(l_index), a_regexp_filter) then - l_filtered_list.extend; - l_filtered_list(l_filtered_list.count) := a_list(l_index); - end if; - l_index := a_list.next(l_index); - end loop; - end if; - - return l_filtered_list; - end; - - function xmlgen_escaped_string(a_string in varchar2) return varchar2 is - l_result varchar2(4000) := a_string; - l_sql varchar2(32767) := q'!select q'[!'||a_string||q'!]' as "!'||a_string||'" from dual'; - begin - if a_string is not null then - select extract(dbms_xmlgen.getxmltype(l_sql),'/*/*/*').getRootElement() - into l_result - from dual; - end if; - return l_result; - end; - - function replace_multiline_comments(a_source clob) return clob is - l_result clob; - l_ml_comment_start binary_integer := 1; - l_comment_start binary_integer := 1; - l_text_start binary_integer := 1; - l_escaped_text_start binary_integer := 1; - l_escaped_text_end_char varchar2(1 char); - l_end binary_integer := 1; - l_ml_comment clob; - l_newlines_count binary_integer; - l_offset binary_integer := 1; - l_length binary_integer := coalesce(dbms_lob.getlength(a_source), 0); - begin - l_ml_comment_start := instr(a_source,'/*'); - l_comment_start := instr(a_source,'--'); - l_text_start := instr(a_source,''''); - l_escaped_text_start := instr(a_source,q'[q']'); - while l_offset > 0 and l_ml_comment_start > 0 loop - - if l_ml_comment_start > 0 and (l_ml_comment_start < l_comment_start or l_comment_start = 0) - and (l_ml_comment_start < l_text_start or l_text_start = 0)and (l_ml_comment_start < l_escaped_text_start or l_escaped_text_start = 0) - then - l_end := instr(a_source,'*/',l_ml_comment_start+2); - append_to_clob(l_result, dbms_lob.substr(a_source, l_ml_comment_start-l_offset, l_offset)); - if l_end > 0 then - l_ml_comment := substr(a_source, l_ml_comment_start, l_end-l_ml_comment_start); - l_newlines_count := length( l_ml_comment ) - length( translate( l_ml_comment, 'a'||chr(10), 'a') ); - if l_newlines_count > 0 then - append_to_clob(l_result, lpad( chr(10), l_newlines_count, chr(10) ) ); - end if; - l_end := l_end + 2; - end if; - else - - if l_comment_start > 0 and (l_comment_start < l_ml_comment_start or l_ml_comment_start = 0) - and (l_comment_start < l_text_start or l_text_start = 0) and (l_comment_start < l_escaped_text_start or l_escaped_text_start = 0) - then - l_end := instr(a_source,chr(10),l_comment_start+2); - if l_end > 0 then - l_end := l_end + 1; - end if; - elsif l_text_start > 0 and (l_text_start < l_ml_comment_start or l_ml_comment_start = 0) - and (l_text_start < l_comment_start or l_comment_start = 0) and (l_text_start < l_escaped_text_start or l_escaped_text_start = 0) - then - l_end := instr(a_source,q'[']',l_text_start+1); - - --skip double quotes while searching for end of quoted text - while l_end > 0 and l_end = instr(a_source,q'['']',l_text_start+1) loop - l_end := instr(a_source,q'[']',l_end+1); - end loop; - if l_end > 0 then - l_end := l_end + 1; - end if; - - elsif l_escaped_text_start > 0 and (l_escaped_text_start < l_ml_comment_start or l_ml_comment_start = 0) - and (l_escaped_text_start < l_comment_start or l_comment_start = 0) and (l_escaped_text_start < l_text_start or l_text_start = 0) - then - --translate char "[" from the start of quoted text "q'[someting]'" into "]" - l_escaped_text_end_char := translate( substr(a_source, l_escaped_text_start + 2, 1), '[{(<', ']})>'); - l_end := instr(a_source,l_escaped_text_end_char||'''',l_escaped_text_start + 3 ); - if l_end > 0 then - l_end := l_end + 2; - end if; - end if; - - if l_end = 0 then - append_to_clob(l_result, substr(a_source, l_offset, l_length-l_offset)); - else - append_to_clob(l_result, substr(a_source, l_offset, l_end-l_offset)); - end if; - end if; - l_offset := l_end; - if l_offset >= l_ml_comment_start then - l_ml_comment_start := instr(a_source,'/*',l_offset); - end if; - if l_offset >= l_comment_start then - l_comment_start := instr(a_source,'--',l_offset); - end if; - if l_offset >= l_text_start then - l_text_start := instr(a_source,'''',l_offset); - end if; - if l_offset >= l_escaped_text_start then - l_escaped_text_start := instr(a_source,q'[q']',l_offset); - end if; - end loop; - append_to_clob(l_result, substr(a_source, l_end)); - return l_result; - end; - - function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info is - l_for_reporters ut_reporters_info := a_for_reporters; - l_results ut_reporters_info; - begin - if l_for_reporters is null then - l_for_reporters := ut_reporters_info(ut_reporter_info('UT_REPORTER_BASE','N','N','N')); - end if; - - select /*+ cardinality(f 10) */ - ut_reporter_info( - object_name => t.type_name, - is_output_reporter => - case - when f.is_output_reporter = 'Y' or t.type_name = 'UT_OUTPUT_REPORTER_BASE' - then 'Y' else 'N' - end, - is_instantiable => case when t.instantiable = 'YES' then 'Y' else 'N' end, - is_final => case when t.final = 'YES' then 'Y' else 'N' end - ) - bulk collect into l_results - from user_types t - join (select * from table(l_for_reporters) where is_final = 'N' ) f - on f.object_name = supertype_name; - - return l_results; - end; - - function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2 is - l_caller_stack_line varchar2(4000); - l_ora_search_pattern varchar2(500) := '^ORA'||a_ora_code||': (.*)$'; - begin - l_caller_stack_line := regexp_replace(srcstr => a_error_stack - ,pattern => l_ora_search_pattern - ,replacestr => null - ,position => 1 - ,occurrence => 1 - ,modifier => 'm'); - return l_caller_stack_line; - end; - - /** - * Change string into unicode to match xmlgen format _00_ - * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/adxdb/generation-of-XML-data-from-relational-data.html#GUID-5BE09A7D-80D8-4734-B9AF-4A61F27FA9B2 - * secion v3.1.7.2935-develop - */ - function char_to_xmlgen_unicode(a_character varchar2) return varchar2 is - begin - return '_x00'||rawtohex(utl_raw.cast_to_raw(a_character))||'_'; - end; - - /** - * Build valid XML column name as element names can contain letters, digits, hyphens, underscores, and periods - */ - function build_valid_xml_name(a_preprocessed_name varchar2) return varchar2 is - l_post_processed varchar2(4000); - begin - for i in (select regexp_substr( a_preprocessed_name ,'(.{1})', 1, level, null, 1 ) AS string_char,level level_no - from dual connect by level <= regexp_count(a_preprocessed_name, '(.{1})')) - loop - if i.level_no = 1 and regexp_like(i.string_char,gc_invalid_first_xml_char) then - l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); - elsif regexp_like(i.string_char,gc_invalid_xml_char) then - l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); - else - l_post_processed := l_post_processed || i.string_char; - end if; - end loop; - return l_post_processed; - end; - - function get_valid_xml_name(a_name varchar2) return varchar2 is - l_valid_name varchar2(4000); - begin - if regexp_like(a_name,gc_full_valid_xml_name) then - l_valid_name := a_name; - else - l_valid_name := build_valid_xml_name(a_name); - end if; - return l_valid_name; - end; - - function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list is - l_result ut_varchar2_list := ut_varchar2_list(); - l_idx binary_integer; - begin - if a_prefix is not null then - l_idx := a_list.first; - while l_idx is not null loop - l_result.extend; - l_result(l_idx) := a_prefix||a_connector||trim(leading a_connector from a_list(l_idx)); - l_idx := a_list.next(l_idx); - end loop; - end if; - return l_result; - end; - -end ut_utils; -/ +create or replace package body ut_utils is + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + /** + * Constants regex used to validate XML name + */ + gc_invalid_first_xml_char constant varchar2(50) := '[^_a-zA-Z]'; + gc_invalid_xml_char constant varchar2(50) := '[^_a-zA-Z0-9\.-]'; + gc_full_valid_xml_name constant varchar2(50) := '^([_a-zA-Z])([_a-zA-Z0-9\.-])*$'; + + function surround_with(a_value varchar2, a_quote_char varchar2) return varchar2 is + begin + return case when a_quote_char is not null then a_quote_char||a_value||a_quote_char else a_value end; + end; + + function test_result_to_char(a_test_result integer) return varchar2 as + l_result varchar2(20); + begin + if a_test_result = gc_success then + l_result := gc_success_char; + elsif a_test_result = gc_failure then + l_result := gc_failure_char; + elsif a_test_result = gc_error then + l_result := gc_error_char; + elsif a_test_result = gc_disabled then + l_result := gc_disabled_char; + else + l_result := 'Unknown(' || coalesce(to_char(a_test_result),'NULL') || ')'; + end if ; + return l_result; + end test_result_to_char; + + + function to_test_result(a_test boolean) return integer is + l_result integer; + begin + if a_test then + l_result := gc_success; + else + l_result := gc_failure; + end if; + return l_result; + end; + + function gen_savepoint_name return varchar2 is + begin + return 's'||trim(to_char(ut_savepoint_seq.nextval,'0000000000000000000000000000')); + end; + + procedure debug_log(a_message varchar2) is + begin + $if $$ut_trace $then + dbms_output.put_line(a_message); + $else + null; + $end + end; + + procedure debug_log(a_message clob) is + l_varchars ut_varchar2_list; + begin + $if $$ut_trace $then + l_varchars := clob_to_table(a_message); + for i in 1..l_varchars.count loop + dbms_output.put_line(l_varchars(i)); + end loop; + $else + null; + $end + end; + + function to_string( + a_value varchar2, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2 is + l_result varchar2(32767); + c_length constant integer := coalesce( length( a_value ), 0 ); + c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); + c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; + begin + if c_length = 0 then + l_result := gc_null_string; + elsif c_length <= c_max_input_string_length then + l_result := surround_with(a_value, a_quote_char); + else + l_result := surround_with(substr(a_value, 1, c_overflow_substr_len ), a_quote_char) || gc_more_data_string; + end if ; + return l_result; + end; + + function to_string( + a_value clob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2 is + l_result varchar2(32767); + c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); + c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); + c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; + begin + if a_value is null then + l_result := gc_null_string; + elsif c_length = 0 then + l_result := gc_empty_string; + elsif c_length <= c_max_input_string_length then + l_result := surround_with(a_value,a_quote_char); + else + l_result := surround_with(dbms_lob.substr(a_value, c_overflow_substr_len), a_quote_char) || gc_more_data_string; + end if; + return l_result; + end; + + function to_string( + a_value blob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2 is + l_result varchar2(32767); + c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); + c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); + c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; + begin + if a_value is null then + l_result := gc_null_string; + elsif c_length = 0 then + l_result := gc_empty_string; + elsif c_length <= c_max_input_string_length then + l_result := surround_with(rawtohex(a_value),a_quote_char); + else + l_result := to_string( rawtohex(dbms_lob.substr(a_value, c_overflow_substr_len)) ); + end if ; + return l_result; + end; + + function to_string(a_value boolean) return varchar2 is + begin + return case a_value when true then 'TRUE' when false then 'FALSE' else gc_null_string end; + end; + + function to_string(a_value number) return varchar2 is + begin + return coalesce(to_char(a_value,gc_number_format), gc_null_string); + end; + + function to_string(a_value date) return varchar2 is + begin + return coalesce(to_char(a_value,gc_date_format), gc_null_string); + end; + + function to_string(a_value timestamp_unconstrained) return varchar2 is + begin + return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); + end; + + function to_string(a_value timestamp_tz_unconstrained) return varchar2 is + begin + return coalesce(to_char(a_value,gc_timestamp_tz_format), gc_null_string); + end; + + function to_string(a_value timestamp_ltz_unconstrained) return varchar2 is + begin + return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); + end; + + function to_string(a_value yminterval_unconstrained) return varchar2 IS + begin + return coalesce(to_char(a_value), gc_null_string); + end; + + function to_string(a_value dsinterval_unconstrained) return varchar2 IS + begin + return coalesce(to_char(a_value), gc_null_string); + end; + + + function boolean_to_int(a_value boolean) return integer is + begin + return case a_value when true then 1 when false then 0 end; + end; + + function int_to_boolean(a_value integer) return boolean is + begin + return case a_value when 1 then true when 0 then false end; + end; + + function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list is + l_offset integer := 1; + l_delimiter_position integer; + l_skip_leading_delimiter boolean := coalesce(a_skip_leading_delimiter = 'Y',false); + l_result ut_varchar2_list := ut_varchar2_list(); + begin + if a_string is null then + return l_result; + end if; + if a_delimiter is null then + return ut_varchar2_list(a_string); + end if; + + loop + l_delimiter_position := instr(a_string, a_delimiter, l_offset); + if not (l_delimiter_position = 1 and l_skip_leading_delimiter) then + l_result.extend; + if l_delimiter_position > 0 then + l_result(l_result.last) := substr(a_string, l_offset, l_delimiter_position - l_offset); + else + l_result(l_result.last) := substr(a_string, l_offset); + end if; + end if; + exit when l_delimiter_position = 0; + l_offset := l_delimiter_position + 1; + end loop; + return l_result; + end; + + function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list is + l_offset integer := 1; + l_length integer := dbms_lob.getlength(a_clob); + l_amount integer; + l_buffer varchar2(32767); + l_last_line varchar2(32767); + l_string_results ut_varchar2_list; + l_results ut_varchar2_list := ut_varchar2_list(); + l_has_last_line boolean; + l_skip_leading_delimiter varchar2(1) := 'N'; + begin + while l_offset <= l_length loop + l_amount := a_max_amount - coalesce( length(l_last_line), 0 ); + dbms_lob.read(a_clob, l_amount, l_offset, l_buffer); + l_offset := l_offset + l_amount; + + l_string_results := string_to_table( l_last_line || l_buffer, a_delimiter, l_skip_leading_delimiter ); + for i in 1 .. l_string_results.count loop + --if a split of lines was not done or not at the last line + if l_string_results.count = 1 or i < l_string_results.count then + l_results.extend; + l_results(l_results.last) := l_string_results(i); + end if; + end loop; + + --check if we need to append the last line to the next element + if l_string_results.count = 1 then + l_has_last_line := false; + l_last_line := null; + elsif l_string_results.count > 1 then + l_has_last_line := true; + l_last_line := l_string_results(l_string_results.count); + end if; + + l_skip_leading_delimiter := 'Y'; + end loop; + if l_has_last_line then + l_results.extend; + l_results(l_results.last) := l_last_line; + end if; + return l_results; + end; + + function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob is + l_result clob; + l_table_rows integer := coalesce(cardinality(a_text_table),0); + begin + for i in 1 .. l_table_rows loop + if i < l_table_rows then + append_to_clob(l_result, a_text_table(i)||a_delimiter); + else + append_to_clob(l_result, a_text_table(i)); + end if; + end loop; + return l_result; + end; + + function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob is + l_result clob; + l_table_rows integer := coalesce(cardinality(a_integer_table),0); + begin + for i in 1 .. l_table_rows loop + if i < l_table_rows then + append_to_clob(l_result, a_integer_table(i)||a_delimiter); + else + append_to_clob(l_result, a_integer_table(i)); + end if; + end loop; + return l_result; + end; + + function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number is + begin + return + extract(day from(a_end_time - a_start_time)) * 24 * 60 * 60 + + extract(hour from(a_end_time - a_start_time)) * 60 * 60 + + extract(minute from(a_end_time - a_start_time)) * 60 + + extract(second from(a_end_time - a_start_time)); + end; + + function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2 is + begin + if a_include_first_line then + return rtrim(lpad( ' ', a_indent_size ) || replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); + else + return rtrim(replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); + end if; + end; + + function get_utplsql_objects_list return ut_object_names is + l_result ut_object_names; + begin + select distinct ut_object_name(sys_context('userenv','current_user'), o.object_name) + bulk collect into l_result + from user_objects o + where o.object_name = 'UT' or object_name like 'UT\_%' escape '\' + and o.object_type <> 'SYNONYM'; + return l_result; + end; + + procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2) is + begin + if a_item is not null then + if a_list is null then + a_list := ut_varchar2_list(); + end if; + a_list.extend; + a_list(a_list.last) := a_item; + end if; + end append_to_list; + + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows) is + begin + if a_items is not null then + if a_list is null then + a_list := ut_varchar2_rows(); + end if; + for i in 1 .. a_items.count loop + a_list.extend; + a_list(a_list.last) := a_items(i); + end loop; + end if; + end; + + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob) is + begin + append_to_list( + a_list, + convert_collection( + clob_to_table( a_item, ut_utils.gc_max_storage_varchar2_len ) + ) + ); + end; + + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2) is + begin + if a_item is not null then + if a_list is null then + a_list := ut_varchar2_rows(); + end if; + if length(a_item) > gc_max_storage_varchar2_len then + append_to_list( + a_list, + ut_utils.convert_collection( + ut_utils.clob_to_table( a_item, gc_max_storage_varchar2_len ) + ) + ); + else + a_list.extend; + a_list(a_list.last) := a_item; + end if; + end if; + end append_to_list; + + procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2:= chr(10)) is + begin + if a_clob_table is not null and cardinality(a_clob_table) > 0 then + if a_src_clob is null then + dbms_lob.createtemporary(a_src_clob, true); + end if; + for i in 1 .. a_clob_table.count loop + dbms_lob.append(a_src_clob,a_clob_table(i)); + if i < a_clob_table.count then + append_to_clob(a_src_clob,a_delimiter); + end if; + end loop; + end if; + end; + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob) is + begin + if a_new_data is not null and dbms_lob.getlength(a_new_data) > 0 then + if a_src_clob is null then + dbms_lob.createtemporary(a_src_clob, true); + end if; + dbms_lob.append(a_src_clob, a_new_data); + end if; + end; + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2) is + begin + if a_new_data is not null then + if a_src_clob is null then + dbms_lob.createtemporary(a_src_clob, true); + end if; + dbms_lob.writeappend(a_src_clob, dbms_lob.getlength(a_new_data), a_new_data); + end if; + end; + + function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows is + l_result ut_varchar2_rows; + begin + if a_collection is not null then + l_result := ut_varchar2_rows(); + for i in 1 .. a_collection.count loop + l_result.extend(); + l_result(i) := substr(a_collection(i),1,gc_max_storage_varchar2_len); + end loop; + end if; + return l_result; + end; + + procedure set_action(a_text in varchar2) is + begin + dbms_application_info.set_module('utPLSQL', a_text); + end; + + procedure set_client_info(a_text in varchar2) is + begin + dbms_application_info.set_client_info(a_text); + end; + + function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2 is + l_xpath varchar2(32767) := a_list; + begin + l_xpath := to_xpath( clob_to_table(a_clob=>a_list, a_delimiter=>','), a_ancestors); + return l_xpath; + end; + + function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2 is + l_xpath varchar2(32767); + l_item varchar2(32767); + l_iter integer; + begin + if a_list is not null then + l_iter := a_list.first; + while l_iter is not null loop + l_item := trim(a_list(l_iter)); + if l_item is not null then + if l_item like '%,%' then + l_xpath := l_xpath || to_xpath( l_item, a_ancestors ) || '|'; + elsif l_item like '/%' then + l_xpath := l_xpath || l_item || '|'; + else + l_xpath := l_xpath || a_ancestors || l_item || '|'; + end if; + end if; + l_iter := a_list.next(l_iter); + end loop; + l_xpath := rtrim(l_xpath,',|'); + end if; + return l_xpath; + end; + + procedure cleanup_temp_tables is + begin + execute immediate 'delete from ut_compound_data_tmp'; + execute immediate 'delete from ut_compound_data_diff_tmp'; + end; + + function to_version(a_version_no varchar2) return t_version is + l_result t_version; + c_version_part_regex constant varchar2(20) := '[0-9]+'; + begin + + if regexp_like(a_version_no,'v?([0-9]+(\.|$)){1,4}') then + l_result.major := regexp_substr(a_version_no, c_version_part_regex, 1, 1); + l_result.minor := regexp_substr(a_version_no, c_version_part_regex, 1, 2); + l_result.bugfix := regexp_substr(a_version_no, c_version_part_regex, 1, 3); + l_result.build := regexp_substr(a_version_no, c_version_part_regex, 1, 4); + else + raise_application_error(gc_invalid_version_no, 'Version string "'||a_version_no||'" is not a valid version'); + end if; + return l_result; + end; + + procedure save_dbms_output_to_cache is + l_status number; + l_line varchar2(32767); + l_offset integer := 0; + l_lines ut_varchar2_rows := ut_varchar2_rows(); + c_lines_limit constant integer := 100; + pragma autonomous_transaction; + + procedure flush_lines(a_lines ut_varchar2_rows, a_offset integer) is + begin + insert into ut_dbms_output_cache (seq_no,text) + select rownum+a_offset, column_value + from table(a_lines); + end; + begin + loop + dbms_output.get_line(line => l_line, status => l_status); + exit when l_status = 1; + l_lines := l_lines multiset union all ut_utils.convert_collection(ut_utils.clob_to_table(l_line||chr(7),4000)); + if l_lines.count > c_lines_limit then + flush_lines(l_lines, l_offset); + l_offset := l_offset + l_lines.count; + l_lines.delete; + end if; + end loop; + flush_lines(l_lines, l_offset); + commit; + end; + + procedure read_cache_to_dbms_output is + l_lines_data sys_refcursor; + l_lines ut_varchar2_rows; + c_lines_limit constant integer := 100; + pragma autonomous_transaction; + begin + open l_lines_data for select text from ut_dbms_output_cache order by seq_no; + loop + fetch l_lines_data bulk collect into l_lines limit c_lines_limit; + for i in 1 .. l_lines.count loop + if substr(l_lines(i),-1) = chr(7) then + dbms_output.put_line(rtrim(l_lines(i),chr(7))); + else + dbms_output.put(l_lines(i)); + end if; + end loop; + exit when l_lines_data%notfound; + end loop; + delete from ut_dbms_output_cache; + commit; + end; + + function ut_owner return varchar2 is + begin + return sys_context('userenv','current_schema'); + end; + + function scale_cardinality(a_cardinality natural) return natural is + begin + return nvl(trunc(power(10,(floor(log(10,a_cardinality))+1))/3),0); + end; + + function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2 is + begin + return 'The syntax: "'||a_old_syntax||'" is deprecated.' ||chr(10)|| + 'Please use the new syntax: "'||a_new_syntax||'".' ||chr(10)|| + 'The deprecated syntax will not be supported in future releases.'; + end; + + function to_xml_number_format(a_value number) return varchar2 is + begin + return to_char(a_value, gc_number_format, 'NLS_NUMERIC_CHARACTERS=''. '''); + end; + + function get_xml_header(a_encoding varchar2) return varchar2 is + begin + return + ''; + end; + + function trim_list_elements(a_list ut_varchar2_list, a_regexp_to_trim varchar2 default '[:space:]') return ut_varchar2_list is + l_trimmed_list ut_varchar2_list; + l_index integer; + begin + if a_list is not null then + l_trimmed_list := ut_varchar2_list(); + l_index := a_list.first; + + while (l_index is not null) loop + l_trimmed_list.extend; + l_trimmed_list(l_trimmed_list.count) := regexp_replace(a_list(l_index), '(^['||a_regexp_to_trim||']*)|(['||a_regexp_to_trim||']*$)'); + l_index := a_list.next(l_index); + end loop; + end if; + + return l_trimmed_list; + end; + + function filter_list(a_list in ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list is + l_filtered_list ut_varchar2_list; + l_index integer; + begin + if a_list is not null then + l_filtered_list := ut_varchar2_list(); + l_index := a_list.first; + + while (l_index is not null) loop + if regexp_like(a_list(l_index), a_regexp_filter) then + l_filtered_list.extend; + l_filtered_list(l_filtered_list.count) := a_list(l_index); + end if; + l_index := a_list.next(l_index); + end loop; + end if; + + return l_filtered_list; + end; + + function xmlgen_escaped_string(a_string in varchar2) return varchar2 is + l_result varchar2(4000) := a_string; + l_sql varchar2(32767) := q'!select q'[!'||a_string||q'!]' as "!'||a_string||'" from dual'; + begin + if a_string is not null then + select extract(dbms_xmlgen.getxmltype(l_sql),'/*/*/*').getRootElement() + into l_result + from dual; + end if; + return l_result; + end; + + function replace_multiline_comments(a_source clob) return clob is + l_result clob; + l_ml_comment_start binary_integer := 1; + l_comment_start binary_integer := 1; + l_text_start binary_integer := 1; + l_escaped_text_start binary_integer := 1; + l_escaped_text_end_char varchar2(1 char); + l_end binary_integer := 1; + l_ml_comment clob; + l_newlines_count binary_integer; + l_offset binary_integer := 1; + l_length binary_integer := coalesce(dbms_lob.getlength(a_source), 0); + begin + l_ml_comment_start := instr(a_source,'/*'); + l_comment_start := instr(a_source,'--'); + l_text_start := instr(a_source,''''); + l_escaped_text_start := instr(a_source,q'[q']'); + while l_offset > 0 and l_ml_comment_start > 0 loop + + if l_ml_comment_start > 0 and (l_ml_comment_start < l_comment_start or l_comment_start = 0) + and (l_ml_comment_start < l_text_start or l_text_start = 0)and (l_ml_comment_start < l_escaped_text_start or l_escaped_text_start = 0) + then + l_end := instr(a_source,'*/',l_ml_comment_start+2); + append_to_clob(l_result, dbms_lob.substr(a_source, l_ml_comment_start-l_offset, l_offset)); + if l_end > 0 then + l_ml_comment := substr(a_source, l_ml_comment_start, l_end-l_ml_comment_start); + l_newlines_count := length( l_ml_comment ) - length( translate( l_ml_comment, 'a'||chr(10), 'a') ); + if l_newlines_count > 0 then + append_to_clob(l_result, lpad( chr(10), l_newlines_count, chr(10) ) ); + end if; + l_end := l_end + 2; + end if; + else + + if l_comment_start > 0 and (l_comment_start < l_ml_comment_start or l_ml_comment_start = 0) + and (l_comment_start < l_text_start or l_text_start = 0) and (l_comment_start < l_escaped_text_start or l_escaped_text_start = 0) + then + l_end := instr(a_source,chr(10),l_comment_start+2); + if l_end > 0 then + l_end := l_end + 1; + end if; + elsif l_text_start > 0 and (l_text_start < l_ml_comment_start or l_ml_comment_start = 0) + and (l_text_start < l_comment_start or l_comment_start = 0) and (l_text_start < l_escaped_text_start or l_escaped_text_start = 0) + then + l_end := instr(a_source,q'[']',l_text_start+1); + + --skip double quotes while searching for end of quoted text + while l_end > 0 and l_end = instr(a_source,q'['']',l_text_start+1) loop + l_end := instr(a_source,q'[']',l_end+1); + end loop; + if l_end > 0 then + l_end := l_end + 1; + end if; + + elsif l_escaped_text_start > 0 and (l_escaped_text_start < l_ml_comment_start or l_ml_comment_start = 0) + and (l_escaped_text_start < l_comment_start or l_comment_start = 0) and (l_escaped_text_start < l_text_start or l_text_start = 0) + then + --translate char "[" from the start of quoted text "q'[someting]'" into "]" + l_escaped_text_end_char := translate( substr(a_source, l_escaped_text_start + 2, 1), '[{(<', ']})>'); + l_end := instr(a_source,l_escaped_text_end_char||'''',l_escaped_text_start + 3 ); + if l_end > 0 then + l_end := l_end + 2; + end if; + end if; + + if l_end = 0 then + append_to_clob(l_result, substr(a_source, l_offset, l_length-l_offset)); + else + append_to_clob(l_result, substr(a_source, l_offset, l_end-l_offset)); + end if; + end if; + l_offset := l_end; + if l_offset >= l_ml_comment_start then + l_ml_comment_start := instr(a_source,'/*',l_offset); + end if; + if l_offset >= l_comment_start then + l_comment_start := instr(a_source,'--',l_offset); + end if; + if l_offset >= l_text_start then + l_text_start := instr(a_source,'''',l_offset); + end if; + if l_offset >= l_escaped_text_start then + l_escaped_text_start := instr(a_source,q'[q']',l_offset); + end if; + end loop; + append_to_clob(l_result, substr(a_source, l_end)); + return l_result; + end; + + function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info is + l_for_reporters ut_reporters_info := a_for_reporters; + l_results ut_reporters_info; + begin + if l_for_reporters is null then + l_for_reporters := ut_reporters_info(ut_reporter_info('UT_REPORTER_BASE','N','N','N')); + end if; + + select /*+ cardinality(f 10) */ + ut_reporter_info( + object_name => t.type_name, + is_output_reporter => + case + when f.is_output_reporter = 'Y' or t.type_name = 'UT_OUTPUT_REPORTER_BASE' + then 'Y' else 'N' + end, + is_instantiable => case when t.instantiable = 'YES' then 'Y' else 'N' end, + is_final => case when t.final = 'YES' then 'Y' else 'N' end + ) + bulk collect into l_results + from user_types t + join (select * from table(l_for_reporters) where is_final = 'N' ) f + on f.object_name = supertype_name; + + return l_results; + end; + + function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2 is + l_caller_stack_line varchar2(4000); + l_ora_search_pattern varchar2(500) := '^ORA'||a_ora_code||': (.*)$'; + begin + l_caller_stack_line := regexp_replace(srcstr => a_error_stack + ,pattern => l_ora_search_pattern + ,replacestr => null + ,position => 1 + ,occurrence => 1 + ,modifier => 'm'); + return l_caller_stack_line; + end; + + /** + * Change string into unicode to match xmlgen format _00_ + * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/adxdb/generation-of-XML-data-from-relational-data.html#GUID-5BE09A7D-80D8-4734-B9AF-4A61F27FA9B2 + * secion v3.1.7.2935-develop + */ + function char_to_xmlgen_unicode(a_character varchar2) return varchar2 is + begin + return '_x00'||rawtohex(utl_raw.cast_to_raw(a_character))||'_'; + end; + + /** + * Build valid XML column name as element names can contain letters, digits, hyphens, underscores, and periods + */ + function build_valid_xml_name(a_preprocessed_name varchar2) return varchar2 is + l_post_processed varchar2(4000); + begin + for i in (select regexp_substr( a_preprocessed_name ,'(.{1})', 1, level, null, 1 ) AS string_char,level level_no + from dual connect by level <= regexp_count(a_preprocessed_name, '(.{1})')) + loop + if i.level_no = 1 and regexp_like(i.string_char,gc_invalid_first_xml_char) then + l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); + elsif regexp_like(i.string_char,gc_invalid_xml_char) then + l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); + else + l_post_processed := l_post_processed || i.string_char; + end if; + end loop; + return l_post_processed; + end; + + function get_valid_xml_name(a_name varchar2) return varchar2 is + l_valid_name varchar2(4000); + begin + if regexp_like(a_name,gc_full_valid_xml_name) then + l_valid_name := a_name; + else + l_valid_name := build_valid_xml_name(a_name); + end if; + return l_valid_name; + end; + + function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list is + l_result ut_varchar2_list := ut_varchar2_list(); + l_idx binary_integer; + begin + if a_prefix is not null then + l_idx := a_list.first; + while l_idx is not null loop + l_result.extend; + l_result(l_idx) := add_prefix(a_list(l_idx), a_prefix, a_connector); + l_idx := a_list.next(l_idx); + end loop; + end if; + return l_result; + end; + + function add_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2 is + begin + return a_prefix||a_connector||trim(leading a_connector from a_item); + end; + + function strip_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2 is + begin + return regexp_replace(a_item,a_prefix||a_connector); + end; + +end ut_utils; +/ diff --git a/source/core/ut_utils.pks b/source/core/ut_utils.pks index 3739819fb..03e553ef2 100644 --- a/source/core/ut_utils.pks +++ b/source/core/ut_utils.pks @@ -1,396 +1,400 @@ -create or replace package ut_utils authid definer is - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - /** - * Common utilities and constants used throughout utPLSQL framework - * - */ - - gc_version constant varchar2(50) := 'v3.1.7.2935-develop'; - - subtype t_executable_type is varchar2(30); - gc_before_all constant t_executable_type := 'beforeall'; - gc_before_each constant t_executable_type := 'beforeeach'; - gc_before_test constant t_executable_type := 'beforetest'; - gc_test_execute constant t_executable_type := 'test'; - gc_after_test constant t_executable_type := 'aftertest'; - gc_after_each constant t_executable_type := 'aftereach'; - gc_after_all constant t_executable_type := 'afterall'; - - /* Constants: Test Results */ - subtype t_test_result is binary_integer range 0 .. 3; - gc_disabled constant t_test_result := 0; -- test/suite was disabled - gc_success constant t_test_result := 1; -- test passed - gc_failure constant t_test_result := 2; -- one or more expectations failed - gc_error constant t_test_result := 3; -- exception was raised - - gc_disabled_char constant varchar2(8) := 'Disabled'; -- test/suite was disabled - gc_success_char constant varchar2(7) := 'Success'; -- test passed - gc_failure_char constant varchar2(7) := 'Failure'; -- one or more expectations failed - gc_error_char constant varchar2(5) := 'Error'; -- exception was raised - - /* - Constants: Rollback type for ut_test_object - */ - subtype t_rollback_type is binary_integer range 0 .. 1; - gc_rollback_auto constant t_rollback_type := 0; -- rollback after each test and suite - gc_rollback_manual constant t_rollback_type := 1; -- leave transaction control manual - gc_rollback_default constant t_rollback_type := gc_rollback_auto; - - ex_unsupported_rollback_type exception; - gc_unsupported_rollback_type constant pls_integer := -20200; - pragma exception_init(ex_unsupported_rollback_type, -20200); - - ex_path_list_is_empty exception; - gc_path_list_is_empty constant pls_integer := -20201; - pragma exception_init(ex_path_list_is_empty, -20201); - - ex_invalid_path_format exception; - gc_invalid_path_format constant pls_integer := -20202; - pragma exception_init(ex_invalid_path_format, -20202); - - ex_suite_package_not_found exception; - gc_suite_package_not_found constant pls_integer := -20204; - pragma exception_init(ex_suite_package_not_found, -20204); - - -- Reporting event time not supported - ex_invalid_rep_event_time exception; - gc_invalid_rep_event_time constant pls_integer := -20210; - pragma exception_init(ex_invalid_rep_event_time, -20210); - - -- Reporting event name not supported - ex_invalid_rep_event_name exception; - gc_invalid_rep_event_name constant pls_integer := -20211; - pragma exception_init(ex_invalid_rep_event_name, -20211); - - -- Any of tests failed - ex_some_tests_failed exception; - gc_some_tests_failed constant pls_integer := -20213; - pragma exception_init(ex_some_tests_failed, -20213); - - -- Version number provided is not in valid format - ex_invalid_version_no exception; - gc_invalid_version_no constant pls_integer := -20214; - pragma exception_init(ex_invalid_version_no, -20214); - - -- Version number provided is not in valid format - ex_out_buffer_timeout exception; - gc_out_buffer_timeout constant pls_integer := -20215; - pragma exception_init(ex_out_buffer_timeout, -20215); - - ex_invalid_package exception; - gc_invalid_package constant pls_integer := -6550; - pragma exception_init(ex_invalid_package, -6550); - - ex_failure_for_all exception; - gc_failure_for_all constant pls_integer := -24381; - pragma exception_init (ex_failure_for_all, -24381); - - ex_dml_for_all exception; - gc_dml_for_all constant pls_integer := -20216; - pragma exception_init (ex_dml_for_all, -20216); - - ex_value_too_large exception; - gc_value_too_large constant pls_integer := -20217; - pragma exception_init (ex_value_too_large, -20217); - - ex_xml_processing exception; - gc_xml_processing constant pls_integer := -19202; - pragma exception_init (ex_xml_processing, -19202); - - ex_failed_open_cur exception; - gc_failed_open_cur constant pls_integer := -20218; - pragma exception_init (ex_failed_open_cur, -20218); - - gc_max_storage_varchar2_len constant integer := 4000; - gc_max_output_string_length constant integer := 4000; - gc_more_data_string constant varchar2(5) := '[...]'; - gc_more_data_string_len constant integer := length( gc_more_data_string ); - gc_number_format constant varchar2(100) := 'TM9'; - gc_date_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ss'; - gc_timestamp_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff'; - gc_timestamp_tz_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff tzh:tzm'; - gc_null_string constant varchar2(4) := 'NULL'; - gc_empty_string constant varchar2(5) := 'EMPTY'; - - gc_bc_fetch_limit constant integer := 1000; - gc_diff_max_rows constant integer := 20; - - type t_version is record( - major natural, - minor natural, - bugfix natural, - build natural - ); - - type t_clob_tab is table of clob; - - /** - * Converts test results into strings - * - * @param a_test_result numeric representation of test result - * - * @return a string representation of a test_result. - */ - function test_result_to_char(a_test_result integer) return varchar2; - - function to_test_result(a_test boolean) return integer; - - /** - * Generates a unique name for a savepoint - * Uses sys_guid, as timestamp gives only miliseconds on Windows and is not unique - * Issue: #506 for details on the implementation approach - */ - function gen_savepoint_name return varchar2; - - procedure debug_log(a_message varchar2); - - procedure debug_log(a_message clob); - - function to_string( - a_value varchar2, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2; - - function to_string( - a_value clob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2; - - function to_string( - a_value blob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2; - - function to_string(a_value boolean) return varchar2; - - function to_string(a_value number) return varchar2; - - function to_string(a_value date) return varchar2; - - function to_string(a_value timestamp_unconstrained) return varchar2; - - function to_string(a_value timestamp_tz_unconstrained) return varchar2; - - function to_string(a_value timestamp_ltz_unconstrained) return varchar2; - - function to_string(a_value yminterval_unconstrained) return varchar2; - - function to_string(a_value dsinterval_unconstrained) return varchar2; - - function boolean_to_int(a_value boolean) return integer; - - function int_to_boolean(a_value integer) return boolean; - - /** - * - * Splits a given string into table of string by delimiter. - * The delimiter gets removed. - * If null passed as any of the parameters, empty table is returned. - * If no occurence of a_delimiter found in a_text then text is returned as a single row of the table. - * If no text between delimiters found then an empty row is returned, example: - * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); - * - * @param a_string the text to be split. - * @param a_delimiter the delimiter character or string - * @param a_skip_leading_delimiter determines if the leading delimiter should be ignored, used by clob_to_table - * - * @return table of varchar2 values - */ - function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list; - - /** - * Splits a given string into table of string by delimiter. - * Default value of a_max_amount is 8191 because of code can contains multibyte character. - * The delimiter gets removed. - * If null passed as any of the parameters, empty table is returned. - * If split text is longer than a_max_amount it gets split into pieces of a_max_amount. - * If no text between delimiters found then an empty row is returned, example: - * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); - * - * @param a_clob the text to be split. - * @param a_delimiter the delimiter character or string (default chr(10) ) - * @param a_max_amount the maximum length of returned string (default 8191) - * @return table of varchar2 values - */ - function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list; - - function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob; - - function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob; - - /** - * Returns time difference in seconds (with miliseconds) between given timestamps - */ - function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number; - - /** - * Returns a text indented with spaces except the first line. - */ - function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2; - - - /** - * Returns a list of object that are part of utPLSQL framework - */ - function get_utplsql_objects_list return ut_object_names; - - /** - * Append a item to the end of ut_varchar2_list - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2); - - /** - * Append a item to the end of ut_varchar2_rows - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2); - - /** - * Append a item to the end of ut_varchar2_rows - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob); - - /** - * Append a list of items to the end of ut_varchar2_rows - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows); - - procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2 := chr(10)); - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob); - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2); - - function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows; - - /** - * Set session's action and module using dbms_application_info - */ - procedure set_action(a_text in varchar2); - - /** - * Set session's client info using dbms_application_info - */ - procedure set_client_info(a_text in varchar2); - - function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2; - - function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2; - - procedure cleanup_temp_tables; - - /** - * Converts version string into version record - * - * @param a_version_no string representation of version in format vX.X.X.X where X is a positive integer - * @return t_version record with up to four positive numbers containing version - * @throws 20214 if passed version string is not matching version pattern - */ - function to_version(a_version_no varchar2) return t_version; - - - /** - * Saves data from dbms_output buffer into a global temporary table (cache) - * used to store dbms_output buffer captured before the run - * - */ - procedure save_dbms_output_to_cache; - - /** - * Reads data from global temporary table (cache) abd puts it back into dbms_output - * used to recover dbms_output buffer data after a run is complete - * - */ - procedure read_cache_to_dbms_output; - - - /** - * Function is used to reference to utPLSQL owned objects in dynamic sql statements executed from packages with invoker rights - * - * @return the name of the utPSQL schema owner - */ - function ut_owner return varchar2; - - - /** - * Used in dynamic sql select statements to maintain balance between - * number of hard-parses and optimiser accurancy for cardinality of collections - * - * - * @return 3, for inputs of: 1-9; 33 for input of 10 - 99; 333 for (100 - 999) - */ - function scale_cardinality(a_cardinality natural) return natural; - - function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2; - - /** - * Returns number as string. The value is represented as decimal according to XML standard: - * https://www.w3.org/TR/xmlschema-2/#decimal - */ - function to_xml_number_format(a_value number) return varchar2; - - - /** - * Returns xml header. If a_encoding is not null, header will include encoding attribute with provided value - */ - function get_xml_header(a_encoding varchar2) return varchar2; - - - /** - * Takes a collection of type ut_varchar2_list and it trims the characters passed as arguments for every element - */ - function trim_list_elements(a_list IN ut_varchar2_list, a_regexp_to_trim in varchar2 default '[:space:]') return ut_varchar2_list; - - /** - * Takes a collection of type ut_varchar2_list and it only returns the elements which meets the regular expression - */ - function filter_list(a_list IN ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list; - - -- Generates XMLGEN escaped string - function xmlgen_escaped_string(a_string in varchar2) return varchar2; - - /** - * Replaces multi-line comments in given source-code with empty lines - */ - function replace_multiline_comments(a_source clob) return clob; - - /** - * Returns list of sub-type reporters for given list of super-type reporters - */ - function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info; - - /** - * Remove given ORA error from stack - */ - function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2; - - /** - * Check if xml name is valid if not build a valid name - */ - function get_valid_xml_name(a_name varchar2) return varchar2; - - /** - * Add prefix word to elements of list - */ - function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list; - -end ut_utils; -/ +create or replace package ut_utils authid definer is + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + /** + * Common utilities and constants used throughout utPLSQL framework + * + */ + + gc_version constant varchar2(50) := 'v3.1.7.2935-develop'; + + subtype t_executable_type is varchar2(30); + gc_before_all constant t_executable_type := 'beforeall'; + gc_before_each constant t_executable_type := 'beforeeach'; + gc_before_test constant t_executable_type := 'beforetest'; + gc_test_execute constant t_executable_type := 'test'; + gc_after_test constant t_executable_type := 'aftertest'; + gc_after_each constant t_executable_type := 'aftereach'; + gc_after_all constant t_executable_type := 'afterall'; + + /* Constants: Test Results */ + subtype t_test_result is binary_integer range 0 .. 3; + gc_disabled constant t_test_result := 0; -- test/suite was disabled + gc_success constant t_test_result := 1; -- test passed + gc_failure constant t_test_result := 2; -- one or more expectations failed + gc_error constant t_test_result := 3; -- exception was raised + + gc_disabled_char constant varchar2(8) := 'Disabled'; -- test/suite was disabled + gc_success_char constant varchar2(7) := 'Success'; -- test passed + gc_failure_char constant varchar2(7) := 'Failure'; -- one or more expectations failed + gc_error_char constant varchar2(5) := 'Error'; -- exception was raised + + /* + Constants: Rollback type for ut_test_object + */ + subtype t_rollback_type is binary_integer range 0 .. 1; + gc_rollback_auto constant t_rollback_type := 0; -- rollback after each test and suite + gc_rollback_manual constant t_rollback_type := 1; -- leave transaction control manual + gc_rollback_default constant t_rollback_type := gc_rollback_auto; + + ex_unsupported_rollback_type exception; + gc_unsupported_rollback_type constant pls_integer := -20200; + pragma exception_init(ex_unsupported_rollback_type, -20200); + + ex_path_list_is_empty exception; + gc_path_list_is_empty constant pls_integer := -20201; + pragma exception_init(ex_path_list_is_empty, -20201); + + ex_invalid_path_format exception; + gc_invalid_path_format constant pls_integer := -20202; + pragma exception_init(ex_invalid_path_format, -20202); + + ex_suite_package_not_found exception; + gc_suite_package_not_found constant pls_integer := -20204; + pragma exception_init(ex_suite_package_not_found, -20204); + + -- Reporting event time not supported + ex_invalid_rep_event_time exception; + gc_invalid_rep_event_time constant pls_integer := -20210; + pragma exception_init(ex_invalid_rep_event_time, -20210); + + -- Reporting event name not supported + ex_invalid_rep_event_name exception; + gc_invalid_rep_event_name constant pls_integer := -20211; + pragma exception_init(ex_invalid_rep_event_name, -20211); + + -- Any of tests failed + ex_some_tests_failed exception; + gc_some_tests_failed constant pls_integer := -20213; + pragma exception_init(ex_some_tests_failed, -20213); + + -- Version number provided is not in valid format + ex_invalid_version_no exception; + gc_invalid_version_no constant pls_integer := -20214; + pragma exception_init(ex_invalid_version_no, -20214); + + -- Version number provided is not in valid format + ex_out_buffer_timeout exception; + gc_out_buffer_timeout constant pls_integer := -20215; + pragma exception_init(ex_out_buffer_timeout, -20215); + + ex_invalid_package exception; + gc_invalid_package constant pls_integer := -6550; + pragma exception_init(ex_invalid_package, -6550); + + ex_failure_for_all exception; + gc_failure_for_all constant pls_integer := -24381; + pragma exception_init (ex_failure_for_all, -24381); + + ex_dml_for_all exception; + gc_dml_for_all constant pls_integer := -20216; + pragma exception_init (ex_dml_for_all, -20216); + + ex_value_too_large exception; + gc_value_too_large constant pls_integer := -20217; + pragma exception_init (ex_value_too_large, -20217); + + ex_xml_processing exception; + gc_xml_processing constant pls_integer := -19202; + pragma exception_init (ex_xml_processing, -19202); + + ex_failed_open_cur exception; + gc_failed_open_cur constant pls_integer := -20218; + pragma exception_init (ex_failed_open_cur, -20218); + + gc_max_storage_varchar2_len constant integer := 4000; + gc_max_output_string_length constant integer := 4000; + gc_more_data_string constant varchar2(5) := '[...]'; + gc_more_data_string_len constant integer := length( gc_more_data_string ); + gc_number_format constant varchar2(100) := 'TM9'; + gc_date_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ss'; + gc_timestamp_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff'; + gc_timestamp_tz_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff tzh:tzm'; + gc_null_string constant varchar2(4) := 'NULL'; + gc_empty_string constant varchar2(5) := 'EMPTY'; + + gc_bc_fetch_limit constant integer := 1000; + gc_diff_max_rows constant integer := 20; + + type t_version is record( + major natural, + minor natural, + bugfix natural, + build natural + ); + + type t_clob_tab is table of clob; + + /** + * Converts test results into strings + * + * @param a_test_result numeric representation of test result + * + * @return a string representation of a test_result. + */ + function test_result_to_char(a_test_result integer) return varchar2; + + function to_test_result(a_test boolean) return integer; + + /** + * Generates a unique name for a savepoint + * Uses sys_guid, as timestamp gives only miliseconds on Windows and is not unique + * Issue: #506 for details on the implementation approach + */ + function gen_savepoint_name return varchar2; + + procedure debug_log(a_message varchar2); + + procedure debug_log(a_message clob); + + function to_string( + a_value varchar2, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2; + + function to_string( + a_value clob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2; + + function to_string( + a_value blob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2; + + function to_string(a_value boolean) return varchar2; + + function to_string(a_value number) return varchar2; + + function to_string(a_value date) return varchar2; + + function to_string(a_value timestamp_unconstrained) return varchar2; + + function to_string(a_value timestamp_tz_unconstrained) return varchar2; + + function to_string(a_value timestamp_ltz_unconstrained) return varchar2; + + function to_string(a_value yminterval_unconstrained) return varchar2; + + function to_string(a_value dsinterval_unconstrained) return varchar2; + + function boolean_to_int(a_value boolean) return integer; + + function int_to_boolean(a_value integer) return boolean; + + /** + * + * Splits a given string into table of string by delimiter. + * The delimiter gets removed. + * If null passed as any of the parameters, empty table is returned. + * If no occurence of a_delimiter found in a_text then text is returned as a single row of the table. + * If no text between delimiters found then an empty row is returned, example: + * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); + * + * @param a_string the text to be split. + * @param a_delimiter the delimiter character or string + * @param a_skip_leading_delimiter determines if the leading delimiter should be ignored, used by clob_to_table + * + * @return table of varchar2 values + */ + function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list; + + /** + * Splits a given string into table of string by delimiter. + * Default value of a_max_amount is 8191 because of code can contains multibyte character. + * The delimiter gets removed. + * If null passed as any of the parameters, empty table is returned. + * If split text is longer than a_max_amount it gets split into pieces of a_max_amount. + * If no text between delimiters found then an empty row is returned, example: + * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); + * + * @param a_clob the text to be split. + * @param a_delimiter the delimiter character or string (default chr(10) ) + * @param a_max_amount the maximum length of returned string (default 8191) + * @return table of varchar2 values + */ + function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list; + + function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob; + + function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob; + + /** + * Returns time difference in seconds (with miliseconds) between given timestamps + */ + function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number; + + /** + * Returns a text indented with spaces except the first line. + */ + function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2; + + + /** + * Returns a list of object that are part of utPLSQL framework + */ + function get_utplsql_objects_list return ut_object_names; + + /** + * Append a item to the end of ut_varchar2_list + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2); + + /** + * Append a item to the end of ut_varchar2_rows + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2); + + /** + * Append a item to the end of ut_varchar2_rows + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob); + + /** + * Append a list of items to the end of ut_varchar2_rows + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows); + + procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2 := chr(10)); + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob); + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2); + + function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows; + + /** + * Set session's action and module using dbms_application_info + */ + procedure set_action(a_text in varchar2); + + /** + * Set session's client info using dbms_application_info + */ + procedure set_client_info(a_text in varchar2); + + function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2; + + function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2; + + procedure cleanup_temp_tables; + + /** + * Converts version string into version record + * + * @param a_version_no string representation of version in format vX.X.X.X where X is a positive integer + * @return t_version record with up to four positive numbers containing version + * @throws 20214 if passed version string is not matching version pattern + */ + function to_version(a_version_no varchar2) return t_version; + + + /** + * Saves data from dbms_output buffer into a global temporary table (cache) + * used to store dbms_output buffer captured before the run + * + */ + procedure save_dbms_output_to_cache; + + /** + * Reads data from global temporary table (cache) abd puts it back into dbms_output + * used to recover dbms_output buffer data after a run is complete + * + */ + procedure read_cache_to_dbms_output; + + + /** + * Function is used to reference to utPLSQL owned objects in dynamic sql statements executed from packages with invoker rights + * + * @return the name of the utPSQL schema owner + */ + function ut_owner return varchar2; + + + /** + * Used in dynamic sql select statements to maintain balance between + * number of hard-parses and optimiser accurancy for cardinality of collections + * + * + * @return 3, for inputs of: 1-9; 33 for input of 10 - 99; 333 for (100 - 999) + */ + function scale_cardinality(a_cardinality natural) return natural; + + function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2; + + /** + * Returns number as string. The value is represented as decimal according to XML standard: + * https://www.w3.org/TR/xmlschema-2/#decimal + */ + function to_xml_number_format(a_value number) return varchar2; + + + /** + * Returns xml header. If a_encoding is not null, header will include encoding attribute with provided value + */ + function get_xml_header(a_encoding varchar2) return varchar2; + + + /** + * Takes a collection of type ut_varchar2_list and it trims the characters passed as arguments for every element + */ + function trim_list_elements(a_list IN ut_varchar2_list, a_regexp_to_trim in varchar2 default '[:space:]') return ut_varchar2_list; + + /** + * Takes a collection of type ut_varchar2_list and it only returns the elements which meets the regular expression + */ + function filter_list(a_list IN ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list; + + -- Generates XMLGEN escaped string + function xmlgen_escaped_string(a_string in varchar2) return varchar2; + + /** + * Replaces multi-line comments in given source-code with empty lines + */ + function replace_multiline_comments(a_source clob) return clob; + + /** + * Returns list of sub-type reporters for given list of super-type reporters + */ + function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info; + + /** + * Remove given ORA error from stack + */ + function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2; + + /** + * Check if xml name is valid if not build a valid name + */ + function get_valid_xml_name(a_name varchar2) return varchar2; + + /** + * Add prefix word to elements of list + */ + function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list; + + function add_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2; + + function strip_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2; + +end ut_utils; +/ diff --git a/source/expectations/data_values/ut_cursor_column.tpb b/source/expectations/data_values/ut_cursor_column.tpb index 3c6931a5d..cb395e54a 100644 --- a/source/expectations/data_values/ut_cursor_column.tpb +++ b/source/expectations/data_values/ut_cursor_column.tpb @@ -1,69 +1,70 @@ -create or replace type body ut_cursor_column as - - member procedure init( - self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer - ) is - begin - self.parent_name := a_parent_name; --Name of the parent if its nested - self.hierarchy_level := a_hierarchy_level; --Hierarchy level - self.column_position := a_col_position; --Position of the column in cursor/ type - self.column_len := a_col_max_len; --length of column - self.column_precision := a_col_precision; - self.column_scale := a_col_scale; - self.column_name := TRIM( BOTH '''' FROM a_col_name); --name of the column - self.column_type_name := coalesce(a_col_type_name,a_col_type); --type name e.g. test_dummy_object or varchar2 - self.xml_valid_name := ut_utils.get_valid_xml_name(self.column_name); - self.display_path := case when a_access_path is null then - self.column_name - else - a_access_path||'/'||self.column_name - end; --Access path used for incldue exclude eg/ TEST_DUMMY_OBJECT/VARCHAR2 - self.access_path := case when a_access_path is null then - self.xml_valid_name - else - a_access_path||'/'||self.xml_valid_name - end; --Access path used for incldue exclude eg/ TEST_DUMMY_OBJECT/VARCHAR2 - self.transformed_name := case when length(self.xml_valid_name) > 30 then - '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' - when self.parent_name is null then - '"'||self.xml_valid_name||'"' - else - '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' - end; --when is nestd we need to hash name to make sure we dont exceed 30 char - self.column_type := a_col_type; --column type e.g. user_defined , varchar2 - self.column_schema := a_col_schema_name; -- schema name - self.is_sql_diffable := case - when lower(self.column_type) = 'user_defined_type' then - 0 - -- Due to bug in 11g/12.1 collection fails on varchar 4000+ - when (lower(self.column_type) in ('varchar2','char')) and (self.column_len > 4000) then - 0 - else - ut_utils.boolean_to_int(ut_compound_data_helper.is_sql_compare_allowed(self.column_type)) - end; --can we directly compare or do we need to hash value - self.is_collection := a_collection; - self.has_nested_col := case when lower(self.column_type) = 'user_defined_type' and self.is_collection = 0 then 1 else 0 end; - end; - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer - ) return self as result is - begin - init(a_col_name, a_col_schema_name, a_col_type_name, a_col_max_len, a_parent_name,a_hierarchy_level, a_col_position, - a_col_type, a_collection,a_access_path,a_col_precision,a_col_scale); - return; - end; - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result is - begin - return; - end; -end; -/ +create or replace type body ut_cursor_column as + + member procedure init( + self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer + ) is + begin + self.parent_name := a_parent_name; --Name of the parent if its nested + self.hierarchy_level := a_hierarchy_level; --Hierarchy level + self.column_position := a_col_position; --Position of the column in cursor/ type + self.column_len := a_col_max_len; --length of column + self.column_precision := a_col_precision; + self.column_scale := a_col_scale; + self.column_name := TRIM( BOTH '''' FROM a_col_name); --name of the column + self.column_type_name := coalesce(a_col_type_name,a_col_type); --type name e.g. test_dummy_object or varchar2 + self.xml_valid_name := ut_utils.get_valid_xml_name(self.column_name); + self.display_path := case when a_access_path is null then + self.column_name + else + a_access_path||'/'||self.column_name + end; --Access path used for incldue exclude eg/ TEST_DUMMY_OBJECT/VARCHAR2 + self.access_path := case when a_access_path is null then + self.xml_valid_name + else + a_access_path||'/'||self.xml_valid_name + end; --Access path used for XMLTABLE query + self.filter_path := self.access_path; --Filter path will differ from access path in anydata type + self.transformed_name := case when length(self.xml_valid_name) > 30 then + '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' + when self.parent_name is null then + '"'||self.xml_valid_name||'"' + else + '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' + end; --when is nestd we need to hash name to make sure we dont exceed 30 char + self.column_type := a_col_type; --column type e.g. user_defined , varchar2 + self.column_schema := a_col_schema_name; -- schema name + self.is_sql_diffable := case + when lower(self.column_type) = 'user_defined_type' then + 0 + -- Due to bug in 11g/12.1 collection fails on varchar 4000+ + when (lower(self.column_type) in ('varchar2','char')) and (self.column_len > 4000) then + 0 + else + ut_utils.boolean_to_int(ut_compound_data_helper.is_sql_compare_allowed(self.column_type)) + end; --can we directly compare or do we need to hash value + self.is_collection := a_collection; + self.has_nested_col := case when lower(self.column_type) = 'user_defined_type' and self.is_collection = 0 then 1 else 0 end; + end; + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer + ) return self as result is + begin + init(a_col_name, a_col_schema_name, a_col_type_name, a_col_max_len, a_parent_name,a_hierarchy_level, a_col_position, + a_col_type, a_collection,a_access_path,a_col_precision,a_col_scale); + return; + end; + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result is + begin + return; + end; +end; +/ diff --git a/source/expectations/data_values/ut_cursor_column.tps b/source/expectations/data_values/ut_cursor_column.tps index db9cbd3ae..da3c004f2 100644 --- a/source/expectations/data_values/ut_cursor_column.tps +++ b/source/expectations/data_values/ut_cursor_column.tps @@ -1,51 +1,52 @@ -create or replace type ut_cursor_column force authid current_user as object ( - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - parent_name varchar2(4000), - access_path varchar2(4000), - display_path varchar2(4000), - has_nested_col number(1,0), - transformed_name varchar2(2000), - hierarchy_level number, - column_position number, - xml_valid_name varchar2(2000), - column_name varchar2(2000), - column_type varchar2(128), - column_type_name varchar2(128), - column_schema varchar2(128), - column_len integer, - column_precision integer, - column_scale integer, - is_sql_diffable number(1, 0), - is_collection number(1, 0), - - member procedure init(self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer), - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer, a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer) - return self as result, - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result -) -/ +create or replace type ut_cursor_column authid current_user as object ( + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + parent_name varchar2(4000), + access_path varchar2(4000), + filter_path varchar2(4000), + display_path varchar2(4000), + has_nested_col number(1,0), + transformed_name varchar2(2000), + hierarchy_level number, + column_position number, + xml_valid_name varchar2(2000), + column_name varchar2(2000), + column_type varchar2(128), + column_type_name varchar2(128), + column_schema varchar2(128), + column_len integer, + column_precision integer, + column_scale integer, + is_sql_diffable number(1, 0), + is_collection number(1, 0), + + member procedure init(self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer), + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer, a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer) + return self as result, + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result +) +/ diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb index 99e1d4edc..2a70a51dd 100644 --- a/source/expectations/data_values/ut_cursor_details.tpb +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -1,286 +1,257 @@ -create or replace type body ut_cursor_details as - - member function equals( a_other ut_cursor_details, a_match_options ut_matcher_options ) return boolean is - l_diffs integer; - begin - select count(1) into l_diffs - from table(self.cursor_columns_info) a - full outer join table(a_other.cursor_columns_info) e - on decode(a.parent_name,e.parent_name,1,0)= 1 - and a.column_name = e.column_name - and replace(a.column_type,'VARCHAR2','CHAR') = replace(e.column_type,'VARCHAR2','CHAR') - and ( a.column_position = e.column_position or a_match_options.columns_are_unordered_flag = 1 ) - where a.column_name is null or e.column_name is null; - return l_diffs = 0; - end; - - member procedure desc_compound_data( - self in out nocopy ut_cursor_details, a_compound_data anytype, - a_parent_name in varchar2, a_level in integer, a_access_path in varchar2 - ) is - l_idx pls_integer := 1; - l_elements_info ut_metadata.t_anytype_members_rec; - l_element_info ut_metadata.t_anytype_elem_info_rec; - l_is_collection boolean; - begin - l_elements_info := ut_metadata.get_anytype_members_info( a_compound_data ); - l_is_collection := ut_metadata.is_collection(l_elements_info.type_code); - if l_elements_info.elements_count is null then - l_element_info := ut_metadata.get_attr_elem_info( a_compound_data ); - self.cursor_columns_info.extend; - self.cursor_columns_info(cursor_columns_info.last) := - ut_cursor_column( - l_elements_info.type_name, - l_elements_info.schema_name, - null, - l_elements_info.length, - a_parent_name, - a_level, - l_idx, - ut_compound_data_helper.get_column_type_desc(l_elements_info.type_code,false), - ut_utils.boolean_to_int(l_is_collection), - a_access_path, - l_elements_info.precision, - l_elements_info.scale - ); - if l_element_info.attr_elt_type is not null then - desc_compound_data( - l_element_info.attr_elt_type, l_elements_info.type_name, - a_level + 1, a_access_path || '/' || l_elements_info.type_name - ); - end if; - else - while l_idx <= l_elements_info.elements_count loop - l_element_info := ut_metadata.get_attr_elem_info( a_compound_data, l_idx ); - - self.cursor_columns_info.extend; - self.cursor_columns_info(cursor_columns_info.last) := - ut_cursor_column( - l_element_info.attribute_name, - l_elements_info.schema_name, - null, - l_element_info.length, - a_parent_name, - a_level, - l_idx, - ut_compound_data_helper.get_column_type_desc(l_element_info.type_code,false), - ut_utils.boolean_to_int(l_is_collection), - a_access_path, - l_elements_info.precision, - l_elements_info.scale - ); - if l_element_info.attr_elt_type is not null then - desc_compound_data( - l_element_info.attr_elt_type, l_element_info.attribute_name, - a_level + 1, a_access_path || '/' || l_element_info.attribute_name - ); - end if; - l_idx := l_idx + 1; - end loop; - end if; - end; - - constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result is - begin - self.cursor_columns_info := ut_cursor_column_tab(); - return; - end; - - constructor function ut_cursor_details( - self in out nocopy ut_cursor_details, - a_cursor_number in number - ) return self as result is - l_columns_count pls_integer; - l_columns_desc dbms_sql.desc_tab3; - l_is_collection boolean; - l_hierarchy_level integer := 1; - begin - self.cursor_columns_info := ut_cursor_column_tab(); - dbms_sql.describe_columns3(a_cursor_number, l_columns_count, l_columns_desc); - - /** - * Due to a bug with object being part of cursor in ANYDATA scenario - * oracle fails to revert number to cursor. We ar using dbms_sql.close cursor to close it - * to avoid leaving open cursors behind. - * a_cursor := dbms_sql.to_refcursor(l_cursor_number); - **/ - for pos in 1 .. l_columns_count loop - l_is_collection := ut_metadata.is_collection( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ); - self.cursor_columns_info.extend; - self.cursor_columns_info(self.cursor_columns_info.last) := - ut_cursor_column( - l_columns_desc(pos).col_name, - l_columns_desc(pos).col_schema_name, - l_columns_desc(pos).col_type_name, - l_columns_desc(pos).col_max_len, - null, - l_hierarchy_level, - pos, - ut_compound_data_helper.get_column_type_desc(l_columns_desc(pos).col_type,true), - ut_utils.boolean_to_int(l_is_collection), - null, - l_columns_desc(pos).col_precision, - l_columns_desc(pos).col_scale - ); - - if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then - desc_compound_data( - ut_metadata.get_user_defined_type( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ), - l_columns_desc(pos).col_name, - l_hierarchy_level + 1, - l_columns_desc(pos).col_name - ); - end if; - end loop; - return; - end; - - member function contains_collection return boolean is - l_collection_elements number; - begin - select count(1) into l_collection_elements - from table(cursor_columns_info) c - where c.is_collection = 1 and rownum = 1; - return l_collection_elements > 0; - end; - - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is - l_result ut_varchar2_list; - l_root varchar2(125); - begin - if self.is_anydata = 1 then - l_root := get_root; - end if; - --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') - select fl.column_value - bulk collect into l_result - from table(a_expected_columns) fl - where not exists ( - select 1 from table(self.cursor_columns_info) c - where regexp_like(c.access_path,'^/?'|| - case - when self.is_anydata = 1 then - l_root||'/'||trim (leading '/' from fl.column_value) - else - fl.column_value - end||'($|/.*)' - ) - ) - order by fl.column_value; - return l_result; - end; - - member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options) is - l_result ut_cursor_details := self; - l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); - l_column ut_cursor_column; - l_root varchar2(125); - c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; - begin - if l_result.cursor_columns_info is not null then - - if self.is_anydata = 1 then - l_root := get_root; - end if; - - --limit columns to those on the include items minus exclude items - if a_match_options.include.items.count > 0 then - -- if include - exclude = 0 then keep all columns - if a_match_options.include.items != a_match_options.exclude.items then - with included_columns as ( - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.include.items) - minus - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.exclude.items) - ) - select value(x) - bulk collect into l_result.cursor_columns_info - from table(self.cursor_columns_info) x - where exists( - select 1 from included_columns f where regexp_like(x.access_path,'^/?'|| - case - when self.is_anydata = 1 then - l_root||'/'||trim(leading '/' from f.col_names) - else - f.col_names - end||'($|/.*)' - ) - ) - or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; - end if; - elsif a_match_options.exclude.items.count > 0 then - with excluded_columns as ( - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.exclude.items) - ) - select value(x) - bulk collect into l_result.cursor_columns_info - from table(self.cursor_columns_info) x - where not exists( - select 1 from excluded_columns f where regexp_like(x.access_path,'^/?'|| - case - when self.is_anydata = 1 then - l_root||'/'||trim(leading '/' from f.col_names) - else - f.col_names - end||'($|/.*)' ) - ); - end if; - - --Rewrite column order after columns been excluded - for i in ( - select parent_name, access_path, display_path, has_nested_col, - transformed_name, hierarchy_level, - rownum as new_position, xml_valid_name, - column_name, column_type, column_type_name, column_schema, - column_len, column_precision ,column_scale ,is_sql_diffable, is_collection,value(x) col_info - from table(l_result.cursor_columns_info) x - order by x.column_position asc - ) loop - l_column := i.col_info; - l_column.column_position := i.new_position; - l_column_tab.extend; - l_column_tab(l_column_tab.last) := l_column; - end loop; - - l_result.cursor_columns_info := l_column_tab; - self := l_result; - end if; - end; - - member function get_xml_children(a_parent_name varchar2 := null) return xmltype is - l_result xmltype; - begin - select xmlagg(xmlelement(evalname t.column_name,t.column_type_name)) - into l_result - from table(self.cursor_columns_info) t - where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) - having count(*) > 0; - return l_result; - end; - - member procedure has_anydata(self in out nocopy ut_cursor_details, a_is_anydata in boolean :=false) is - begin - self.is_anydata := case when nvl(a_is_anydata,false) then 1 else 0 end; - end; - - member function has_anydata return boolean is - begin - return ut_utils.int_to_boolean(nvl(self.is_anydata,0)); - end; - - member function get_root return varchar2 is - l_root varchar2(250); - begin - if self.cursor_columns_info.count > 0 then - select x.access_path into l_root from table(self.cursor_columns_info) x - where x.hierarchy_level = 1; - else - l_root := null; - end if; - return l_root; - end; - -end; -/ +create or replace type body ut_cursor_details as + + member function equals( a_other ut_cursor_details, a_match_options ut_matcher_options ) return boolean is + l_diffs integer; + begin + select count(1) into l_diffs + from table(self.cursor_columns_info) a + full outer join table(a_other.cursor_columns_info) e + on decode(a.parent_name,e.parent_name,1,0)= 1 + and a.column_name = e.column_name + and replace(a.column_type,'VARCHAR2','CHAR') = replace(e.column_type,'VARCHAR2','CHAR') + and ( a.column_position = e.column_position or a_match_options.columns_are_unordered_flag = 1 ) + where a.column_name is null or e.column_name is null; + return l_diffs = 0; + end; + + member procedure desc_compound_data( + self in out nocopy ut_cursor_details, a_compound_data anytype, + a_parent_name in varchar2, a_level in integer, a_access_path in varchar2 + ) is + l_idx pls_integer := 1; + l_elements_info ut_metadata.t_anytype_members_rec; + l_element_info ut_metadata.t_anytype_elem_info_rec; + l_is_collection boolean; + begin + l_elements_info := ut_metadata.get_anytype_members_info( a_compound_data ); + l_is_collection := ut_metadata.is_collection(l_elements_info.type_code); + if l_elements_info.elements_count is null then + l_element_info := ut_metadata.get_attr_elem_info( a_compound_data ); + self.cursor_columns_info.extend; + self.cursor_columns_info(cursor_columns_info.last) := + ut_cursor_column( + l_elements_info.type_name, + l_elements_info.schema_name, + null, + l_elements_info.length, + a_parent_name, + a_level, + l_idx, + ut_compound_data_helper.get_column_type_desc(l_elements_info.type_code,false), + ut_utils.boolean_to_int(l_is_collection), + a_access_path, + l_elements_info.precision, + l_elements_info.scale + ); + if l_element_info.attr_elt_type is not null then + desc_compound_data( + l_element_info.attr_elt_type, l_elements_info.type_name, + a_level + 1, a_access_path || '/' || l_elements_info.type_name + ); + end if; + else + while l_idx <= l_elements_info.elements_count loop + l_element_info := ut_metadata.get_attr_elem_info( a_compound_data, l_idx ); + + self.cursor_columns_info.extend; + self.cursor_columns_info(cursor_columns_info.last) := + ut_cursor_column( + l_element_info.attribute_name, + l_elements_info.schema_name, + null, + l_element_info.length, + a_parent_name, + a_level, + l_idx, + ut_compound_data_helper.get_column_type_desc(l_element_info.type_code,false), + ut_utils.boolean_to_int(l_is_collection), + a_access_path, + l_elements_info.precision, + l_elements_info.scale + ); + if l_element_info.attr_elt_type is not null then + desc_compound_data( + l_element_info.attr_elt_type, l_element_info.attribute_name, + a_level + 1, a_access_path || '/' || l_element_info.attribute_name + ); + end if; + l_idx := l_idx + 1; + end loop; + end if; + end; + + constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result is + begin + self.cursor_columns_info := ut_cursor_column_tab(); + return; + end; + + constructor function ut_cursor_details( + self in out nocopy ut_cursor_details, + a_cursor_number in number + ) return self as result is + l_columns_count pls_integer; + l_columns_desc dbms_sql.desc_tab3; + l_is_collection boolean; + l_hierarchy_level integer := 1; + begin + self.cursor_columns_info := ut_cursor_column_tab(); + self.is_anydata := 0; + dbms_sql.describe_columns3(a_cursor_number, l_columns_count, l_columns_desc); + + /** + * Due to a bug with object being part of cursor in ANYDATA scenario + * oracle fails to revert number to cursor. We ar using dbms_sql.close cursor to close it + * to avoid leaving open cursors behind. + * a_cursor := dbms_sql.to_refcursor(l_cursor_number); + **/ + for pos in 1 .. l_columns_count loop + l_is_collection := ut_metadata.is_collection( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ); + self.cursor_columns_info.extend; + self.cursor_columns_info(self.cursor_columns_info.last) := + ut_cursor_column( + l_columns_desc(pos).col_name, + l_columns_desc(pos).col_schema_name, + l_columns_desc(pos).col_type_name, + l_columns_desc(pos).col_max_len, + null, + l_hierarchy_level, + pos, + ut_compound_data_helper.get_column_type_desc(l_columns_desc(pos).col_type,true), + ut_utils.boolean_to_int(l_is_collection), + null, + l_columns_desc(pos).col_precision, + l_columns_desc(pos).col_scale + ); + + if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then + desc_compound_data( + ut_metadata.get_user_defined_type( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ), + l_columns_desc(pos).col_name, + l_hierarchy_level + 1, + l_columns_desc(pos).col_name + ); + end if; + end loop; + return; + end; + + member function contains_collection return boolean is + l_collection_elements number; + begin + select count(1) into l_collection_elements + from table(cursor_columns_info) c + where c.is_collection = 1 and rownum = 1; + return l_collection_elements > 0; + end; + + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is + l_result ut_varchar2_list; + begin + --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') + select fl.column_value + bulk collect into l_result + from table(a_expected_columns) fl + where not exists ( + select 1 from table(self.cursor_columns_info) c + where regexp_like(c.filter_path,'^/?'||fl.column_value||'($|/.*)' ) + ) + order by fl.column_value; + return l_result; + end; + + member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options) is + l_result ut_cursor_details := self; + l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); + l_column ut_cursor_column; + c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; + begin + if l_result.cursor_columns_info is not null then + + --limit columns to those on the include items minus exclude items + if a_match_options.include.items.count > 0 then + -- if include - exclude = 0 then keep all columns + if a_match_options.include.items != a_match_options.exclude.items then + with included_columns as ( + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.include.items) + minus + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.exclude.items) + ) + select value(x) + bulk collect into l_result.cursor_columns_info + from table(self.cursor_columns_info) x + where exists( + select 1 from included_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) + ) + or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; + end if; + elsif a_match_options.exclude.items.count > 0 then + with excluded_columns as ( + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.exclude.items) + ) + select value(x) + bulk collect into l_result.cursor_columns_info + from table(self.cursor_columns_info) x + where not exists( + select 1 from excluded_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) + ); + end if; + + --Rewrite column order after columns been excluded + for i in ( + select parent_name, access_path, display_path, has_nested_col, + transformed_name, hierarchy_level, + rownum as new_position, xml_valid_name, + column_name, column_type, column_type_name, column_schema, + column_len, column_precision ,column_scale ,is_sql_diffable, is_collection,value(x) col_info + from table(l_result.cursor_columns_info) x + order by x.column_position asc + ) loop + l_column := i.col_info; + l_column.column_position := i.new_position; + l_column_tab.extend; + l_column_tab(l_column_tab.last) := l_column; + end loop; + + l_result.cursor_columns_info := l_column_tab; + self := l_result; + end if; + end; + + member function get_xml_children(a_parent_name varchar2 := null) return xmltype is + l_result xmltype; + begin + select xmlagg(xmlelement(evalname t.column_name,t.column_type_name)) + into l_result + from table(self.cursor_columns_info) t + where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) + having count(*) > 0; + return l_result; + end; + + member function get_root return varchar2 is + l_root varchar2(250); + begin + if self.cursor_columns_info.count > 0 then + select x.access_path into l_root from table(self.cursor_columns_info) x + where x.hierarchy_level = 1; + else + l_root := null; + end if; + return l_root; + end; + + member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) is + l_root varchar2(250) := get_root(); + begin + self.is_anydata := 1; + for i in 1..cursor_columns_info.count loop + self.cursor_columns_info(i).filter_path := ut_utils.strip_prefix(self.cursor_columns_info(i).access_path,l_root); + end loop; + end; + +end; +/ diff --git a/source/expectations/data_values/ut_cursor_details.tps b/source/expectations/data_values/ut_cursor_details.tps index ce5aefbe7..e6c80a3b5 100644 --- a/source/expectations/data_values/ut_cursor_details.tps +++ b/source/expectations/data_values/ut_cursor_details.tps @@ -1,42 +1,41 @@ -create or replace type ut_cursor_details force authid current_user as object ( - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - cursor_columns_info ut_cursor_column_tab, - - /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ - is_anydata number(1,0), - constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, - constructor function ut_cursor_details( - self in out nocopy ut_cursor_details,a_cursor_number in number - ) return self as result, - member function equals(a_other ut_cursor_details, a_match_options ut_matcher_options) return boolean, - member procedure desc_compound_data( - self in out nocopy ut_cursor_details, - a_compound_data anytype, - a_parent_name in varchar2, - a_level in integer, - a_access_path in varchar2 - ), - member function contains_collection return boolean, - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, - member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), - member function get_xml_children(a_parent_name varchar2 := null) return xmltype, - member procedure has_anydata(self in out nocopy ut_cursor_details, a_is_anydata in boolean := false), - member function has_anydata return boolean, - member function get_root return varchar2 -) -/ +create or replace type ut_cursor_details authid current_user as object ( + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + cursor_columns_info ut_cursor_column_tab, + + /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ + is_anydata number(1,0), + constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, + constructor function ut_cursor_details( + self in out nocopy ut_cursor_details,a_cursor_number in number + ) return self as result, + member function equals(a_other ut_cursor_details, a_match_options ut_matcher_options) return boolean, + member procedure desc_compound_data( + self in out nocopy ut_cursor_details, + a_compound_data anytype, + a_parent_name in varchar2, + a_level in integer, + a_access_path in varchar2 + ), + member function contains_collection return boolean, + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, + member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), + member function get_xml_children(a_parent_name varchar2 := null) return xmltype, + member function get_root return varchar2, + member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) +) +/ diff --git a/source/expectations/data_values/ut_data_value_anydata.tpb b/source/expectations/data_values/ut_data_value_anydata.tpb index fa59ad67c..808e52197 100644 --- a/source/expectations/data_values/ut_data_value_anydata.tpb +++ b/source/expectations/data_values/ut_data_value_anydata.tpb @@ -1,145 +1,143 @@ -create or replace type body ut_data_value_anydata as - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - overriding member function get_object_info return varchar2 is - begin - return self.data_type || case when self.compound_type = 'collection' then ' [ count = '||self.elements_count||' ]' else null end; - end; - - member function get_extract_path(a_data_value anydata) return varchar2 is - l_path varchar2(10); - begin - if self.compound_type = 'object' then - l_path := '/*/*'; - else - case when ut_metadata.has_collection_members(a_data_value) then - l_path := '/*/*'; - else - l_path := '/*'; - end case; - end if; - return l_path; - end; - - member function get_cursor_sql_from_anydata(a_data_value anydata) return varchar2 is - l_cursor_sql varchar2(32767); - begin - l_cursor_sql := ' - declare - l_data '||self.data_type||'; - l_value anydata := :a_value; - l_status integer; - l_tmp_refcursor sys_refcursor; - begin - l_status := l_value.get'||self.compound_type||'(l_data); '|| - case when self.compound_type = 'collection' then - q'[ open :l_tmp_refcursor for select value(x) as "]'|| - ut_metadata.get_object_name(ut_metadata.get_collection_element(a_data_value))|| - q'[" from table(l_data) x;]' - else - q'[ open :l_tmp_refcursor for select l_data as "]'||ut_metadata.get_object_name(self.data_type)|| - q'[" from dual;]' - end || - 'end;'; - return l_cursor_sql; - end; - - member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata) is - l_refcursor sys_refcursor; - l_ctx number; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - cursor_not_open exception; - l_cursor_number number; - l_anydata_sql varchar2(32767); - begin - self.data_type := ut_metadata.get_anydata_typename(a_value); - self.compound_type := get_instance(a_value); - self.is_data_null := ut_metadata.is_anytype_null(a_value,self.compound_type); - self.data_id := sys_guid(); - self.self_type := $$plsql_unit; - self.cursor_details := ut_cursor_details(); - - ut_compound_data_helper.cleanup_diff; - - if not self.is_null() then - self.extract_path := get_extract_path(a_value); - l_anydata_sql := get_cursor_sql_from_anydata(a_value); - execute immediate l_anydata_sql using in a_value, in out l_refcursor; - if l_refcursor%isopen then - self.extract_cursor(l_refcursor); - l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); - self.cursor_details := ut_cursor_details(l_cursor_number); - self.cursor_details.has_anydata(true); - dbms_sql.close_cursor(l_cursor_number); - elsif not l_refcursor%isopen then - raise cursor_not_open; - end if; - end if; - exception - when cursor_not_open then - raise_application_error(-20155, 'Cursor is not open'); - when others then - if l_refcursor%isopen then - close l_refcursor; - end if; - raise; - end; - - member function get_instance(a_data_value anydata) return varchar2 is - l_result varchar2(30); - begin - l_result := ut_metadata.get_anydata_compound_type(a_data_value); - if l_result not in ('object','collection') then - raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL'); - end if; - return l_result; - end; - - constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata, a_value anydata) return self as result - is - begin - init(a_value); - return; - end; - - overriding member function compare_implementation( - a_other ut_data_value, - a_match_options ut_matcher_options, - a_inclusion_compare boolean := false, - a_is_negated boolean := false - ) return integer is - l_result integer := 0; - begin - if not a_other is of (ut_data_value_anydata) then - raise value_error; - end if; - l_result := l_result + (self as ut_data_value_refcursor).compare_implementation(a_other,a_match_options,a_inclusion_compare,a_is_negated); - return l_result; - end; - - overriding member function is_empty return boolean is - begin - if self.compound_type = 'collection' then - return self.elements_count = 0; - else - raise value_error; - end if; - end; - -end; -/ +create or replace type body ut_data_value_anydata as + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + overriding member function get_object_info return varchar2 is + begin + return self.data_type || case when self.compound_type = 'collection' then ' [ count = '||self.elements_count||' ]' else null end; + end; + + member function get_extract_path(a_data_value anydata) return varchar2 is + l_path varchar2(10); + begin + if self.compound_type = 'object' then + l_path := '/*/*'; + else + case when ut_metadata.has_collection_members(a_data_value) then + l_path := '/*/*'; + else + l_path := '/*'; + end case; + end if; + return l_path; + end; + + member function get_cursor_sql_from_anydata(a_data_value anydata) return varchar2 is + l_cursor_sql varchar2(32767); + begin + l_cursor_sql := ' + declare + l_data '||self.data_type||'; + l_value anydata := :a_value; + l_status integer; + l_tmp_refcursor sys_refcursor; + begin + l_status := l_value.get'||self.compound_type||'(l_data); '|| + case when self.compound_type = 'collection' then + q'[ open :l_tmp_refcursor for select value(x) as "]'|| + ut_metadata.get_object_name(ut_metadata.get_collection_element(a_data_value))|| + q'[" from table(l_data) x;]' + else + q'[ open :l_tmp_refcursor for select l_data as "]'||ut_metadata.get_object_name(self.data_type)|| + q'[" from dual;]' + end || + 'end;'; + return l_cursor_sql; + end; + + member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata) is + l_refcursor sys_refcursor; + cursor_not_open exception; + l_cursor_number number; + l_anydata_sql varchar2(32767); + begin + self.data_type := ut_metadata.get_anydata_typename(a_value); + self.compound_type := get_instance(a_value); + self.is_data_null := ut_metadata.is_anytype_null(a_value,self.compound_type); + self.data_id := sys_guid(); + self.self_type := $$plsql_unit; + self.cursor_details := ut_cursor_details(); + + ut_compound_data_helper.cleanup_diff; + + if not self.is_null() then + self.extract_path := get_extract_path(a_value); + l_anydata_sql := get_cursor_sql_from_anydata(a_value); + execute immediate l_anydata_sql using in a_value, in out l_refcursor; + if l_refcursor%isopen then + self.extract_cursor(l_refcursor); + l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); + self.cursor_details := ut_cursor_details(l_cursor_number); + self.cursor_details.strip_root_from_anydata; + dbms_sql.close_cursor(l_cursor_number); + elsif not l_refcursor%isopen then + raise cursor_not_open; + end if; + end if; + exception + when cursor_not_open then + raise_application_error(-20155, 'Cursor is not open'); + when others then + if l_refcursor%isopen then + close l_refcursor; + end if; + raise; + end; + + member function get_instance(a_data_value anydata) return varchar2 is + l_result varchar2(30); + begin + l_result := ut_metadata.get_anydata_compound_type(a_data_value); + if l_result not in ('object','collection') then + raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL'); + end if; + return l_result; + end; + + constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata, a_value anydata) return self as result + is + begin + init(a_value); + return; + end; + + overriding member function compare_implementation( + a_other ut_data_value, + a_match_options ut_matcher_options, + a_inclusion_compare boolean := false, + a_is_negated boolean := false + ) return integer is + l_result integer := 0; + begin + if not a_other is of (ut_data_value_anydata) then + raise value_error; + end if; + l_result := l_result + (self as ut_data_value_refcursor).compare_implementation(a_other,a_match_options,a_inclusion_compare,a_is_negated); + return l_result; + end; + + overriding member function is_empty return boolean is + begin + if self.compound_type = 'collection' then + return self.elements_count = 0; + else + raise value_error; + end if; + end; + +end; +/ diff --git a/source/expectations/data_values/ut_data_value_refcursor.tpb b/source/expectations/data_values/ut_data_value_refcursor.tpb index 2aac626e3..b93158cf1 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tpb +++ b/source/expectations/data_values/ut_data_value_refcursor.tpb @@ -1,399 +1,398 @@ -create or replace type body ut_data_value_refcursor as - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) - return self as result is - begin - init(a_value); - return; - end; - - member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) - is - c_bulk_rows constant integer := 10000; - l_cursor sys_refcursor := a_value; - l_ctx number; - l_xml xmltype; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_set_id integer := 0; - l_elements_count number := 0; - begin - -- We use DBMS_XMLGEN in order to: - -- 1) be able to process data in bulks (set of rows) - -- 2) be able to influence the ROWSET/ROW tags - -- 3) be able to influence the way NULL values are handled (empty TAG) - -- 4) be able to influence the way TIMESTAMP is formatted. - -- Due to Oracle feature/bug, it is not possible to change the DATE formatting of cursor data - -- AFTER the cursor was opened. - -- The only solution for this is to change NLS settings before opening the cursor. - -- - -- This would work fine if we could use DBMS_XMLGEN.restartQuery. - -- The restartQuery fails however if PLSQL variables of TIMESTAMP/INTERVAL or CLOB/BLOB are used. - ut_expectation_processor.set_xml_nls_params(); - l_ctx := dbms_xmlgen.newContext(l_cursor); - dbms_xmlgen.setNullHandling(l_ctx, dbms_xmlgen.empty_tag); - dbms_xmlgen.setMaxRows(l_ctx, c_bulk_rows); - loop - l_xml := dbms_xmlgen.getxmltype(l_ctx); - exit when dbms_xmlgen.getNumRowsProcessed(l_ctx) = 0; - --Bug in oracle 12.2+ where XML binary storage trimming insignificant whitespaces. - $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then - l_xml := xmltype( replace(l_xml.getClobVal(),' 0 then - ut_utils.append_to_clob( l_result, self.cursor_details.get_xml_children().getclobval() ); - end if; - ut_utils.append_to_clob(l_result,chr(10)||(self as ut_compound_data_value).to_string()); - l_result_string := ut_utils.to_string(l_result,null); - dbms_lob.freetemporary(l_result); - end if; - return l_result_string; - end; - - overriding member function diff( a_other ut_data_value, a_match_options ut_matcher_options ) return varchar2 is - l_result clob; - l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); - l_result_string varchar2(32767); - l_other ut_data_value_refcursor; - l_self ut_data_value_refcursor := self; - l_column_diffs ut_compound_data_helper.tt_column_diffs; - - l_other_cols ut_cursor_column_tab; - l_self_cols ut_cursor_column_tab; - - l_act_missing_pk ut_varchar2_list := ut_varchar2_list(); - l_exp_missing_pk ut_varchar2_list := ut_varchar2_list(); - - c_max_rows integer := ut_utils.gc_diff_max_rows; - l_diff_id ut_compound_data_helper.t_hash; - l_diff_row_count integer; - l_row_diffs ut_compound_data_helper.tt_row_diffs; - l_message varchar2(32767); - - function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is - begin - return - case a_col.diff_type - when '-' then - ' Column <'||a_col.expected_name||'> [data-type: '||a_col.expected_type||'] is missing. Expected column position: '||a_col.expected_pos||'.' - when '+' then - ' Column <'||a_col.actual_name||'> [position: '||a_col.actual_pos||', data-type: '||a_col.actual_type||'] is not expected in results.' - when 't' then - ' Column <'||a_col.actual_name||'> data-type is invalid. Expected: '||a_col.expected_type||',' ||' actual: '||a_col.actual_type||'.' - when 'p' then - ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' - end; - end; - - function remove_incomparable_cols( - a_cursor_details ut_cursor_column_tab, a_column_diffs ut_compound_data_helper.tt_column_diffs - ) return ut_cursor_column_tab is - l_missing_cols ut_varchar2_list := ut_varchar2_list(); - l_result ut_cursor_column_tab; - begin - for i in 1 .. a_column_diffs.count loop - if a_column_diffs(i).diff_type in ('-','+') then - l_missing_cols.extend; - l_missing_cols(l_missing_cols.last) := coalesce(a_column_diffs(i).expected_name, a_column_diffs(i).actual_name); - end if; - end loop; - select value(i) bulk collect into l_result - from table(a_cursor_details) i - where i.access_path not in ( - select c.column_value - from table(l_missing_cols) c - ); - return l_result; - end; - - function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs, a_is_unordered boolean) return varchar2 is - begin - if a_is_unordered then - if 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; - else - return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; - else - return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; - 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); - l_other.cursor_details.filter_columns(a_match_options); - l_self.cursor_details.filter_columns(a_match_options); - - l_other_cols := l_other.cursor_details.cursor_columns_info; - l_self_cols := l_self.cursor_details.cursor_columns_info; - - dbms_lob.createtemporary(l_result,true); - --diff columns - if not l_self.is_null and not l_other.is_null then - l_column_diffs := ut_compound_data_helper.get_columns_diff( - l_self.cursor_details.cursor_columns_info, - l_other.cursor_details.cursor_columns_info, - a_match_options.ordered_columns() - ); - - if l_column_diffs.count > 0 then - ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); - l_other_cols := remove_incomparable_cols( l_other_cols, l_column_diffs ); - l_self_cols := remove_incomparable_cols( l_self_cols, l_column_diffs ); - for i in 1 .. l_column_diffs.count loop - l_results.extend; - l_results(l_results.last) := get_col_diff_text(l_column_diffs(i)); - end loop; - ut_utils.append_to_clob(l_result, l_results); - end if; - end if; - - --check for missing pk - if a_match_options.join_by.items.count > 0 then - l_act_missing_pk := l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); - l_exp_missing_pk := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); - end if; - - --diff rows and row elements if the pk is not missing - if l_act_missing_pk.count + l_exp_missing_pk.count = 0 then - l_diff_id := ut_compound_data_helper.get_hash( l_self.data_id || l_other.data_id ); - - -- First tell how many rows are different - l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; - if l_diff_row_count > 0 then - l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( - l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, - l_diff_id, - case - when - l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) - else - a_match_options.join_by.items - end, - a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path - ); - 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 - ||' ]'||chr(10)|| case when l_row_diffs.count = 0 then ' All rows are different as the columns are not matching.' else null end; - ut_utils.append_to_clob( l_result, l_message ); - l_results := ut_utils.t_clob_tab(); - for i in 1 .. l_row_diffs.count loop - l_results.extend; - l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_match_options.unordered); - end loop; - ut_utils.append_to_clob(l_result,l_results); - else - l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.'; - ut_utils.append_to_clob( l_result, l_message ); - end if; - else - ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); - - for i in 1 .. l_exp_missing_pk.count loop - ut_utils.append_to_clob(l_result, ' Join key '||l_exp_missing_pk(i)||' does not exists in expected'||chr(10)); - end loop; - - for i in 1 .. l_act_missing_pk.count loop - ut_utils.append_to_clob(l_result, ' Join key '||l_act_missing_pk(i)||' does not exists in actual'||chr(10)); - end loop; - - if l_self.cursor_details.contains_collection() or l_other.cursor_details.contains_collection() then - ut_utils.append_to_clob(l_result,' Please make sure that your join clause is not refferring to collection element'|| chr(10)); - end if; - - 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) return integer is - begin - return compare_implementation( a_other, null ); - end; - - member function compare_implementation( - a_other ut_data_value, - a_match_options ut_matcher_options, - a_inclusion_compare boolean := false, - a_is_negated boolean := false - ) return integer is - l_result integer := 0; - l_self ut_data_value_refcursor := self; - l_other ut_data_value_refcursor; - l_diff_cursor_text clob; - - function compare_data( - a_self ut_data_value_refcursor, - a_other ut_data_value_refcursor, - a_diff_cursor_text clob - ) return integer is - l_diff_id ut_compound_data_helper.t_hash; - l_result integer; - --We will start with number od differences being displayed. - l_cursor sys_refcursor; - l_diff_tab ut_compound_data_helper.t_diff_tab; - l_diif_rowcount integer :=0; - begin - l_diff_id := ut_compound_data_helper.get_hash(a_self.data_id||a_other.data_id); - - begin - l_cursor := ut_compound_data_helper.get_compare_cursor(a_diff_cursor_text, - a_self.data_id, a_other.data_id); - --fetch and save rows for display of diff - fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_diff_max_rows; - exception when others then - if l_cursor%isopen then - close l_cursor; - end if; - raise; - end; - - ut_compound_data_helper.insert_diffs_result( l_diff_tab, l_diff_id ); - --fetch rows for count only - loop - exit when l_diff_tab.count = 0; - l_diif_rowcount := l_diif_rowcount + l_diff_tab.count; - fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_bc_fetch_limit; - end loop; - - ut_compound_data_helper.set_rows_diff(l_diif_rowcount); - - --result is OK only if both are same - if l_diif_rowcount = 0 and a_self.is_null = a_other.is_null then - l_result := 0; - else - l_result := 1; - end if; - close l_cursor; - return l_result; - 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); - l_other.cursor_details.filter_columns( a_match_options ); - l_self.cursor_details.filter_columns( a_match_options ); - - if a_match_options.join_by.items.count > 0 then - l_result := - l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count - + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; - end if; - - if l_result = 0 then - if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then - l_result := 1; - end if; - - l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( - l_other, - a_match_options.join_by.items, - a_match_options.unordered(), - a_inclusion_compare, - a_is_negated - ); - l_result := l_result + compare_data( l_self, l_other, l_diff_cursor_text ); - end if; - return l_result; - end; - - overriding member function is_empty return boolean is - begin - return self.elements_count = 0; - end; - -end; -/ +create or replace type body ut_data_value_refcursor as + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) + return self as result is + begin + init(a_value); + return; + end; + + member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) + is + c_bulk_rows constant integer := 10000; + l_cursor sys_refcursor := a_value; + l_ctx number; + l_xml xmltype; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_set_id integer := 0; + l_elements_count number := 0; + begin + -- We use DBMS_XMLGEN in order to: + -- 1) be able to process data in bulks (set of rows) + -- 2) be able to influence the ROWSET/ROW tags + -- 3) be able to influence the way NULL values are handled (empty TAG) + -- 4) be able to influence the way TIMESTAMP is formatted. + -- Due to Oracle feature/bug, it is not possible to change the DATE formatting of cursor data + -- AFTER the cursor was opened. + -- The only solution for this is to change NLS settings before opening the cursor. + -- + -- This would work fine if we could use DBMS_XMLGEN.restartQuery. + -- The restartQuery fails however if PLSQL variables of TIMESTAMP/INTERVAL or CLOB/BLOB are used. + ut_expectation_processor.set_xml_nls_params(); + l_ctx := dbms_xmlgen.newContext(l_cursor); + dbms_xmlgen.setNullHandling(l_ctx, dbms_xmlgen.empty_tag); + dbms_xmlgen.setMaxRows(l_ctx, c_bulk_rows); + loop + l_xml := dbms_xmlgen.getxmltype(l_ctx); + exit when dbms_xmlgen.getNumRowsProcessed(l_ctx) = 0; + --Bug in oracle 12.2+ where XML binary storage trimming insignificant whitespaces. + $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then + l_xml := xmltype( replace(l_xml.getClobVal(),' 0 then + ut_utils.append_to_clob( l_result, self.cursor_details.get_xml_children().getclobval() ); + end if; + ut_utils.append_to_clob(l_result,chr(10)||(self as ut_compound_data_value).to_string()); + l_result_string := ut_utils.to_string(l_result,null); + dbms_lob.freetemporary(l_result); + end if; + return l_result_string; + end; + + overriding member function diff( a_other ut_data_value, a_match_options ut_matcher_options ) return varchar2 is + l_result clob; + l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); + l_result_string varchar2(32767); + l_other ut_data_value_refcursor; + l_self ut_data_value_refcursor := self; + l_column_diffs ut_compound_data_helper.tt_column_diffs; + + l_other_cols ut_cursor_column_tab; + l_self_cols ut_cursor_column_tab; + + l_act_missing_pk ut_varchar2_list := ut_varchar2_list(); + l_exp_missing_pk ut_varchar2_list := ut_varchar2_list(); + + c_max_rows integer := ut_utils.gc_diff_max_rows; + l_diff_id ut_compound_data_helper.t_hash; + l_diff_row_count integer; + l_row_diffs ut_compound_data_helper.tt_row_diffs; + l_message varchar2(32767); + + function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is + begin + return + case a_col.diff_type + when '-' then + ' Column <'||a_col.expected_name||'> [data-type: '||a_col.expected_type||'] is missing. Expected column position: '||a_col.expected_pos||'.' + when '+' then + ' Column <'||a_col.actual_name||'> [position: '||a_col.actual_pos||', data-type: '||a_col.actual_type||'] is not expected in results.' + when 't' then + ' Column <'||a_col.actual_name||'> data-type is invalid. Expected: '||a_col.expected_type||',' ||' actual: '||a_col.actual_type||'.' + when 'p' then + ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' + end; + end; + + function remove_incomparable_cols( + a_cursor_details ut_cursor_column_tab, a_column_diffs ut_compound_data_helper.tt_column_diffs + ) return ut_cursor_column_tab is + l_missing_cols ut_varchar2_list := ut_varchar2_list(); + l_result ut_cursor_column_tab; + begin + for i in 1 .. a_column_diffs.count loop + if a_column_diffs(i).diff_type in ('-','+') then + l_missing_cols.extend; + l_missing_cols(l_missing_cols.last) := coalesce(a_column_diffs(i).expected_name, a_column_diffs(i).actual_name); + end if; + end loop; + select value(i) bulk collect into l_result + from table(a_cursor_details) i + where i.access_path not in ( + select c.column_value + from table(l_missing_cols) c + ); + return l_result; + end; + + function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs, a_is_unordered boolean) return varchar2 is + begin + if a_is_unordered then + if 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; + else + return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + else + return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + 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); + l_other.cursor_details.filter_columns(a_match_options); + l_self.cursor_details.filter_columns(a_match_options); + + l_other_cols := l_other.cursor_details.cursor_columns_info; + l_self_cols := l_self.cursor_details.cursor_columns_info; + + dbms_lob.createtemporary(l_result,true); + --diff columns + if not l_self.is_null and not l_other.is_null then + l_column_diffs := ut_compound_data_helper.get_columns_diff( + l_self.cursor_details.cursor_columns_info, + l_other.cursor_details.cursor_columns_info, + a_match_options.ordered_columns() + ); + + if l_column_diffs.count > 0 then + ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); + l_other_cols := remove_incomparable_cols( l_other_cols, l_column_diffs ); + l_self_cols := remove_incomparable_cols( l_self_cols, l_column_diffs ); + for i in 1 .. l_column_diffs.count loop + l_results.extend; + l_results(l_results.last) := get_col_diff_text(l_column_diffs(i)); + end loop; + ut_utils.append_to_clob(l_result, l_results); + end if; + end if; + + --check for missing pk + if a_match_options.join_by.items.count > 0 then + l_act_missing_pk := l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); + l_exp_missing_pk := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); + end if; + + --diff rows and row elements if the pk is not missing + if l_act_missing_pk.count + l_exp_missing_pk.count = 0 then + l_diff_id := ut_compound_data_helper.get_hash( l_self.data_id || l_other.data_id ); + + -- First tell how many rows are different + l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; + if l_diff_row_count > 0 then + l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( + l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, + l_diff_id, + case + when + l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) + else + a_match_options.join_by.items + end, + a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path + ); + 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 + ||' ]'||chr(10)|| case when l_row_diffs.count = 0 then ' All rows are different as the columns are not matching.' else null end; + ut_utils.append_to_clob( l_result, l_message ); + l_results := ut_utils.t_clob_tab(); + for i in 1 .. l_row_diffs.count loop + l_results.extend; + l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_match_options.unordered); + end loop; + ut_utils.append_to_clob(l_result,l_results); + else + l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.'; + ut_utils.append_to_clob( l_result, l_message ); + end if; + else + ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); + + for i in 1 .. l_exp_missing_pk.count loop + ut_utils.append_to_clob(l_result, ' Join key '||l_exp_missing_pk(i)||' does not exists in expected'||chr(10)); + end loop; + + for i in 1 .. l_act_missing_pk.count loop + ut_utils.append_to_clob(l_result, ' Join key '||l_act_missing_pk(i)||' does not exists in actual'||chr(10)); + end loop; + + if l_self.cursor_details.contains_collection() or l_other.cursor_details.contains_collection() then + ut_utils.append_to_clob(l_result,' Please make sure that your join clause is not refferring to collection element'|| chr(10)); + end if; + + 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) return integer is + begin + return compare_implementation( a_other, null ); + end; + + member function compare_implementation( + a_other ut_data_value, + a_match_options ut_matcher_options, + a_inclusion_compare boolean := false, + a_is_negated boolean := false + ) return integer is + l_result integer := 0; + l_self ut_data_value_refcursor := self; + l_other ut_data_value_refcursor; + l_diff_cursor_text clob; + + function compare_data( + a_self ut_data_value_refcursor, + a_other ut_data_value_refcursor, + a_diff_cursor_text clob + ) return integer is + l_diff_id ut_compound_data_helper.t_hash; + l_result integer; + --We will start with number od differences being displayed. + l_cursor sys_refcursor; + l_diff_tab ut_compound_data_helper.t_diff_tab; + l_diif_rowcount integer :=0; + begin + l_diff_id := ut_compound_data_helper.get_hash(a_self.data_id||a_other.data_id); + + begin + l_cursor := ut_compound_data_helper.get_compare_cursor(a_diff_cursor_text, + a_self.data_id, a_other.data_id); + --fetch and save rows for display of diff + fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_diff_max_rows; + exception when others then + if l_cursor%isopen then + close l_cursor; + end if; + raise; + end; + + ut_compound_data_helper.insert_diffs_result( l_diff_tab, l_diff_id ); + --fetch rows for count only + loop + exit when l_diff_tab.count = 0; + l_diif_rowcount := l_diif_rowcount + l_diff_tab.count; + fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_bc_fetch_limit; + end loop; + + ut_compound_data_helper.set_rows_diff(l_diif_rowcount); + + --result is OK only if both are same + if l_diif_rowcount = 0 and a_self.is_null = a_other.is_null then + l_result := 0; + else + l_result := 1; + end if; + close l_cursor; + return l_result; + 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); + l_other.cursor_details.filter_columns( a_match_options ); + l_self.cursor_details.filter_columns( a_match_options ); + + if a_match_options.join_by.items.count > 0 then + l_result := + l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count + + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; + end if; + + if l_result = 0 then + if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then + l_result := 1; + end if; + + l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( + l_other, + a_match_options.join_by.items, + a_match_options.unordered(), + a_inclusion_compare, + a_is_negated + ); + l_result := l_result + compare_data( l_self, l_other, l_diff_cursor_text ); + end if; + return l_result; + end; + + overriding member function is_empty return boolean is + begin + return self.elements_count = 0; + end; + +end; +/ From 7d27d3a4ffba48d1b0ab3f8fd3022b7c8268e0a6 Mon Sep 17 00:00:00 2001 From: LUKASZ104 Date: Fri, 24 May 2019 14:43:58 +0100 Subject: [PATCH 5/8] TAG: Phase2 Adding a new attribute filterpath that is used for filtering cursor in anydata / refcursor. This value will be different from cursor in anydata as we skip root element. --- source/core/ut_utils.pkb | 1644 +++++++++-------- source/core/ut_utils.pks | 796 ++++---- .../data_values/ut_compound_data_helper.pkb | 1388 +++++++------- .../data_values/ut_cursor_column.tpb | 139 +- .../data_values/ut_cursor_column.tps | 103 +- .../data_values/ut_cursor_details.tpb | 543 +++--- .../data_values/ut_cursor_details.tps | 83 +- .../data_values/ut_data_value_anydata.tpb | 288 ++- .../data_values/ut_data_value_refcursor.tpb | 797 ++++---- 9 files changed, 2882 insertions(+), 2899 deletions(-) diff --git a/source/core/ut_utils.pkb b/source/core/ut_utils.pkb index 2e35229e6..5b80e1f59 100644 --- a/source/core/ut_utils.pkb +++ b/source/core/ut_utils.pkb @@ -1,817 +1,827 @@ -create or replace package body ut_utils is - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - /** - * Constants regex used to validate XML name - */ - gc_invalid_first_xml_char constant varchar2(50) := '[^_a-zA-Z]'; - gc_invalid_xml_char constant varchar2(50) := '[^_a-zA-Z0-9\.-]'; - gc_full_valid_xml_name constant varchar2(50) := '^([_a-zA-Z])([_a-zA-Z0-9\.-])*$'; - - function surround_with(a_value varchar2, a_quote_char varchar2) return varchar2 is - begin - return case when a_quote_char is not null then a_quote_char||a_value||a_quote_char else a_value end; - end; - - function test_result_to_char(a_test_result integer) return varchar2 as - l_result varchar2(20); - begin - if a_test_result = gc_success then - l_result := gc_success_char; - elsif a_test_result = gc_failure then - l_result := gc_failure_char; - elsif a_test_result = gc_error then - l_result := gc_error_char; - elsif a_test_result = gc_disabled then - l_result := gc_disabled_char; - else - l_result := 'Unknown(' || coalesce(to_char(a_test_result),'NULL') || ')'; - end if ; - return l_result; - end test_result_to_char; - - - function to_test_result(a_test boolean) return integer is - l_result integer; - begin - if a_test then - l_result := gc_success; - else - l_result := gc_failure; - end if; - return l_result; - end; - - function gen_savepoint_name return varchar2 is - begin - return 's'||trim(to_char(ut_savepoint_seq.nextval,'0000000000000000000000000000')); - end; - - procedure debug_log(a_message varchar2) is - begin - $if $$ut_trace $then - dbms_output.put_line(a_message); - $else - null; - $end - end; - - procedure debug_log(a_message clob) is - l_varchars ut_varchar2_list; - begin - $if $$ut_trace $then - l_varchars := clob_to_table(a_message); - for i in 1..l_varchars.count loop - dbms_output.put_line(l_varchars(i)); - end loop; - $else - null; - $end - end; - - function to_string( - a_value varchar2, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2 is - l_result varchar2(32767); - c_length constant integer := coalesce( length( a_value ), 0 ); - c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); - c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; - begin - if c_length = 0 then - l_result := gc_null_string; - elsif c_length <= c_max_input_string_length then - l_result := surround_with(a_value, a_quote_char); - else - l_result := surround_with(substr(a_value, 1, c_overflow_substr_len ), a_quote_char) || gc_more_data_string; - end if ; - return l_result; - end; - - function to_string( - a_value clob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2 is - l_result varchar2(32767); - c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); - c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); - c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; - begin - if a_value is null then - l_result := gc_null_string; - elsif c_length = 0 then - l_result := gc_empty_string; - elsif c_length <= c_max_input_string_length then - l_result := surround_with(a_value,a_quote_char); - else - l_result := surround_with(dbms_lob.substr(a_value, c_overflow_substr_len), a_quote_char) || gc_more_data_string; - end if; - return l_result; - end; - - function to_string( - a_value blob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2 is - l_result varchar2(32767); - c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); - c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); - c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; - begin - if a_value is null then - l_result := gc_null_string; - elsif c_length = 0 then - l_result := gc_empty_string; - elsif c_length <= c_max_input_string_length then - l_result := surround_with(rawtohex(a_value),a_quote_char); - else - l_result := to_string( rawtohex(dbms_lob.substr(a_value, c_overflow_substr_len)) ); - end if ; - return l_result; - end; - - function to_string(a_value boolean) return varchar2 is - begin - return case a_value when true then 'TRUE' when false then 'FALSE' else gc_null_string end; - end; - - function to_string(a_value number) return varchar2 is - begin - return coalesce(to_char(a_value,gc_number_format), gc_null_string); - end; - - function to_string(a_value date) return varchar2 is - begin - return coalesce(to_char(a_value,gc_date_format), gc_null_string); - end; - - function to_string(a_value timestamp_unconstrained) return varchar2 is - begin - return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); - end; - - function to_string(a_value timestamp_tz_unconstrained) return varchar2 is - begin - return coalesce(to_char(a_value,gc_timestamp_tz_format), gc_null_string); - end; - - function to_string(a_value timestamp_ltz_unconstrained) return varchar2 is - begin - return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); - end; - - function to_string(a_value yminterval_unconstrained) return varchar2 IS - begin - return coalesce(to_char(a_value), gc_null_string); - end; - - function to_string(a_value dsinterval_unconstrained) return varchar2 IS - begin - return coalesce(to_char(a_value), gc_null_string); - end; - - - function boolean_to_int(a_value boolean) return integer is - begin - return case a_value when true then 1 when false then 0 end; - end; - - function int_to_boolean(a_value integer) return boolean is - begin - return case a_value when 1 then true when 0 then false end; - end; - - function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list is - l_offset integer := 1; - l_delimiter_position integer; - l_skip_leading_delimiter boolean := coalesce(a_skip_leading_delimiter = 'Y',false); - l_result ut_varchar2_list := ut_varchar2_list(); - begin - if a_string is null then - return l_result; - end if; - if a_delimiter is null then - return ut_varchar2_list(a_string); - end if; - - loop - l_delimiter_position := instr(a_string, a_delimiter, l_offset); - if not (l_delimiter_position = 1 and l_skip_leading_delimiter) then - l_result.extend; - if l_delimiter_position > 0 then - l_result(l_result.last) := substr(a_string, l_offset, l_delimiter_position - l_offset); - else - l_result(l_result.last) := substr(a_string, l_offset); - end if; - end if; - exit when l_delimiter_position = 0; - l_offset := l_delimiter_position + 1; - end loop; - return l_result; - end; - - function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list is - l_offset integer := 1; - l_length integer := dbms_lob.getlength(a_clob); - l_amount integer; - l_buffer varchar2(32767); - l_last_line varchar2(32767); - l_string_results ut_varchar2_list; - l_results ut_varchar2_list := ut_varchar2_list(); - l_has_last_line boolean; - l_skip_leading_delimiter varchar2(1) := 'N'; - begin - while l_offset <= l_length loop - l_amount := a_max_amount - coalesce( length(l_last_line), 0 ); - dbms_lob.read(a_clob, l_amount, l_offset, l_buffer); - l_offset := l_offset + l_amount; - - l_string_results := string_to_table( l_last_line || l_buffer, a_delimiter, l_skip_leading_delimiter ); - for i in 1 .. l_string_results.count loop - --if a split of lines was not done or not at the last line - if l_string_results.count = 1 or i < l_string_results.count then - l_results.extend; - l_results(l_results.last) := l_string_results(i); - end if; - end loop; - - --check if we need to append the last line to the next element - if l_string_results.count = 1 then - l_has_last_line := false; - l_last_line := null; - elsif l_string_results.count > 1 then - l_has_last_line := true; - l_last_line := l_string_results(l_string_results.count); - end if; - - l_skip_leading_delimiter := 'Y'; - end loop; - if l_has_last_line then - l_results.extend; - l_results(l_results.last) := l_last_line; - end if; - return l_results; - end; - - function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob is - l_result clob; - l_table_rows integer := coalesce(cardinality(a_text_table),0); - begin - for i in 1 .. l_table_rows loop - if i < l_table_rows then - append_to_clob(l_result, a_text_table(i)||a_delimiter); - else - append_to_clob(l_result, a_text_table(i)); - end if; - end loop; - return l_result; - end; - - function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob is - l_result clob; - l_table_rows integer := coalesce(cardinality(a_integer_table),0); - begin - for i in 1 .. l_table_rows loop - if i < l_table_rows then - append_to_clob(l_result, a_integer_table(i)||a_delimiter); - else - append_to_clob(l_result, a_integer_table(i)); - end if; - end loop; - return l_result; - end; - - function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number is - begin - return - extract(day from(a_end_time - a_start_time)) * 24 * 60 * 60 + - extract(hour from(a_end_time - a_start_time)) * 60 * 60 + - extract(minute from(a_end_time - a_start_time)) * 60 + - extract(second from(a_end_time - a_start_time)); - end; - - function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2 is - begin - if a_include_first_line then - return rtrim(lpad( ' ', a_indent_size ) || replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); - else - return rtrim(replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); - end if; - end; - - function get_utplsql_objects_list return ut_object_names is - l_result ut_object_names; - begin - select distinct ut_object_name(sys_context('userenv','current_user'), o.object_name) - bulk collect into l_result - from user_objects o - where o.object_name = 'UT' or object_name like 'UT\_%' escape '\' - and o.object_type <> 'SYNONYM'; - return l_result; - end; - - procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2) is - begin - if a_item is not null then - if a_list is null then - a_list := ut_varchar2_list(); - end if; - a_list.extend; - a_list(a_list.last) := a_item; - end if; - end append_to_list; - - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows) is - begin - if a_items is not null then - if a_list is null then - a_list := ut_varchar2_rows(); - end if; - for i in 1 .. a_items.count loop - a_list.extend; - a_list(a_list.last) := a_items(i); - end loop; - end if; - end; - - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob) is - begin - append_to_list( - a_list, - convert_collection( - clob_to_table( a_item, ut_utils.gc_max_storage_varchar2_len ) - ) - ); - end; - - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2) is - begin - if a_item is not null then - if a_list is null then - a_list := ut_varchar2_rows(); - end if; - if length(a_item) > gc_max_storage_varchar2_len then - append_to_list( - a_list, - ut_utils.convert_collection( - ut_utils.clob_to_table( a_item, gc_max_storage_varchar2_len ) - ) - ); - else - a_list.extend; - a_list(a_list.last) := a_item; - end if; - end if; - end append_to_list; - - procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2:= chr(10)) is - begin - if a_clob_table is not null and cardinality(a_clob_table) > 0 then - if a_src_clob is null then - dbms_lob.createtemporary(a_src_clob, true); - end if; - for i in 1 .. a_clob_table.count loop - dbms_lob.append(a_src_clob,a_clob_table(i)); - if i < a_clob_table.count then - append_to_clob(a_src_clob,a_delimiter); - end if; - end loop; - end if; - end; - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob) is - begin - if a_new_data is not null and dbms_lob.getlength(a_new_data) > 0 then - if a_src_clob is null then - dbms_lob.createtemporary(a_src_clob, true); - end if; - dbms_lob.append(a_src_clob, a_new_data); - end if; - end; - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2) is - begin - if a_new_data is not null then - if a_src_clob is null then - dbms_lob.createtemporary(a_src_clob, true); - end if; - dbms_lob.writeappend(a_src_clob, dbms_lob.getlength(a_new_data), a_new_data); - end if; - end; - - function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows is - l_result ut_varchar2_rows; - begin - if a_collection is not null then - l_result := ut_varchar2_rows(); - for i in 1 .. a_collection.count loop - l_result.extend(); - l_result(i) := substr(a_collection(i),1,gc_max_storage_varchar2_len); - end loop; - end if; - return l_result; - end; - - procedure set_action(a_text in varchar2) is - begin - dbms_application_info.set_module('utPLSQL', a_text); - end; - - procedure set_client_info(a_text in varchar2) is - begin - dbms_application_info.set_client_info(a_text); - end; - - function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2 is - l_xpath varchar2(32767) := a_list; - begin - l_xpath := to_xpath( clob_to_table(a_clob=>a_list, a_delimiter=>','), a_ancestors); - return l_xpath; - end; - - function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2 is - l_xpath varchar2(32767); - l_item varchar2(32767); - l_iter integer; - begin - if a_list is not null then - l_iter := a_list.first; - while l_iter is not null loop - l_item := trim(a_list(l_iter)); - if l_item is not null then - if l_item like '%,%' then - l_xpath := l_xpath || to_xpath( l_item, a_ancestors ) || '|'; - elsif l_item like '/%' then - l_xpath := l_xpath || l_item || '|'; - else - l_xpath := l_xpath || a_ancestors || l_item || '|'; - end if; - end if; - l_iter := a_list.next(l_iter); - end loop; - l_xpath := rtrim(l_xpath,',|'); - end if; - return l_xpath; - end; - - procedure cleanup_temp_tables is - begin - execute immediate 'delete from ut_compound_data_tmp'; - execute immediate 'delete from ut_compound_data_diff_tmp'; - end; - - function to_version(a_version_no varchar2) return t_version is - l_result t_version; - c_version_part_regex constant varchar2(20) := '[0-9]+'; - begin - - if regexp_like(a_version_no,'v?([0-9]+(\.|$)){1,4}') then - l_result.major := regexp_substr(a_version_no, c_version_part_regex, 1, 1); - l_result.minor := regexp_substr(a_version_no, c_version_part_regex, 1, 2); - l_result.bugfix := regexp_substr(a_version_no, c_version_part_regex, 1, 3); - l_result.build := regexp_substr(a_version_no, c_version_part_regex, 1, 4); - else - raise_application_error(gc_invalid_version_no, 'Version string "'||a_version_no||'" is not a valid version'); - end if; - return l_result; - end; - - procedure save_dbms_output_to_cache is - l_status number; - l_line varchar2(32767); - l_offset integer := 0; - l_lines ut_varchar2_rows := ut_varchar2_rows(); - c_lines_limit constant integer := 100; - pragma autonomous_transaction; - - procedure flush_lines(a_lines ut_varchar2_rows, a_offset integer) is - begin - insert into ut_dbms_output_cache (seq_no,text) - select rownum+a_offset, column_value - from table(a_lines); - end; - begin - loop - dbms_output.get_line(line => l_line, status => l_status); - exit when l_status = 1; - l_lines := l_lines multiset union all ut_utils.convert_collection(ut_utils.clob_to_table(l_line||chr(7),4000)); - if l_lines.count > c_lines_limit then - flush_lines(l_lines, l_offset); - l_offset := l_offset + l_lines.count; - l_lines.delete; - end if; - end loop; - flush_lines(l_lines, l_offset); - commit; - end; - - procedure read_cache_to_dbms_output is - l_lines_data sys_refcursor; - l_lines ut_varchar2_rows; - c_lines_limit constant integer := 100; - pragma autonomous_transaction; - begin - open l_lines_data for select text from ut_dbms_output_cache order by seq_no; - loop - fetch l_lines_data bulk collect into l_lines limit c_lines_limit; - for i in 1 .. l_lines.count loop - if substr(l_lines(i),-1) = chr(7) then - dbms_output.put_line(rtrim(l_lines(i),chr(7))); - else - dbms_output.put(l_lines(i)); - end if; - end loop; - exit when l_lines_data%notfound; - end loop; - delete from ut_dbms_output_cache; - commit; - end; - - function ut_owner return varchar2 is - begin - return sys_context('userenv','current_schema'); - end; - - function scale_cardinality(a_cardinality natural) return natural is - begin - return nvl(trunc(power(10,(floor(log(10,a_cardinality))+1))/3),0); - end; - - function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2 is - begin - return 'The syntax: "'||a_old_syntax||'" is deprecated.' ||chr(10)|| - 'Please use the new syntax: "'||a_new_syntax||'".' ||chr(10)|| - 'The deprecated syntax will not be supported in future releases.'; - end; - - function to_xml_number_format(a_value number) return varchar2 is - begin - return to_char(a_value, gc_number_format, 'NLS_NUMERIC_CHARACTERS=''. '''); - end; - - function get_xml_header(a_encoding varchar2) return varchar2 is - begin - return - ''; - end; - - function trim_list_elements(a_list ut_varchar2_list, a_regexp_to_trim varchar2 default '[:space:]') return ut_varchar2_list is - l_trimmed_list ut_varchar2_list; - l_index integer; - begin - if a_list is not null then - l_trimmed_list := ut_varchar2_list(); - l_index := a_list.first; - - while (l_index is not null) loop - l_trimmed_list.extend; - l_trimmed_list(l_trimmed_list.count) := regexp_replace(a_list(l_index), '(^['||a_regexp_to_trim||']*)|(['||a_regexp_to_trim||']*$)'); - l_index := a_list.next(l_index); - end loop; - end if; - - return l_trimmed_list; - end; - - function filter_list(a_list in ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list is - l_filtered_list ut_varchar2_list; - l_index integer; - begin - if a_list is not null then - l_filtered_list := ut_varchar2_list(); - l_index := a_list.first; - - while (l_index is not null) loop - if regexp_like(a_list(l_index), a_regexp_filter) then - l_filtered_list.extend; - l_filtered_list(l_filtered_list.count) := a_list(l_index); - end if; - l_index := a_list.next(l_index); - end loop; - end if; - - return l_filtered_list; - end; - - function xmlgen_escaped_string(a_string in varchar2) return varchar2 is - l_result varchar2(4000) := a_string; - l_sql varchar2(32767) := q'!select q'[!'||a_string||q'!]' as "!'||a_string||'" from dual'; - begin - if a_string is not null then - select extract(dbms_xmlgen.getxmltype(l_sql),'/*/*/*').getRootElement() - into l_result - from dual; - end if; - return l_result; - end; - - function replace_multiline_comments(a_source clob) return clob is - l_result clob; - l_ml_comment_start binary_integer := 1; - l_comment_start binary_integer := 1; - l_text_start binary_integer := 1; - l_escaped_text_start binary_integer := 1; - l_escaped_text_end_char varchar2(1 char); - l_end binary_integer := 1; - l_ml_comment clob; - l_newlines_count binary_integer; - l_offset binary_integer := 1; - l_length binary_integer := coalesce(dbms_lob.getlength(a_source), 0); - begin - l_ml_comment_start := instr(a_source,'/*'); - l_comment_start := instr(a_source,'--'); - l_text_start := instr(a_source,''''); - l_escaped_text_start := instr(a_source,q'[q']'); - while l_offset > 0 and l_ml_comment_start > 0 loop - - if l_ml_comment_start > 0 and (l_ml_comment_start < l_comment_start or l_comment_start = 0) - and (l_ml_comment_start < l_text_start or l_text_start = 0)and (l_ml_comment_start < l_escaped_text_start or l_escaped_text_start = 0) - then - l_end := instr(a_source,'*/',l_ml_comment_start+2); - append_to_clob(l_result, dbms_lob.substr(a_source, l_ml_comment_start-l_offset, l_offset)); - if l_end > 0 then - l_ml_comment := substr(a_source, l_ml_comment_start, l_end-l_ml_comment_start); - l_newlines_count := length( l_ml_comment ) - length( translate( l_ml_comment, 'a'||chr(10), 'a') ); - if l_newlines_count > 0 then - append_to_clob(l_result, lpad( chr(10), l_newlines_count, chr(10) ) ); - end if; - l_end := l_end + 2; - end if; - else - - if l_comment_start > 0 and (l_comment_start < l_ml_comment_start or l_ml_comment_start = 0) - and (l_comment_start < l_text_start or l_text_start = 0) and (l_comment_start < l_escaped_text_start or l_escaped_text_start = 0) - then - l_end := instr(a_source,chr(10),l_comment_start+2); - if l_end > 0 then - l_end := l_end + 1; - end if; - elsif l_text_start > 0 and (l_text_start < l_ml_comment_start or l_ml_comment_start = 0) - and (l_text_start < l_comment_start or l_comment_start = 0) and (l_text_start < l_escaped_text_start or l_escaped_text_start = 0) - then - l_end := instr(a_source,q'[']',l_text_start+1); - - --skip double quotes while searching for end of quoted text - while l_end > 0 and l_end = instr(a_source,q'['']',l_text_start+1) loop - l_end := instr(a_source,q'[']',l_end+1); - end loop; - if l_end > 0 then - l_end := l_end + 1; - end if; - - elsif l_escaped_text_start > 0 and (l_escaped_text_start < l_ml_comment_start or l_ml_comment_start = 0) - and (l_escaped_text_start < l_comment_start or l_comment_start = 0) and (l_escaped_text_start < l_text_start or l_text_start = 0) - then - --translate char "[" from the start of quoted text "q'[someting]'" into "]" - l_escaped_text_end_char := translate( substr(a_source, l_escaped_text_start + 2, 1), '[{(<', ']})>'); - l_end := instr(a_source,l_escaped_text_end_char||'''',l_escaped_text_start + 3 ); - if l_end > 0 then - l_end := l_end + 2; - end if; - end if; - - if l_end = 0 then - append_to_clob(l_result, substr(a_source, l_offset, l_length-l_offset)); - else - append_to_clob(l_result, substr(a_source, l_offset, l_end-l_offset)); - end if; - end if; - l_offset := l_end; - if l_offset >= l_ml_comment_start then - l_ml_comment_start := instr(a_source,'/*',l_offset); - end if; - if l_offset >= l_comment_start then - l_comment_start := instr(a_source,'--',l_offset); - end if; - if l_offset >= l_text_start then - l_text_start := instr(a_source,'''',l_offset); - end if; - if l_offset >= l_escaped_text_start then - l_escaped_text_start := instr(a_source,q'[q']',l_offset); - end if; - end loop; - append_to_clob(l_result, substr(a_source, l_end)); - return l_result; - end; - - function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info is - l_for_reporters ut_reporters_info := a_for_reporters; - l_results ut_reporters_info; - begin - if l_for_reporters is null then - l_for_reporters := ut_reporters_info(ut_reporter_info('UT_REPORTER_BASE','N','N','N')); - end if; - - select /*+ cardinality(f 10) */ - ut_reporter_info( - object_name => t.type_name, - is_output_reporter => - case - when f.is_output_reporter = 'Y' or t.type_name = 'UT_OUTPUT_REPORTER_BASE' - then 'Y' else 'N' - end, - is_instantiable => case when t.instantiable = 'YES' then 'Y' else 'N' end, - is_final => case when t.final = 'YES' then 'Y' else 'N' end - ) - bulk collect into l_results - from user_types t - join (select * from table(l_for_reporters) where is_final = 'N' ) f - on f.object_name = supertype_name; - - return l_results; - end; - - function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2 is - l_caller_stack_line varchar2(4000); - l_ora_search_pattern varchar2(500) := '^ORA'||a_ora_code||': (.*)$'; - begin - l_caller_stack_line := regexp_replace(srcstr => a_error_stack - ,pattern => l_ora_search_pattern - ,replacestr => null - ,position => 1 - ,occurrence => 1 - ,modifier => 'm'); - return l_caller_stack_line; - end; - - /** - * Change string into unicode to match xmlgen format _00_ - * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/adxdb/generation-of-XML-data-from-relational-data.html#GUID-5BE09A7D-80D8-4734-B9AF-4A61F27FA9B2 - * secion v3.1.7.2935-develop - */ - function char_to_xmlgen_unicode(a_character varchar2) return varchar2 is - begin - return '_x00'||rawtohex(utl_raw.cast_to_raw(a_character))||'_'; - end; - - /** - * Build valid XML column name as element names can contain letters, digits, hyphens, underscores, and periods - */ - function build_valid_xml_name(a_preprocessed_name varchar2) return varchar2 is - l_post_processed varchar2(4000); - begin - for i in (select regexp_substr( a_preprocessed_name ,'(.{1})', 1, level, null, 1 ) AS string_char,level level_no - from dual connect by level <= regexp_count(a_preprocessed_name, '(.{1})')) - loop - if i.level_no = 1 and regexp_like(i.string_char,gc_invalid_first_xml_char) then - l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); - elsif regexp_like(i.string_char,gc_invalid_xml_char) then - l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); - else - l_post_processed := l_post_processed || i.string_char; - end if; - end loop; - return l_post_processed; - end; - - function get_valid_xml_name(a_name varchar2) return varchar2 is - l_valid_name varchar2(4000); - begin - if regexp_like(a_name,gc_full_valid_xml_name) then - l_valid_name := a_name; - else - l_valid_name := build_valid_xml_name(a_name); - end if; - return l_valid_name; - end; - - function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list is - l_result ut_varchar2_list := ut_varchar2_list(); - l_idx binary_integer; - begin - if a_prefix is not null then - l_idx := a_list.first; - while l_idx is not null loop - l_result.extend; - l_result(l_idx) := a_prefix||a_connector||trim(leading a_connector from a_list(l_idx)); - l_idx := a_list.next(l_idx); - end loop; - end if; - return l_result; - end; - -end ut_utils; -/ +create or replace package body ut_utils is + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + /** + * Constants regex used to validate XML name + */ + gc_invalid_first_xml_char constant varchar2(50) := '[^_a-zA-Z]'; + gc_invalid_xml_char constant varchar2(50) := '[^_a-zA-Z0-9\.-]'; + gc_full_valid_xml_name constant varchar2(50) := '^([_a-zA-Z])([_a-zA-Z0-9\.-])*$'; + + function surround_with(a_value varchar2, a_quote_char varchar2) return varchar2 is + begin + return case when a_quote_char is not null then a_quote_char||a_value||a_quote_char else a_value end; + end; + + function test_result_to_char(a_test_result integer) return varchar2 as + l_result varchar2(20); + begin + if a_test_result = gc_success then + l_result := gc_success_char; + elsif a_test_result = gc_failure then + l_result := gc_failure_char; + elsif a_test_result = gc_error then + l_result := gc_error_char; + elsif a_test_result = gc_disabled then + l_result := gc_disabled_char; + else + l_result := 'Unknown(' || coalesce(to_char(a_test_result),'NULL') || ')'; + end if ; + return l_result; + end test_result_to_char; + + + function to_test_result(a_test boolean) return integer is + l_result integer; + begin + if a_test then + l_result := gc_success; + else + l_result := gc_failure; + end if; + return l_result; + end; + + function gen_savepoint_name return varchar2 is + begin + return 's'||trim(to_char(ut_savepoint_seq.nextval,'0000000000000000000000000000')); + end; + + procedure debug_log(a_message varchar2) is + begin + $if $$ut_trace $then + dbms_output.put_line(a_message); + $else + null; + $end + end; + + procedure debug_log(a_message clob) is + l_varchars ut_varchar2_list; + begin + $if $$ut_trace $then + l_varchars := clob_to_table(a_message); + for i in 1..l_varchars.count loop + dbms_output.put_line(l_varchars(i)); + end loop; + $else + null; + $end + end; + + function to_string( + a_value varchar2, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2 is + l_result varchar2(32767); + c_length constant integer := coalesce( length( a_value ), 0 ); + c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); + c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; + begin + if c_length = 0 then + l_result := gc_null_string; + elsif c_length <= c_max_input_string_length then + l_result := surround_with(a_value, a_quote_char); + else + l_result := surround_with(substr(a_value, 1, c_overflow_substr_len ), a_quote_char) || gc_more_data_string; + end if ; + return l_result; + end; + + function to_string( + a_value clob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2 is + l_result varchar2(32767); + c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); + c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); + c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; + begin + if a_value is null then + l_result := gc_null_string; + elsif c_length = 0 then + l_result := gc_empty_string; + elsif c_length <= c_max_input_string_length then + l_result := surround_with(a_value,a_quote_char); + else + l_result := surround_with(dbms_lob.substr(a_value, c_overflow_substr_len), a_quote_char) || gc_more_data_string; + end if; + return l_result; + end; + + function to_string( + a_value blob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2 is + l_result varchar2(32767); + c_length constant integer := coalesce(dbms_lob.getlength(a_value), 0); + c_max_input_string_length constant integer := a_max_output_len - coalesce( length( a_quote_char ) * 2, 0 ); + c_overflow_substr_len constant integer := c_max_input_string_length - gc_more_data_string_len; + begin + if a_value is null then + l_result := gc_null_string; + elsif c_length = 0 then + l_result := gc_empty_string; + elsif c_length <= c_max_input_string_length then + l_result := surround_with(rawtohex(a_value),a_quote_char); + else + l_result := to_string( rawtohex(dbms_lob.substr(a_value, c_overflow_substr_len)) ); + end if ; + return l_result; + end; + + function to_string(a_value boolean) return varchar2 is + begin + return case a_value when true then 'TRUE' when false then 'FALSE' else gc_null_string end; + end; + + function to_string(a_value number) return varchar2 is + begin + return coalesce(to_char(a_value,gc_number_format), gc_null_string); + end; + + function to_string(a_value date) return varchar2 is + begin + return coalesce(to_char(a_value,gc_date_format), gc_null_string); + end; + + function to_string(a_value timestamp_unconstrained) return varchar2 is + begin + return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); + end; + + function to_string(a_value timestamp_tz_unconstrained) return varchar2 is + begin + return coalesce(to_char(a_value,gc_timestamp_tz_format), gc_null_string); + end; + + function to_string(a_value timestamp_ltz_unconstrained) return varchar2 is + begin + return coalesce(to_char(a_value,gc_timestamp_format), gc_null_string); + end; + + function to_string(a_value yminterval_unconstrained) return varchar2 IS + begin + return coalesce(to_char(a_value), gc_null_string); + end; + + function to_string(a_value dsinterval_unconstrained) return varchar2 IS + begin + return coalesce(to_char(a_value), gc_null_string); + end; + + + function boolean_to_int(a_value boolean) return integer is + begin + return case a_value when true then 1 when false then 0 end; + end; + + function int_to_boolean(a_value integer) return boolean is + begin + return case a_value when 1 then true when 0 then false end; + end; + + function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list is + l_offset integer := 1; + l_delimiter_position integer; + l_skip_leading_delimiter boolean := coalesce(a_skip_leading_delimiter = 'Y',false); + l_result ut_varchar2_list := ut_varchar2_list(); + begin + if a_string is null then + return l_result; + end if; + if a_delimiter is null then + return ut_varchar2_list(a_string); + end if; + + loop + l_delimiter_position := instr(a_string, a_delimiter, l_offset); + if not (l_delimiter_position = 1 and l_skip_leading_delimiter) then + l_result.extend; + if l_delimiter_position > 0 then + l_result(l_result.last) := substr(a_string, l_offset, l_delimiter_position - l_offset); + else + l_result(l_result.last) := substr(a_string, l_offset); + end if; + end if; + exit when l_delimiter_position = 0; + l_offset := l_delimiter_position + 1; + end loop; + return l_result; + end; + + function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list is + l_offset integer := 1; + l_length integer := dbms_lob.getlength(a_clob); + l_amount integer; + l_buffer varchar2(32767); + l_last_line varchar2(32767); + l_string_results ut_varchar2_list; + l_results ut_varchar2_list := ut_varchar2_list(); + l_has_last_line boolean; + l_skip_leading_delimiter varchar2(1) := 'N'; + begin + while l_offset <= l_length loop + l_amount := a_max_amount - coalesce( length(l_last_line), 0 ); + dbms_lob.read(a_clob, l_amount, l_offset, l_buffer); + l_offset := l_offset + l_amount; + + l_string_results := string_to_table( l_last_line || l_buffer, a_delimiter, l_skip_leading_delimiter ); + for i in 1 .. l_string_results.count loop + --if a split of lines was not done or not at the last line + if l_string_results.count = 1 or i < l_string_results.count then + l_results.extend; + l_results(l_results.last) := l_string_results(i); + end if; + end loop; + + --check if we need to append the last line to the next element + if l_string_results.count = 1 then + l_has_last_line := false; + l_last_line := null; + elsif l_string_results.count > 1 then + l_has_last_line := true; + l_last_line := l_string_results(l_string_results.count); + end if; + + l_skip_leading_delimiter := 'Y'; + end loop; + if l_has_last_line then + l_results.extend; + l_results(l_results.last) := l_last_line; + end if; + return l_results; + end; + + function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob is + l_result clob; + l_table_rows integer := coalesce(cardinality(a_text_table),0); + begin + for i in 1 .. l_table_rows loop + if i < l_table_rows then + append_to_clob(l_result, a_text_table(i)||a_delimiter); + else + append_to_clob(l_result, a_text_table(i)); + end if; + end loop; + return l_result; + end; + + function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob is + l_result clob; + l_table_rows integer := coalesce(cardinality(a_integer_table),0); + begin + for i in 1 .. l_table_rows loop + if i < l_table_rows then + append_to_clob(l_result, a_integer_table(i)||a_delimiter); + else + append_to_clob(l_result, a_integer_table(i)); + end if; + end loop; + return l_result; + end; + + function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number is + begin + return + extract(day from(a_end_time - a_start_time)) * 24 * 60 * 60 + + extract(hour from(a_end_time - a_start_time)) * 60 * 60 + + extract(minute from(a_end_time - a_start_time)) * 60 + + extract(second from(a_end_time - a_start_time)); + end; + + function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2 is + begin + if a_include_first_line then + return rtrim(lpad( ' ', a_indent_size ) || replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); + else + return rtrim(replace( a_text, chr(10), chr(10) || lpad( ' ', a_indent_size ) )); + end if; + end; + + function get_utplsql_objects_list return ut_object_names is + l_result ut_object_names; + begin + select distinct ut_object_name(sys_context('userenv','current_user'), o.object_name) + bulk collect into l_result + from user_objects o + where o.object_name = 'UT' or object_name like 'UT\_%' escape '\' + and o.object_type <> 'SYNONYM'; + return l_result; + end; + + procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2) is + begin + if a_item is not null then + if a_list is null then + a_list := ut_varchar2_list(); + end if; + a_list.extend; + a_list(a_list.last) := a_item; + end if; + end append_to_list; + + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows) is + begin + if a_items is not null then + if a_list is null then + a_list := ut_varchar2_rows(); + end if; + for i in 1 .. a_items.count loop + a_list.extend; + a_list(a_list.last) := a_items(i); + end loop; + end if; + end; + + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob) is + begin + append_to_list( + a_list, + convert_collection( + clob_to_table( a_item, ut_utils.gc_max_storage_varchar2_len ) + ) + ); + end; + + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2) is + begin + if a_item is not null then + if a_list is null then + a_list := ut_varchar2_rows(); + end if; + if length(a_item) > gc_max_storage_varchar2_len then + append_to_list( + a_list, + ut_utils.convert_collection( + ut_utils.clob_to_table( a_item, gc_max_storage_varchar2_len ) + ) + ); + else + a_list.extend; + a_list(a_list.last) := a_item; + end if; + end if; + end append_to_list; + + procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2:= chr(10)) is + begin + if a_clob_table is not null and cardinality(a_clob_table) > 0 then + if a_src_clob is null then + dbms_lob.createtemporary(a_src_clob, true); + end if; + for i in 1 .. a_clob_table.count loop + dbms_lob.append(a_src_clob,a_clob_table(i)); + if i < a_clob_table.count then + append_to_clob(a_src_clob,a_delimiter); + end if; + end loop; + end if; + end; + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob) is + begin + if a_new_data is not null and dbms_lob.getlength(a_new_data) > 0 then + if a_src_clob is null then + dbms_lob.createtemporary(a_src_clob, true); + end if; + dbms_lob.append(a_src_clob, a_new_data); + end if; + end; + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2) is + begin + if a_new_data is not null then + if a_src_clob is null then + dbms_lob.createtemporary(a_src_clob, true); + end if; + dbms_lob.writeappend(a_src_clob, dbms_lob.getlength(a_new_data), a_new_data); + end if; + end; + + function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows is + l_result ut_varchar2_rows; + begin + if a_collection is not null then + l_result := ut_varchar2_rows(); + for i in 1 .. a_collection.count loop + l_result.extend(); + l_result(i) := substr(a_collection(i),1,gc_max_storage_varchar2_len); + end loop; + end if; + return l_result; + end; + + procedure set_action(a_text in varchar2) is + begin + dbms_application_info.set_module('utPLSQL', a_text); + end; + + procedure set_client_info(a_text in varchar2) is + begin + dbms_application_info.set_client_info(a_text); + end; + + function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2 is + l_xpath varchar2(32767) := a_list; + begin + l_xpath := to_xpath( clob_to_table(a_clob=>a_list, a_delimiter=>','), a_ancestors); + return l_xpath; + end; + + function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2 is + l_xpath varchar2(32767); + l_item varchar2(32767); + l_iter integer; + begin + if a_list is not null then + l_iter := a_list.first; + while l_iter is not null loop + l_item := trim(a_list(l_iter)); + if l_item is not null then + if l_item like '%,%' then + l_xpath := l_xpath || to_xpath( l_item, a_ancestors ) || '|'; + elsif l_item like '/%' then + l_xpath := l_xpath || l_item || '|'; + else + l_xpath := l_xpath || a_ancestors || l_item || '|'; + end if; + end if; + l_iter := a_list.next(l_iter); + end loop; + l_xpath := rtrim(l_xpath,',|'); + end if; + return l_xpath; + end; + + procedure cleanup_temp_tables is + begin + execute immediate 'delete from ut_compound_data_tmp'; + execute immediate 'delete from ut_compound_data_diff_tmp'; + end; + + function to_version(a_version_no varchar2) return t_version is + l_result t_version; + c_version_part_regex constant varchar2(20) := '[0-9]+'; + begin + + if regexp_like(a_version_no,'v?([0-9]+(\.|$)){1,4}') then + l_result.major := regexp_substr(a_version_no, c_version_part_regex, 1, 1); + l_result.minor := regexp_substr(a_version_no, c_version_part_regex, 1, 2); + l_result.bugfix := regexp_substr(a_version_no, c_version_part_regex, 1, 3); + l_result.build := regexp_substr(a_version_no, c_version_part_regex, 1, 4); + else + raise_application_error(gc_invalid_version_no, 'Version string "'||a_version_no||'" is not a valid version'); + end if; + return l_result; + end; + + procedure save_dbms_output_to_cache is + l_status number; + l_line varchar2(32767); + l_offset integer := 0; + l_lines ut_varchar2_rows := ut_varchar2_rows(); + c_lines_limit constant integer := 100; + pragma autonomous_transaction; + + procedure flush_lines(a_lines ut_varchar2_rows, a_offset integer) is + begin + insert into ut_dbms_output_cache (seq_no,text) + select rownum+a_offset, column_value + from table(a_lines); + end; + begin + loop + dbms_output.get_line(line => l_line, status => l_status); + exit when l_status = 1; + l_lines := l_lines multiset union all ut_utils.convert_collection(ut_utils.clob_to_table(l_line||chr(7),4000)); + if l_lines.count > c_lines_limit then + flush_lines(l_lines, l_offset); + l_offset := l_offset + l_lines.count; + l_lines.delete; + end if; + end loop; + flush_lines(l_lines, l_offset); + commit; + end; + + procedure read_cache_to_dbms_output is + l_lines_data sys_refcursor; + l_lines ut_varchar2_rows; + c_lines_limit constant integer := 100; + pragma autonomous_transaction; + begin + open l_lines_data for select text from ut_dbms_output_cache order by seq_no; + loop + fetch l_lines_data bulk collect into l_lines limit c_lines_limit; + for i in 1 .. l_lines.count loop + if substr(l_lines(i),-1) = chr(7) then + dbms_output.put_line(rtrim(l_lines(i),chr(7))); + else + dbms_output.put(l_lines(i)); + end if; + end loop; + exit when l_lines_data%notfound; + end loop; + delete from ut_dbms_output_cache; + commit; + end; + + function ut_owner return varchar2 is + begin + return sys_context('userenv','current_schema'); + end; + + function scale_cardinality(a_cardinality natural) return natural is + begin + return nvl(trunc(power(10,(floor(log(10,a_cardinality))+1))/3),0); + end; + + function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2 is + begin + return 'The syntax: "'||a_old_syntax||'" is deprecated.' ||chr(10)|| + 'Please use the new syntax: "'||a_new_syntax||'".' ||chr(10)|| + 'The deprecated syntax will not be supported in future releases.'; + end; + + function to_xml_number_format(a_value number) return varchar2 is + begin + return to_char(a_value, gc_number_format, 'NLS_NUMERIC_CHARACTERS=''. '''); + end; + + function get_xml_header(a_encoding varchar2) return varchar2 is + begin + return + ''; + end; + + function trim_list_elements(a_list ut_varchar2_list, a_regexp_to_trim varchar2 default '[:space:]') return ut_varchar2_list is + l_trimmed_list ut_varchar2_list; + l_index integer; + begin + if a_list is not null then + l_trimmed_list := ut_varchar2_list(); + l_index := a_list.first; + + while (l_index is not null) loop + l_trimmed_list.extend; + l_trimmed_list(l_trimmed_list.count) := regexp_replace(a_list(l_index), '(^['||a_regexp_to_trim||']*)|(['||a_regexp_to_trim||']*$)'); + l_index := a_list.next(l_index); + end loop; + end if; + + return l_trimmed_list; + end; + + function filter_list(a_list in ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list is + l_filtered_list ut_varchar2_list; + l_index integer; + begin + if a_list is not null then + l_filtered_list := ut_varchar2_list(); + l_index := a_list.first; + + while (l_index is not null) loop + if regexp_like(a_list(l_index), a_regexp_filter) then + l_filtered_list.extend; + l_filtered_list(l_filtered_list.count) := a_list(l_index); + end if; + l_index := a_list.next(l_index); + end loop; + end if; + + return l_filtered_list; + end; + + function xmlgen_escaped_string(a_string in varchar2) return varchar2 is + l_result varchar2(4000) := a_string; + l_sql varchar2(32767) := q'!select q'[!'||a_string||q'!]' as "!'||a_string||'" from dual'; + begin + if a_string is not null then + select extract(dbms_xmlgen.getxmltype(l_sql),'/*/*/*').getRootElement() + into l_result + from dual; + end if; + return l_result; + end; + + function replace_multiline_comments(a_source clob) return clob is + l_result clob; + l_ml_comment_start binary_integer := 1; + l_comment_start binary_integer := 1; + l_text_start binary_integer := 1; + l_escaped_text_start binary_integer := 1; + l_escaped_text_end_char varchar2(1 char); + l_end binary_integer := 1; + l_ml_comment clob; + l_newlines_count binary_integer; + l_offset binary_integer := 1; + l_length binary_integer := coalesce(dbms_lob.getlength(a_source), 0); + begin + l_ml_comment_start := instr(a_source,'/*'); + l_comment_start := instr(a_source,'--'); + l_text_start := instr(a_source,''''); + l_escaped_text_start := instr(a_source,q'[q']'); + while l_offset > 0 and l_ml_comment_start > 0 loop + + if l_ml_comment_start > 0 and (l_ml_comment_start < l_comment_start or l_comment_start = 0) + and (l_ml_comment_start < l_text_start or l_text_start = 0)and (l_ml_comment_start < l_escaped_text_start or l_escaped_text_start = 0) + then + l_end := instr(a_source,'*/',l_ml_comment_start+2); + append_to_clob(l_result, dbms_lob.substr(a_source, l_ml_comment_start-l_offset, l_offset)); + if l_end > 0 then + l_ml_comment := substr(a_source, l_ml_comment_start, l_end-l_ml_comment_start); + l_newlines_count := length( l_ml_comment ) - length( translate( l_ml_comment, 'a'||chr(10), 'a') ); + if l_newlines_count > 0 then + append_to_clob(l_result, lpad( chr(10), l_newlines_count, chr(10) ) ); + end if; + l_end := l_end + 2; + end if; + else + + if l_comment_start > 0 and (l_comment_start < l_ml_comment_start or l_ml_comment_start = 0) + and (l_comment_start < l_text_start or l_text_start = 0) and (l_comment_start < l_escaped_text_start or l_escaped_text_start = 0) + then + l_end := instr(a_source,chr(10),l_comment_start+2); + if l_end > 0 then + l_end := l_end + 1; + end if; + elsif l_text_start > 0 and (l_text_start < l_ml_comment_start or l_ml_comment_start = 0) + and (l_text_start < l_comment_start or l_comment_start = 0) and (l_text_start < l_escaped_text_start or l_escaped_text_start = 0) + then + l_end := instr(a_source,q'[']',l_text_start+1); + + --skip double quotes while searching for end of quoted text + while l_end > 0 and l_end = instr(a_source,q'['']',l_text_start+1) loop + l_end := instr(a_source,q'[']',l_end+1); + end loop; + if l_end > 0 then + l_end := l_end + 1; + end if; + + elsif l_escaped_text_start > 0 and (l_escaped_text_start < l_ml_comment_start or l_ml_comment_start = 0) + and (l_escaped_text_start < l_comment_start or l_comment_start = 0) and (l_escaped_text_start < l_text_start or l_text_start = 0) + then + --translate char "[" from the start of quoted text "q'[someting]'" into "]" + l_escaped_text_end_char := translate( substr(a_source, l_escaped_text_start + 2, 1), '[{(<', ']})>'); + l_end := instr(a_source,l_escaped_text_end_char||'''',l_escaped_text_start + 3 ); + if l_end > 0 then + l_end := l_end + 2; + end if; + end if; + + if l_end = 0 then + append_to_clob(l_result, substr(a_source, l_offset, l_length-l_offset)); + else + append_to_clob(l_result, substr(a_source, l_offset, l_end-l_offset)); + end if; + end if; + l_offset := l_end; + if l_offset >= l_ml_comment_start then + l_ml_comment_start := instr(a_source,'/*',l_offset); + end if; + if l_offset >= l_comment_start then + l_comment_start := instr(a_source,'--',l_offset); + end if; + if l_offset >= l_text_start then + l_text_start := instr(a_source,'''',l_offset); + end if; + if l_offset >= l_escaped_text_start then + l_escaped_text_start := instr(a_source,q'[q']',l_offset); + end if; + end loop; + append_to_clob(l_result, substr(a_source, l_end)); + return l_result; + end; + + function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info is + l_for_reporters ut_reporters_info := a_for_reporters; + l_results ut_reporters_info; + begin + if l_for_reporters is null then + l_for_reporters := ut_reporters_info(ut_reporter_info('UT_REPORTER_BASE','N','N','N')); + end if; + + select /*+ cardinality(f 10) */ + ut_reporter_info( + object_name => t.type_name, + is_output_reporter => + case + when f.is_output_reporter = 'Y' or t.type_name = 'UT_OUTPUT_REPORTER_BASE' + then 'Y' else 'N' + end, + is_instantiable => case when t.instantiable = 'YES' then 'Y' else 'N' end, + is_final => case when t.final = 'YES' then 'Y' else 'N' end + ) + bulk collect into l_results + from user_types t + join (select * from table(l_for_reporters) where is_final = 'N' ) f + on f.object_name = supertype_name; + + return l_results; + end; + + function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2 is + l_caller_stack_line varchar2(4000); + l_ora_search_pattern varchar2(500) := '^ORA'||a_ora_code||': (.*)$'; + begin + l_caller_stack_line := regexp_replace(srcstr => a_error_stack + ,pattern => l_ora_search_pattern + ,replacestr => null + ,position => 1 + ,occurrence => 1 + ,modifier => 'm'); + return l_caller_stack_line; + end; + + /** + * Change string into unicode to match xmlgen format _00_ + * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/adxdb/generation-of-XML-data-from-relational-data.html#GUID-5BE09A7D-80D8-4734-B9AF-4A61F27FA9B2 + * secion v3.1.7.2935-develop + */ + function char_to_xmlgen_unicode(a_character varchar2) return varchar2 is + begin + return '_x00'||rawtohex(utl_raw.cast_to_raw(a_character))||'_'; + end; + + /** + * Build valid XML column name as element names can contain letters, digits, hyphens, underscores, and periods + */ + function build_valid_xml_name(a_preprocessed_name varchar2) return varchar2 is + l_post_processed varchar2(4000); + begin + for i in (select regexp_substr( a_preprocessed_name ,'(.{1})', 1, level, null, 1 ) AS string_char,level level_no + from dual connect by level <= regexp_count(a_preprocessed_name, '(.{1})')) + loop + if i.level_no = 1 and regexp_like(i.string_char,gc_invalid_first_xml_char) then + l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); + elsif regexp_like(i.string_char,gc_invalid_xml_char) then + l_post_processed := l_post_processed || char_to_xmlgen_unicode(i.string_char); + else + l_post_processed := l_post_processed || i.string_char; + end if; + end loop; + return l_post_processed; + end; + + function get_valid_xml_name(a_name varchar2) return varchar2 is + l_valid_name varchar2(4000); + begin + if regexp_like(a_name,gc_full_valid_xml_name) then + l_valid_name := a_name; + else + l_valid_name := build_valid_xml_name(a_name); + end if; + return l_valid_name; + end; + + function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list is + l_result ut_varchar2_list := ut_varchar2_list(); + l_idx binary_integer; + begin + if a_prefix is not null then + l_idx := a_list.first; + while l_idx is not null loop + l_result.extend; + l_result(l_idx) := add_prefix(a_list(l_idx), a_prefix, a_connector); + l_idx := a_list.next(l_idx); + end loop; + end if; + return l_result; + end; + + function add_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2 is + begin + return a_prefix||a_connector||trim(leading a_connector from a_item); + end; + + function strip_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2 is + begin + return regexp_replace(a_item,a_prefix||a_connector); + end; + +end ut_utils; +/ diff --git a/source/core/ut_utils.pks b/source/core/ut_utils.pks index 3739819fb..03e553ef2 100644 --- a/source/core/ut_utils.pks +++ b/source/core/ut_utils.pks @@ -1,396 +1,400 @@ -create or replace package ut_utils authid definer is - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - /** - * Common utilities and constants used throughout utPLSQL framework - * - */ - - gc_version constant varchar2(50) := 'v3.1.7.2935-develop'; - - subtype t_executable_type is varchar2(30); - gc_before_all constant t_executable_type := 'beforeall'; - gc_before_each constant t_executable_type := 'beforeeach'; - gc_before_test constant t_executable_type := 'beforetest'; - gc_test_execute constant t_executable_type := 'test'; - gc_after_test constant t_executable_type := 'aftertest'; - gc_after_each constant t_executable_type := 'aftereach'; - gc_after_all constant t_executable_type := 'afterall'; - - /* Constants: Test Results */ - subtype t_test_result is binary_integer range 0 .. 3; - gc_disabled constant t_test_result := 0; -- test/suite was disabled - gc_success constant t_test_result := 1; -- test passed - gc_failure constant t_test_result := 2; -- one or more expectations failed - gc_error constant t_test_result := 3; -- exception was raised - - gc_disabled_char constant varchar2(8) := 'Disabled'; -- test/suite was disabled - gc_success_char constant varchar2(7) := 'Success'; -- test passed - gc_failure_char constant varchar2(7) := 'Failure'; -- one or more expectations failed - gc_error_char constant varchar2(5) := 'Error'; -- exception was raised - - /* - Constants: Rollback type for ut_test_object - */ - subtype t_rollback_type is binary_integer range 0 .. 1; - gc_rollback_auto constant t_rollback_type := 0; -- rollback after each test and suite - gc_rollback_manual constant t_rollback_type := 1; -- leave transaction control manual - gc_rollback_default constant t_rollback_type := gc_rollback_auto; - - ex_unsupported_rollback_type exception; - gc_unsupported_rollback_type constant pls_integer := -20200; - pragma exception_init(ex_unsupported_rollback_type, -20200); - - ex_path_list_is_empty exception; - gc_path_list_is_empty constant pls_integer := -20201; - pragma exception_init(ex_path_list_is_empty, -20201); - - ex_invalid_path_format exception; - gc_invalid_path_format constant pls_integer := -20202; - pragma exception_init(ex_invalid_path_format, -20202); - - ex_suite_package_not_found exception; - gc_suite_package_not_found constant pls_integer := -20204; - pragma exception_init(ex_suite_package_not_found, -20204); - - -- Reporting event time not supported - ex_invalid_rep_event_time exception; - gc_invalid_rep_event_time constant pls_integer := -20210; - pragma exception_init(ex_invalid_rep_event_time, -20210); - - -- Reporting event name not supported - ex_invalid_rep_event_name exception; - gc_invalid_rep_event_name constant pls_integer := -20211; - pragma exception_init(ex_invalid_rep_event_name, -20211); - - -- Any of tests failed - ex_some_tests_failed exception; - gc_some_tests_failed constant pls_integer := -20213; - pragma exception_init(ex_some_tests_failed, -20213); - - -- Version number provided is not in valid format - ex_invalid_version_no exception; - gc_invalid_version_no constant pls_integer := -20214; - pragma exception_init(ex_invalid_version_no, -20214); - - -- Version number provided is not in valid format - ex_out_buffer_timeout exception; - gc_out_buffer_timeout constant pls_integer := -20215; - pragma exception_init(ex_out_buffer_timeout, -20215); - - ex_invalid_package exception; - gc_invalid_package constant pls_integer := -6550; - pragma exception_init(ex_invalid_package, -6550); - - ex_failure_for_all exception; - gc_failure_for_all constant pls_integer := -24381; - pragma exception_init (ex_failure_for_all, -24381); - - ex_dml_for_all exception; - gc_dml_for_all constant pls_integer := -20216; - pragma exception_init (ex_dml_for_all, -20216); - - ex_value_too_large exception; - gc_value_too_large constant pls_integer := -20217; - pragma exception_init (ex_value_too_large, -20217); - - ex_xml_processing exception; - gc_xml_processing constant pls_integer := -19202; - pragma exception_init (ex_xml_processing, -19202); - - ex_failed_open_cur exception; - gc_failed_open_cur constant pls_integer := -20218; - pragma exception_init (ex_failed_open_cur, -20218); - - gc_max_storage_varchar2_len constant integer := 4000; - gc_max_output_string_length constant integer := 4000; - gc_more_data_string constant varchar2(5) := '[...]'; - gc_more_data_string_len constant integer := length( gc_more_data_string ); - gc_number_format constant varchar2(100) := 'TM9'; - gc_date_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ss'; - gc_timestamp_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff'; - gc_timestamp_tz_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff tzh:tzm'; - gc_null_string constant varchar2(4) := 'NULL'; - gc_empty_string constant varchar2(5) := 'EMPTY'; - - gc_bc_fetch_limit constant integer := 1000; - gc_diff_max_rows constant integer := 20; - - type t_version is record( - major natural, - minor natural, - bugfix natural, - build natural - ); - - type t_clob_tab is table of clob; - - /** - * Converts test results into strings - * - * @param a_test_result numeric representation of test result - * - * @return a string representation of a test_result. - */ - function test_result_to_char(a_test_result integer) return varchar2; - - function to_test_result(a_test boolean) return integer; - - /** - * Generates a unique name for a savepoint - * Uses sys_guid, as timestamp gives only miliseconds on Windows and is not unique - * Issue: #506 for details on the implementation approach - */ - function gen_savepoint_name return varchar2; - - procedure debug_log(a_message varchar2); - - procedure debug_log(a_message clob); - - function to_string( - a_value varchar2, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2; - - function to_string( - a_value clob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2; - - function to_string( - a_value blob, - a_quote_char varchar2 := '''', - a_max_output_len in number := gc_max_output_string_length - ) return varchar2; - - function to_string(a_value boolean) return varchar2; - - function to_string(a_value number) return varchar2; - - function to_string(a_value date) return varchar2; - - function to_string(a_value timestamp_unconstrained) return varchar2; - - function to_string(a_value timestamp_tz_unconstrained) return varchar2; - - function to_string(a_value timestamp_ltz_unconstrained) return varchar2; - - function to_string(a_value yminterval_unconstrained) return varchar2; - - function to_string(a_value dsinterval_unconstrained) return varchar2; - - function boolean_to_int(a_value boolean) return integer; - - function int_to_boolean(a_value integer) return boolean; - - /** - * - * Splits a given string into table of string by delimiter. - * The delimiter gets removed. - * If null passed as any of the parameters, empty table is returned. - * If no occurence of a_delimiter found in a_text then text is returned as a single row of the table. - * If no text between delimiters found then an empty row is returned, example: - * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); - * - * @param a_string the text to be split. - * @param a_delimiter the delimiter character or string - * @param a_skip_leading_delimiter determines if the leading delimiter should be ignored, used by clob_to_table - * - * @return table of varchar2 values - */ - function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list; - - /** - * Splits a given string into table of string by delimiter. - * Default value of a_max_amount is 8191 because of code can contains multibyte character. - * The delimiter gets removed. - * If null passed as any of the parameters, empty table is returned. - * If split text is longer than a_max_amount it gets split into pieces of a_max_amount. - * If no text between delimiters found then an empty row is returned, example: - * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); - * - * @param a_clob the text to be split. - * @param a_delimiter the delimiter character or string (default chr(10) ) - * @param a_max_amount the maximum length of returned string (default 8191) - * @return table of varchar2 values - */ - function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list; - - function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob; - - function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob; - - /** - * Returns time difference in seconds (with miliseconds) between given timestamps - */ - function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number; - - /** - * Returns a text indented with spaces except the first line. - */ - function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2; - - - /** - * Returns a list of object that are part of utPLSQL framework - */ - function get_utplsql_objects_list return ut_object_names; - - /** - * Append a item to the end of ut_varchar2_list - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2); - - /** - * Append a item to the end of ut_varchar2_rows - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2); - - /** - * Append a item to the end of ut_varchar2_rows - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob); - - /** - * Append a list of items to the end of ut_varchar2_rows - */ - procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows); - - procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2 := chr(10)); - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob); - - procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2); - - function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows; - - /** - * Set session's action and module using dbms_application_info - */ - procedure set_action(a_text in varchar2); - - /** - * Set session's client info using dbms_application_info - */ - procedure set_client_info(a_text in varchar2); - - function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2; - - function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2; - - procedure cleanup_temp_tables; - - /** - * Converts version string into version record - * - * @param a_version_no string representation of version in format vX.X.X.X where X is a positive integer - * @return t_version record with up to four positive numbers containing version - * @throws 20214 if passed version string is not matching version pattern - */ - function to_version(a_version_no varchar2) return t_version; - - - /** - * Saves data from dbms_output buffer into a global temporary table (cache) - * used to store dbms_output buffer captured before the run - * - */ - procedure save_dbms_output_to_cache; - - /** - * Reads data from global temporary table (cache) abd puts it back into dbms_output - * used to recover dbms_output buffer data after a run is complete - * - */ - procedure read_cache_to_dbms_output; - - - /** - * Function is used to reference to utPLSQL owned objects in dynamic sql statements executed from packages with invoker rights - * - * @return the name of the utPSQL schema owner - */ - function ut_owner return varchar2; - - - /** - * Used in dynamic sql select statements to maintain balance between - * number of hard-parses and optimiser accurancy for cardinality of collections - * - * - * @return 3, for inputs of: 1-9; 33 for input of 10 - 99; 333 for (100 - 999) - */ - function scale_cardinality(a_cardinality natural) return natural; - - function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2; - - /** - * Returns number as string. The value is represented as decimal according to XML standard: - * https://www.w3.org/TR/xmlschema-2/#decimal - */ - function to_xml_number_format(a_value number) return varchar2; - - - /** - * Returns xml header. If a_encoding is not null, header will include encoding attribute with provided value - */ - function get_xml_header(a_encoding varchar2) return varchar2; - - - /** - * Takes a collection of type ut_varchar2_list and it trims the characters passed as arguments for every element - */ - function trim_list_elements(a_list IN ut_varchar2_list, a_regexp_to_trim in varchar2 default '[:space:]') return ut_varchar2_list; - - /** - * Takes a collection of type ut_varchar2_list and it only returns the elements which meets the regular expression - */ - function filter_list(a_list IN ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list; - - -- Generates XMLGEN escaped string - function xmlgen_escaped_string(a_string in varchar2) return varchar2; - - /** - * Replaces multi-line comments in given source-code with empty lines - */ - function replace_multiline_comments(a_source clob) return clob; - - /** - * Returns list of sub-type reporters for given list of super-type reporters - */ - function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info; - - /** - * Remove given ORA error from stack - */ - function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2; - - /** - * Check if xml name is valid if not build a valid name - */ - function get_valid_xml_name(a_name varchar2) return varchar2; - - /** - * Add prefix word to elements of list - */ - function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list; - -end ut_utils; -/ +create or replace package ut_utils authid definer is + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + /** + * Common utilities and constants used throughout utPLSQL framework + * + */ + + gc_version constant varchar2(50) := 'v3.1.7.2935-develop'; + + subtype t_executable_type is varchar2(30); + gc_before_all constant t_executable_type := 'beforeall'; + gc_before_each constant t_executable_type := 'beforeeach'; + gc_before_test constant t_executable_type := 'beforetest'; + gc_test_execute constant t_executable_type := 'test'; + gc_after_test constant t_executable_type := 'aftertest'; + gc_after_each constant t_executable_type := 'aftereach'; + gc_after_all constant t_executable_type := 'afterall'; + + /* Constants: Test Results */ + subtype t_test_result is binary_integer range 0 .. 3; + gc_disabled constant t_test_result := 0; -- test/suite was disabled + gc_success constant t_test_result := 1; -- test passed + gc_failure constant t_test_result := 2; -- one or more expectations failed + gc_error constant t_test_result := 3; -- exception was raised + + gc_disabled_char constant varchar2(8) := 'Disabled'; -- test/suite was disabled + gc_success_char constant varchar2(7) := 'Success'; -- test passed + gc_failure_char constant varchar2(7) := 'Failure'; -- one or more expectations failed + gc_error_char constant varchar2(5) := 'Error'; -- exception was raised + + /* + Constants: Rollback type for ut_test_object + */ + subtype t_rollback_type is binary_integer range 0 .. 1; + gc_rollback_auto constant t_rollback_type := 0; -- rollback after each test and suite + gc_rollback_manual constant t_rollback_type := 1; -- leave transaction control manual + gc_rollback_default constant t_rollback_type := gc_rollback_auto; + + ex_unsupported_rollback_type exception; + gc_unsupported_rollback_type constant pls_integer := -20200; + pragma exception_init(ex_unsupported_rollback_type, -20200); + + ex_path_list_is_empty exception; + gc_path_list_is_empty constant pls_integer := -20201; + pragma exception_init(ex_path_list_is_empty, -20201); + + ex_invalid_path_format exception; + gc_invalid_path_format constant pls_integer := -20202; + pragma exception_init(ex_invalid_path_format, -20202); + + ex_suite_package_not_found exception; + gc_suite_package_not_found constant pls_integer := -20204; + pragma exception_init(ex_suite_package_not_found, -20204); + + -- Reporting event time not supported + ex_invalid_rep_event_time exception; + gc_invalid_rep_event_time constant pls_integer := -20210; + pragma exception_init(ex_invalid_rep_event_time, -20210); + + -- Reporting event name not supported + ex_invalid_rep_event_name exception; + gc_invalid_rep_event_name constant pls_integer := -20211; + pragma exception_init(ex_invalid_rep_event_name, -20211); + + -- Any of tests failed + ex_some_tests_failed exception; + gc_some_tests_failed constant pls_integer := -20213; + pragma exception_init(ex_some_tests_failed, -20213); + + -- Version number provided is not in valid format + ex_invalid_version_no exception; + gc_invalid_version_no constant pls_integer := -20214; + pragma exception_init(ex_invalid_version_no, -20214); + + -- Version number provided is not in valid format + ex_out_buffer_timeout exception; + gc_out_buffer_timeout constant pls_integer := -20215; + pragma exception_init(ex_out_buffer_timeout, -20215); + + ex_invalid_package exception; + gc_invalid_package constant pls_integer := -6550; + pragma exception_init(ex_invalid_package, -6550); + + ex_failure_for_all exception; + gc_failure_for_all constant pls_integer := -24381; + pragma exception_init (ex_failure_for_all, -24381); + + ex_dml_for_all exception; + gc_dml_for_all constant pls_integer := -20216; + pragma exception_init (ex_dml_for_all, -20216); + + ex_value_too_large exception; + gc_value_too_large constant pls_integer := -20217; + pragma exception_init (ex_value_too_large, -20217); + + ex_xml_processing exception; + gc_xml_processing constant pls_integer := -19202; + pragma exception_init (ex_xml_processing, -19202); + + ex_failed_open_cur exception; + gc_failed_open_cur constant pls_integer := -20218; + pragma exception_init (ex_failed_open_cur, -20218); + + gc_max_storage_varchar2_len constant integer := 4000; + gc_max_output_string_length constant integer := 4000; + gc_more_data_string constant varchar2(5) := '[...]'; + gc_more_data_string_len constant integer := length( gc_more_data_string ); + gc_number_format constant varchar2(100) := 'TM9'; + gc_date_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ss'; + gc_timestamp_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff'; + gc_timestamp_tz_format constant varchar2(100) := 'yyyy-mm-dd"T"hh24:mi:ssxff tzh:tzm'; + gc_null_string constant varchar2(4) := 'NULL'; + gc_empty_string constant varchar2(5) := 'EMPTY'; + + gc_bc_fetch_limit constant integer := 1000; + gc_diff_max_rows constant integer := 20; + + type t_version is record( + major natural, + minor natural, + bugfix natural, + build natural + ); + + type t_clob_tab is table of clob; + + /** + * Converts test results into strings + * + * @param a_test_result numeric representation of test result + * + * @return a string representation of a test_result. + */ + function test_result_to_char(a_test_result integer) return varchar2; + + function to_test_result(a_test boolean) return integer; + + /** + * Generates a unique name for a savepoint + * Uses sys_guid, as timestamp gives only miliseconds on Windows and is not unique + * Issue: #506 for details on the implementation approach + */ + function gen_savepoint_name return varchar2; + + procedure debug_log(a_message varchar2); + + procedure debug_log(a_message clob); + + function to_string( + a_value varchar2, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2; + + function to_string( + a_value clob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2; + + function to_string( + a_value blob, + a_quote_char varchar2 := '''', + a_max_output_len in number := gc_max_output_string_length + ) return varchar2; + + function to_string(a_value boolean) return varchar2; + + function to_string(a_value number) return varchar2; + + function to_string(a_value date) return varchar2; + + function to_string(a_value timestamp_unconstrained) return varchar2; + + function to_string(a_value timestamp_tz_unconstrained) return varchar2; + + function to_string(a_value timestamp_ltz_unconstrained) return varchar2; + + function to_string(a_value yminterval_unconstrained) return varchar2; + + function to_string(a_value dsinterval_unconstrained) return varchar2; + + function boolean_to_int(a_value boolean) return integer; + + function int_to_boolean(a_value integer) return boolean; + + /** + * + * Splits a given string into table of string by delimiter. + * The delimiter gets removed. + * If null passed as any of the parameters, empty table is returned. + * If no occurence of a_delimiter found in a_text then text is returned as a single row of the table. + * If no text between delimiters found then an empty row is returned, example: + * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); + * + * @param a_string the text to be split. + * @param a_delimiter the delimiter character or string + * @param a_skip_leading_delimiter determines if the leading delimiter should be ignored, used by clob_to_table + * + * @return table of varchar2 values + */ + function string_to_table(a_string varchar2, a_delimiter varchar2:= chr(10), a_skip_leading_delimiter varchar2 := 'N') return ut_varchar2_list; + + /** + * Splits a given string into table of string by delimiter. + * Default value of a_max_amount is 8191 because of code can contains multibyte character. + * The delimiter gets removed. + * If null passed as any of the parameters, empty table is returned. + * If split text is longer than a_max_amount it gets split into pieces of a_max_amount. + * If no text between delimiters found then an empty row is returned, example: + * string_to_table( 'a,,b', ',' ) gives table ut_varchar2_list( 'a', null, 'b' ); + * + * @param a_clob the text to be split. + * @param a_delimiter the delimiter character or string (default chr(10) ) + * @param a_max_amount the maximum length of returned string (default 8191) + * @return table of varchar2 values + */ + function clob_to_table(a_clob clob, a_max_amount integer := 8191, a_delimiter varchar2:= chr(10)) return ut_varchar2_list; + + function table_to_clob(a_text_table ut_varchar2_list, a_delimiter varchar2:= chr(10)) return clob; + + function table_to_clob(a_integer_table ut_integer_list, a_delimiter varchar2:= chr(10)) return clob; + + /** + * Returns time difference in seconds (with miliseconds) between given timestamps + */ + function time_diff(a_start_time timestamp with time zone, a_end_time timestamp with time zone) return number; + + /** + * Returns a text indented with spaces except the first line. + */ + function indent_lines(a_text varchar2, a_indent_size integer := 4, a_include_first_line boolean := false) return varchar2; + + + /** + * Returns a list of object that are part of utPLSQL framework + */ + function get_utplsql_objects_list return ut_object_names; + + /** + * Append a item to the end of ut_varchar2_list + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_list, a_item varchar2); + + /** + * Append a item to the end of ut_varchar2_rows + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item varchar2); + + /** + * Append a item to the end of ut_varchar2_rows + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_item clob); + + /** + * Append a list of items to the end of ut_varchar2_rows + */ + procedure append_to_list(a_list in out nocopy ut_varchar2_rows, a_items ut_varchar2_rows); + + procedure append_to_clob(a_src_clob in out nocopy clob, a_clob_table t_clob_tab, a_delimiter varchar2 := chr(10)); + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data clob); + + procedure append_to_clob(a_src_clob in out nocopy clob, a_new_data varchar2); + + function convert_collection(a_collection ut_varchar2_list) return ut_varchar2_rows; + + /** + * Set session's action and module using dbms_application_info + */ + procedure set_action(a_text in varchar2); + + /** + * Set session's client info using dbms_application_info + */ + procedure set_client_info(a_text in varchar2); + + function to_xpath(a_list varchar2, a_ancestors varchar2 := '/*/') return varchar2; + + function to_xpath(a_list ut_varchar2_list, a_ancestors varchar2 := '/*/') return varchar2; + + procedure cleanup_temp_tables; + + /** + * Converts version string into version record + * + * @param a_version_no string representation of version in format vX.X.X.X where X is a positive integer + * @return t_version record with up to four positive numbers containing version + * @throws 20214 if passed version string is not matching version pattern + */ + function to_version(a_version_no varchar2) return t_version; + + + /** + * Saves data from dbms_output buffer into a global temporary table (cache) + * used to store dbms_output buffer captured before the run + * + */ + procedure save_dbms_output_to_cache; + + /** + * Reads data from global temporary table (cache) abd puts it back into dbms_output + * used to recover dbms_output buffer data after a run is complete + * + */ + procedure read_cache_to_dbms_output; + + + /** + * Function is used to reference to utPLSQL owned objects in dynamic sql statements executed from packages with invoker rights + * + * @return the name of the utPSQL schema owner + */ + function ut_owner return varchar2; + + + /** + * Used in dynamic sql select statements to maintain balance between + * number of hard-parses and optimiser accurancy for cardinality of collections + * + * + * @return 3, for inputs of: 1-9; 33 for input of 10 - 99; 333 for (100 - 999) + */ + function scale_cardinality(a_cardinality natural) return natural; + + function build_depreciation_warning(a_old_syntax varchar2, a_new_syntax varchar2) return varchar2; + + /** + * Returns number as string. The value is represented as decimal according to XML standard: + * https://www.w3.org/TR/xmlschema-2/#decimal + */ + function to_xml_number_format(a_value number) return varchar2; + + + /** + * Returns xml header. If a_encoding is not null, header will include encoding attribute with provided value + */ + function get_xml_header(a_encoding varchar2) return varchar2; + + + /** + * Takes a collection of type ut_varchar2_list and it trims the characters passed as arguments for every element + */ + function trim_list_elements(a_list IN ut_varchar2_list, a_regexp_to_trim in varchar2 default '[:space:]') return ut_varchar2_list; + + /** + * Takes a collection of type ut_varchar2_list and it only returns the elements which meets the regular expression + */ + function filter_list(a_list IN ut_varchar2_list, a_regexp_filter in varchar2) return ut_varchar2_list; + + -- Generates XMLGEN escaped string + function xmlgen_escaped_string(a_string in varchar2) return varchar2; + + /** + * Replaces multi-line comments in given source-code with empty lines + */ + function replace_multiline_comments(a_source clob) return clob; + + /** + * Returns list of sub-type reporters for given list of super-type reporters + */ + function get_child_reporters(a_for_reporters ut_reporters_info := null) return ut_reporters_info; + + /** + * Remove given ORA error from stack + */ + function remove_error_from_stack(a_error_stack varchar2, a_ora_code number) return varchar2; + + /** + * Check if xml name is valid if not build a valid name + */ + function get_valid_xml_name(a_name varchar2) return varchar2; + + /** + * Add prefix word to elements of list + */ + function add_prefix(a_list ut_varchar2_list, a_prefix varchar2, a_connector varchar2 := '/') return ut_varchar2_list; + + function add_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2; + + function strip_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2; + +end ut_utils; +/ diff --git a/source/expectations/data_values/ut_compound_data_helper.pkb b/source/expectations/data_values/ut_compound_data_helper.pkb index fa736ecfb..759e3ad70 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pkb +++ b/source/expectations/data_values/ut_compound_data_helper.pkb @@ -1,694 +1,694 @@ -create or replace package body ut_compound_data_helper is - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - g_diff_count integer; - type t_type_name_map is table of varchar2(128) index by binary_integer; - type t_types_no_length is table of varchar2(128) index by varchar2(128); - g_type_name_map t_type_name_map; - g_anytype_name_map t_type_name_map; - g_type_no_length_map t_types_no_length; - - g_compare_sql_template varchar2(4000) := - q'[ - with exp as ( - select - ucd.*, - {:duplicate_number:} dup_no - from ( - select - ucd.item_data - ,x.data_id data_id - ,position + x.item_no item_no - {:columns:} - from {:ut3_owner:}.ut_compound_data_tmp x, - xmltable('/ROWSET/ROW' passing x.item_data columns - item_data xmltype path '*' - ,position for ordinality - {:xml_to_columns:} ) ucd - where data_id = :exp_guid - ) ucd - ) - , act as ( - select - ucd.*, - {:duplicate_number:} dup_no - from ( - select - ucd.item_data - ,x.data_id data_id - ,position + x.item_no item_no - {:columns:} - from {:ut3_owner:}.ut_compound_data_tmp x, - xmltable('/ROWSET/ROW' passing x.item_data columns - item_data xmltype path '*' - ,position for ordinality - {:xml_to_columns:} ) ucd - where data_id = :act_guid - ) ucd - ) - select - a.item_data as act_item_data, - a.data_id act_data_id, - e.item_data as exp_item_data, - e.data_id exp_data_id, - {:item_no:} as item_no, - nvl(e.dup_no,a.dup_no) dup_no - from act a {:join_type:} exp e on ( {:join_condition:} ) - where {:where_condition:}]'; - - function get_columns_diff( - a_expected ut_cursor_column_tab, - a_actual ut_cursor_column_tab, - a_order_enforced boolean := false - ) return tt_column_diffs is - l_results tt_column_diffs; - begin - execute immediate q'[with - expected_cols as ( - select display_path exp_column_name,column_position exp_col_pos, - replace(column_type_name,'VARCHAR2','CHAR') exp_col_type_compare, column_type_name exp_col_type - from table(:a_expected) - where parent_name is null and hierarchy_level = 1 and column_name is not null - ), - actual_cols as ( - select display_path act_column_name,column_position act_col_pos, - replace(column_type_name,'VARCHAR2','CHAR') act_col_type_compare, column_type_name act_col_type - from table(:a_actual) - where parent_name is null and hierarchy_level = 1 and column_name is not null - ), - joined_cols as ( - select e.*,a.*]' - || case when a_order_enforced then ', - row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by a.act_col_pos) a_pos_nn, - row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by e.exp_col_pos) e_pos_nn' - else - null - end ||q'[ - from expected_cols e - full outer join actual_cols a - on e.exp_column_name = a.act_column_name - ) - select case - when exp_col_pos is null and act_col_pos is not null then '+' - when exp_col_pos is not null and act_col_pos is null then '-' - when exp_col_type_compare != act_col_type_compare then 't' - else 'p' - end as diff_type, - exp_column_name, exp_col_type, exp_col_pos, - act_column_name, act_col_type, act_col_pos - from joined_cols - --column is unexpected (extra) or missing - where act_col_pos is null or exp_col_pos is null - --column type is not matching (except CHAR/VARCHAR2) - or act_col_type_compare != exp_col_type_compare]' - || case when a_order_enforced then q'[ - --column position is not matching (both when excluded extra/missing columns as well as when they are included) - or (a_pos_nn != e_pos_nn and exp_col_pos != act_col_pos)]' - else - null - end ||q'[ - order by exp_col_pos, act_col_pos]' - bulk collect into l_results using a_expected, a_actual; - return l_results; - end; - - function generate_not_equal_stmt( - a_data_info ut_cursor_column, a_pk_table ut_varchar2_list - ) return varchar2 - is - l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); - l_index integer; - l_sql_stmt varchar2(32767); - l_exists boolean := false; - begin - l_index := l_pk_tab.first; - if l_pk_tab.count > 0 then - loop - if a_data_info.access_path = l_pk_tab(l_index) then - l_exists := true; - end if; - exit when l_index = l_pk_tab.count or (a_data_info.access_path = l_pk_tab(l_index)); - l_index := a_pk_table.next(l_index); - end loop; - end if; - if not(l_exists) then - l_sql_stmt := ' (decode(a.'||a_data_info.transformed_name||','||' e.'||a_data_info.transformed_name||',1,0) = 0)'; - end if; - return l_sql_stmt; - end; - - function generate_join_by_stmt( - a_data_info ut_cursor_column, a_pk_table ut_varchar2_list - ) return varchar2 - is - l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); - l_index integer; - l_sql_stmt varchar2(32767); - begin - if l_pk_tab.count <> 0 then - l_index:= l_pk_tab.first; - loop - if l_pk_tab(l_index) in (a_data_info.access_path, a_data_info.parent_name) then - --When then table is nested and join is on whole table - l_sql_stmt := l_sql_stmt ||' a.'||a_data_info.transformed_name||q'[ = ]'||' e.'||a_data_info.transformed_name; - end if; - exit when (a_data_info.access_path = l_pk_tab(l_index)) or l_index = l_pk_tab.count; - l_index := l_pk_tab.next(l_index); - end loop; - end if; - return l_sql_stmt; - end; - - function generate_equal_sql(a_col_name in varchar2) return varchar2 is - begin - return ' decode(a.'||a_col_name||','||' e.'||a_col_name||',1,0) = 1 '; - end; - - function generate_partition_stmt( - a_data_info ut_cursor_column, a_pk_table in ut_varchar2_list, a_alias varchar2 := 'ucd.' - ) return varchar2 - is - l_index integer; - l_sql_stmt varchar2(32767); - begin - if a_pk_table is not empty then - l_index:= a_pk_table.first; - loop - if a_pk_table(l_index) in (a_data_info.access_path, a_data_info.parent_name) then - --When then table is nested and join is on whole table - l_sql_stmt := l_sql_stmt ||a_alias||a_data_info.transformed_name; - end if; - exit when (a_data_info.access_path = a_pk_table(l_index)) or l_index = a_pk_table.count; - l_index := a_pk_table.next(l_index); - end loop; - else - l_sql_stmt := a_alias||a_data_info.transformed_name; - end if; - return l_sql_stmt; - end; - - function generate_select_stmt(a_data_info ut_cursor_column, a_alias varchar2 := 'ucd.') - return varchar2 - is - l_alias varchar2(10) := a_alias; - l_col_syntax varchar2(4000); - l_ut_owner varchar2(250) := ut_utils.ut_owner; - begin - if a_data_info.is_sql_diffable = 0 then - l_col_syntax := l_ut_owner ||'.ut_compound_data_helper.get_hash('||l_alias||a_data_info.transformed_name||'.getClobVal()) as '||a_data_info.transformed_name ; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type = 'DATE' then - l_col_syntax := 'to_date('||l_alias||a_data_info.transformed_name||') as '|| a_data_info.transformed_name; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP') then - l_col_syntax := 'to_timestamp('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_format||''') as '|| a_data_info.transformed_name; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH TIME ZONE') then - l_col_syntax := 'to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') as '|| a_data_info.transformed_name; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH LOCAL TIME ZONE') then - l_col_syntax := ' cast( to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') AS TIMESTAMP WITH LOCAL TIME ZONE) as '|| a_data_info.transformed_name; - else - l_col_syntax := l_alias||a_data_info.transformed_name||' as '|| a_data_info.transformed_name; - end if; - return l_col_syntax; - end; - - function generate_xmltab_stmt(a_data_info ut_cursor_column) return varchar2 is - l_col_type varchar2(4000); - begin - if a_data_info.is_sql_diffable = 0 then - l_col_type := 'XMLTYPE'; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('DATE','TIMESTAMP','TIMESTAMP WITH TIME ZONE', - 'TIMESTAMP WITH LOCAL TIME ZONE') then - l_col_type := 'VARCHAR2(50)'; - elsif a_data_info.is_sql_diffable = 1 and type_no_length(a_data_info.column_type) then - l_col_type := a_data_info.column_type; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('VARCHAR2','CHAR') then - l_col_type := 'VARCHAR2('||greatest(a_data_info.column_len,4000)||')'; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('NUMBER') then - --We cannot use a precision and scale as dbms_sql.describe_columns3 return precision 0 for dual table - -- there is also no need for that as we not process data but only read and compare as they are stored - l_col_type := a_data_info.column_type; - else - l_col_type := a_data_info.column_type - ||case when a_data_info.column_len is not null - then '('||a_data_info.column_len||')' - else null - end; - end if; - return a_data_info.transformed_name||' '||l_col_type||q'[ PATH ']'||a_data_info.access_path||q'[']'; - end; - - procedure gen_sql_pieces_out_of_cursor( - a_data_info ut_cursor_column_tab, - a_pk_table ut_varchar2_list, - a_unordered boolean, - a_xml_stmt out nocopy clob, - a_select_stmt out nocopy clob, - a_partition_stmt out nocopy clob, - a_join_by_stmt out nocopy clob, - a_not_equal_stmt out nocopy clob - ) is - l_partition_tmp clob; - l_xmltab_list ut_varchar2_list := ut_varchar2_list(); - l_select_list ut_varchar2_list := ut_varchar2_list(); - l_partition_list ut_varchar2_list := ut_varchar2_list(); - l_equal_list ut_varchar2_list := ut_varchar2_list(); - l_join_by_list ut_varchar2_list := ut_varchar2_list(); - l_not_equal_list ut_varchar2_list := ut_varchar2_list(); - - procedure add_element_to_list(a_list in out ut_varchar2_list, a_list_element in varchar2) - is - begin - if a_list_element is not null then - a_list.extend; - a_list(a_list.last) := a_list_element; - end if; - end; - - begin - if a_data_info is not empty then - for i in 1..a_data_info.count loop - if a_data_info(i).has_nested_col = 0 then - --Get XMLTABLE column list - add_element_to_list(l_xmltab_list,generate_xmltab_stmt(a_data_info(i))); - --Get Select statment list of columns - add_element_to_list(l_select_list, generate_select_stmt(a_data_info(i))); - --Get columns by which we partition - add_element_to_list(l_partition_list,generate_partition_stmt(a_data_info(i), a_pk_table)); - --Get equal statement - add_element_to_list(l_equal_list,generate_equal_sql(a_data_info(i).transformed_name)); - --Generate join by stmt - add_element_to_list(l_join_by_list,generate_join_by_stmt(a_data_info(i), a_pk_table)); - --Generate not equal stmt - add_element_to_list(l_not_equal_list,generate_not_equal_stmt(a_data_info(i), a_pk_table)); - end if; - end loop; - - a_xml_stmt := nullif(','||ut_utils.table_to_clob(l_xmltab_list, ' , '),','); - a_select_stmt := nullif(','||ut_utils.table_to_clob(l_select_list, ' , '),','); - l_partition_tmp := ut_utils.table_to_clob(l_partition_list, ' , '); - ut_utils.append_to_clob(a_partition_stmt,' row_number() over (partition by '||l_partition_tmp||' order by '||l_partition_tmp||' ) '); - - if a_pk_table.count > 0 then - -- If key defined do the join or these and where on diffrences - a_join_by_stmt := ut_utils.table_to_clob(l_join_by_list, ' and '); - elsif a_unordered then - -- If no key defined do the join on all columns - a_join_by_stmt := ' e.dup_no = a.dup_no and '||ut_utils.table_to_clob(l_equal_list, ' and '); - else - -- Else join on rownumber - a_join_by_stmt := 'a.item_no = e.item_no '; - end if; - a_not_equal_stmt := ut_utils.table_to_clob(l_not_equal_list, ' or '); - else - --Partition by piece when no data - ut_utils.append_to_clob(a_partition_stmt,' 1 '); - a_join_by_stmt := 'a.item_no = e.item_no '; - end if; - end; - - function gen_compare_sql( - a_other ut_data_value_refcursor, - a_join_by_list ut_varchar2_list, - a_unordered boolean, - a_inclusion_type boolean, - a_is_negated boolean - ) return clob is - l_compare_sql clob; - l_xmltable_stmt clob; - l_select_stmt clob; - l_partition_stmt clob; - l_join_on_stmt clob; - l_not_equal_stmt clob; - l_where_stmt clob; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_join_by_list ut_varchar2_list; - - function get_join_type(a_inclusion_compare in boolean,a_negated in boolean) return varchar2 is - begin - return - case - when a_inclusion_compare and not(a_negated) then ' right outer join ' - when a_inclusion_compare and a_negated then ' inner join ' - else ' full outer join ' - end; - end; - - function get_item_no(a_unordered boolean) return varchar2 is - begin - return - case - when a_unordered then 'row_number() over ( order by nvl(e.item_no,a.item_no))' - else 'nvl(e.item_no,a.item_no) ' - end; - end; - - begin - /** - * We already estabilished cursor equality so now we add anydata root if we compare anydata - * to join by. - */ - l_join_by_list := - case - when a_other is of (ut_data_value_anydata) then ut_utils.add_prefix(a_join_by_list, a_other.cursor_details.get_root) - else a_join_by_list - end; - - dbms_lob.createtemporary(l_compare_sql, true); - --Initiate a SQL template with placeholders - ut_utils.append_to_clob(l_compare_sql, g_compare_sql_template); - --Generate a pieceso of dynamic SQL that will substitute placeholders - gen_sql_pieces_out_of_cursor( - a_other.cursor_details.cursor_columns_info, l_join_by_list, a_unordered, - l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_join_on_stmt, - l_not_equal_stmt - ); - - l_compare_sql := replace(l_compare_sql,'{:duplicate_number:}',l_partition_stmt); - l_compare_sql := replace(l_compare_sql,'{:columns:}',l_select_stmt); - l_compare_sql := replace(l_compare_sql,'{:ut3_owner:}',l_ut_owner); - l_compare_sql := replace(l_compare_sql,'{:xml_to_columns:}',l_xmltable_stmt); - l_compare_sql := replace(l_compare_sql,'{:item_no:}',get_item_no(a_unordered)); - l_compare_sql := replace(l_compare_sql,'{:join_type:}',get_join_type(a_inclusion_type,a_is_negated)); - l_compare_sql := replace(l_compare_sql,'{:join_condition:}',l_join_on_stmt); - - if l_not_equal_stmt is not null and ((l_join_by_list.count > 0 and not a_is_negated) or (not a_unordered)) then - ut_utils.append_to_clob(l_where_stmt,' ( '||l_not_equal_stmt||' ) or '); - end if; - --If its inclusion we expect a actual set to fully match and have no extra elements over expected - if a_inclusion_type then - ut_utils.append_to_clob(l_where_stmt,case when a_is_negated then ' 1 = 1 ' else ' ( a.data_id is null ) ' end); - else - ut_utils.append_to_clob(l_where_stmt,' (a.data_id is null or e.data_id is null) '); - end if; - - l_compare_sql := replace(l_compare_sql,'{:where_condition:}',l_where_stmt); - return l_compare_sql; - end; - - function get_column_extract_path(a_cursor_info ut_cursor_column_tab) return ut_varchar2_list is - l_column_list ut_varchar2_list := ut_varchar2_list(); - begin - for i in 1..a_cursor_info.count loop - l_column_list.extend; - l_column_list(l_column_list.last) := a_cursor_info(i).access_path; - end loop; - return l_column_list; - end; - - function get_rows_diff_by_sql( - a_act_cursor_info ut_cursor_column_tab, a_exp_cursor_info ut_cursor_column_tab, - a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, - a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false, - a_extract_path varchar2 - ) return tt_row_diffs is - l_act_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_act_cursor_info)); - l_exp_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_exp_cursor_info)); - l_join_xpath varchar2(32767) := ut_utils.to_xpath(a_join_by_list); - l_results tt_row_diffs; - l_sql varchar2(32767); - begin - l_sql := q'[ - with exp as ( - select - exp_item_data, exp_data_id, item_no rn, rownum col_no, pk_value, - s.column_value col, s.column_value.getRootElement() col_name, - nvl(s.column_value.getclobval(),empty_clob()) col_val - from ( - select - exp_data_id, extract( ucd.exp_item_data, :column_path ) exp_item_data, item_no, - replace( extract( ucd.exp_item_data, :join_by ).getclobval(), chr(10) ) pk_value - from ut_compound_data_diff_tmp ucd - where diff_id = :diff_id - and ucd.exp_data_id = :self_guid - ) i, - table( xmlsequence( extract(i.exp_item_data,:extract_path) ) ) s - ), - act as ( - select - act_item_data, act_data_id, item_no rn, rownum col_no, pk_value, - s.column_value col, s.column_value.getRootElement() col_name, - nvl(s.column_value.getclobval(),empty_clob()) col_val - from ( - select - act_data_id, extract( ucd.act_item_data, :column_path ) act_item_data, item_no, - replace( extract( ucd.act_item_data, :join_by ).getclobval(), chr(10) ) pk_value - from ut_compound_data_diff_tmp ucd - where diff_id = :diff_id - and ucd.act_data_id = :other_guid - ) i, - table( xmlsequence( extract(i.act_item_data,:extract_path) ) ) s - ) - select rn, diff_type, diffed_row, pk_value pk_value - from ( - select rn, diff_type, diffed_row, pk_value, - case when diff_type = 'Actual:' then 1 else 2 end rnk, - 1 final_order, - col_name - from ( ]' - || case when a_unordered then q'[ - select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, pk_value, col_name - from ( - select nvl(exp.rn, act.rn) rn, - nvl(exp.pk_value, act.pk_value) pk_value, - exp.col exp_item, - act.col act_item, - nvl(exp.col_name,act.col_name) col_name - from exp - join act - on exp.rn = act.rn and exp.col_name = act.col_name - 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:') ) ]' - else q'[ - select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value, col_name - 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, - max(nvl(exp.col_name,act.col_name)) col_name - from exp exp - join act act - on exp.rn = act.rn and exp.col_name = act.col_name - where dbms_lob.compare(exp.col_val, act.col_val) != 0 - group by (exp.rn, act.rn) - ) - unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') ) ]' - end ||q'[ - ) - union all - select - item_no as rn, - case when exp_data_id is null then 'Extra:' else 'Missing:' end as diff_type, - xmlserialize( - content ( - extract( (case when exp_data_id is null then act_item_data else exp_item_data end),'/*/*') - ) no indent - ) diffed_row, - nvl2( - :join_by, - replace( - extract( case when exp_data_id is null then act_item_data else exp_item_data end, :join_by ).getclobval(), - chr(10) - ), - null - ) pk_value, - case when exp_data_id is null then 1 else 2 end rnk, - 2 final_order, - null col_name - from ut_compound_data_diff_tmp i - where diff_id = :diff_id - and act_data_id is null or exp_data_id is null - ) - order by final_order,]' - ||case when a_enforce_column_order or (not(a_enforce_column_order) and not(a_unordered)) then - q'[ - case when final_order = 1 then rn else rnk end, - case when final_order = 1 then rnk else rn end - ]' - when a_unordered then - q'[ - case when final_order = 1 then col_name else to_char(rnk) end, - case when final_order = 1 then to_char(rn) else col_name end, - case when final_order = 1 then to_char(rnk) else col_name end - ]' - else - null - end; - execute immediate l_sql - bulk collect into l_results - using l_exp_extract_xpath, l_join_xpath, a_diff_id, a_expected_dataset_guid,a_extract_path, - l_act_extract_xpath, l_join_xpath, a_diff_id, a_actual_dataset_guid,a_extract_path, - l_join_xpath, l_join_xpath, a_diff_id; - return l_results; - 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); - end; - - function get_hash(a_data clob, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is - begin - return dbms_crypto.hash(a_data, a_hash_type); - end; - - function get_fixed_size_hash(a_string varchar2, a_base integer :=0,a_size integer := 9999999) return number is - begin - return dbms_utility.get_hash_value(a_string,a_base,a_size); - end; - - procedure insert_diffs_result(a_diff_tab t_diff_tab, a_diff_id raw) is - begin - forall idx in 1..a_diff_tab.count save exceptions - insert into ut_compound_data_diff_tmp - ( diff_id, act_item_data, act_data_id, exp_item_data, exp_data_id, item_no, duplicate_no ) - values - (a_diff_id, - xmlelement( name "ROW", a_diff_tab(idx).act_item_data), a_diff_tab(idx).act_data_id, - xmlelement( name "ROW", a_diff_tab(idx).exp_item_data), a_diff_tab(idx).exp_data_id, - a_diff_tab(idx).item_no, a_diff_tab(idx).dup_no); - exception - when ut_utils.ex_failure_for_all then - raise_application_error(ut_utils.gc_dml_for_all,'Failure to insert a diff tmp data.'); - end; - - procedure set_rows_diff(a_rows_diff integer) is - begin - g_diff_count := a_rows_diff; - end; - - procedure cleanup_diff is - begin - g_diff_count := 0; - end; - - function get_rows_diff_count return integer is - begin - return g_diff_count; - end; - - function is_sql_compare_allowed(a_type_name varchar2) - return boolean is - l_assert boolean; - begin - --clob/blob/xmltype/object/nestedcursor/nestedtable - if a_type_name IN (g_type_name_map(dbms_sql.blob_type), - g_type_name_map(dbms_sql.clob_type), - g_type_name_map(dbms_sql.long_type), - g_type_name_map(dbms_sql.long_raw_type), - g_type_name_map(dbms_sql.bfile_type), - g_anytype_name_map(dbms_types.typecode_namedcollection)) - then - l_assert := false; - else - l_assert := true; - end if; - return l_assert; - end; - - function get_column_type_desc(a_type_code in integer, a_dbms_sql_desc in boolean) - return varchar2 is - begin - return - case - when a_dbms_sql_desc then g_type_name_map(a_type_code) - else g_anytype_name_map(a_type_code) - end; - end; - - function get_compare_cursor(a_diff_cursor_text in clob,a_self_id raw, a_other_id raw) return sys_refcursor is - l_diff_cursor sys_refcursor; - begin - open l_diff_cursor for a_diff_cursor_text using a_self_id, a_other_id; - return l_diff_cursor; - end; - - function create_err_cursor_msg(a_error_stack varchar2) return varchar2 is - begin - return 'SQL exception thrown when fetching data from cursor:'|| - ut_utils.remove_error_from_stack(sqlerrm,ut_utils.gc_xml_processing)||chr(10)|| - ut_expectation_processor.who_called_expectation(a_error_stack)|| - 'Check the query and data for errors.'; - end; - - function type_no_length ( a_type_name varchar2) return boolean is - begin - return case - when g_type_no_length_map.exists(a_type_name) then - true - else - false - end; - end; - -begin - g_anytype_name_map(dbms_types.typecode_date) := 'DATE'; - g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; - g_anytype_name_map(3 /*INTEGER in object type*/) := '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_anytype_name_map(dbms_types.typecode_varray) := 'VARRRAY'; - g_anytype_name_map(dbms_types.typecode_table) := 'TABLE'; - g_anytype_name_map(dbms_types.typecode_namedcollection) := 'NAMEDCOLLECTION'; - g_anytype_name_map(dbms_types.typecode_object) := 'OBJECT'; - - 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'; - g_type_name_map( dbms_sql.user_defined_type ) := 'USER_DEFINED_TYPE'; - g_type_name_map( dbms_sql.ref_type ) := 'REF_TYPE'; - - - /** - * List of types that have no length but can produce a max_len from desc_cursor function. - */ - g_type_no_length_map('ROWID') := 'ROWID'; - g_type_no_length_map('INTERVAL DAY TO SECOND') := 'INTERVAL DAY TO SECOND'; - g_type_no_length_map('INTERVAL YEAR TO MONTH') := 'INTERVAL YEAR TO MONTH'; - g_type_no_length_map('BINARY_DOUBLE') := 'BINARY_DOUBLE'; - g_type_no_length_map('BINARY_FLOAT') := 'BINARY_FLOAT'; -end; -/ +create or replace package body ut_compound_data_helper is + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + g_diff_count integer; + type t_type_name_map is table of varchar2(128) index by binary_integer; + type t_types_no_length is table of varchar2(128) index by varchar2(128); + g_type_name_map t_type_name_map; + g_anytype_name_map t_type_name_map; + g_type_no_length_map t_types_no_length; + + g_compare_sql_template varchar2(4000) := + q'[ + with exp as ( + select + ucd.*, + {:duplicate_number:} dup_no + from ( + select + ucd.item_data + ,x.data_id data_id + ,position + x.item_no item_no + {:columns:} + from {:ut3_owner:}.ut_compound_data_tmp x, + xmltable('/ROWSET/ROW' passing x.item_data columns + item_data xmltype path '*' + ,position for ordinality + {:xml_to_columns:} ) ucd + where data_id = :exp_guid + ) ucd + ) + , act as ( + select + ucd.*, + {:duplicate_number:} dup_no + from ( + select + ucd.item_data + ,x.data_id data_id + ,position + x.item_no item_no + {:columns:} + from {:ut3_owner:}.ut_compound_data_tmp x, + xmltable('/ROWSET/ROW' passing x.item_data columns + item_data xmltype path '*' + ,position for ordinality + {:xml_to_columns:} ) ucd + where data_id = :act_guid + ) ucd + ) + select + a.item_data as act_item_data, + a.data_id act_data_id, + e.item_data as exp_item_data, + e.data_id exp_data_id, + {:item_no:} as item_no, + nvl(e.dup_no,a.dup_no) dup_no + from act a {:join_type:} exp e on ( {:join_condition:} ) + where {:where_condition:}]'; + + function get_columns_diff( + a_expected ut_cursor_column_tab, + a_actual ut_cursor_column_tab, + a_order_enforced boolean := false + ) return tt_column_diffs is + l_results tt_column_diffs; + begin + execute immediate q'[with + expected_cols as ( + select display_path exp_column_name,column_position exp_col_pos, + replace(column_type_name,'VARCHAR2','CHAR') exp_col_type_compare, column_type_name exp_col_type + from table(:a_expected) + where parent_name is null and hierarchy_level = 1 and column_name is not null + ), + actual_cols as ( + select display_path act_column_name,column_position act_col_pos, + replace(column_type_name,'VARCHAR2','CHAR') act_col_type_compare, column_type_name act_col_type + from table(:a_actual) + where parent_name is null and hierarchy_level = 1 and column_name is not null + ), + joined_cols as ( + select e.*,a.*]' + || case when a_order_enforced then ', + row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by a.act_col_pos) a_pos_nn, + row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by e.exp_col_pos) e_pos_nn' + else + null + end ||q'[ + from expected_cols e + full outer join actual_cols a + on e.exp_column_name = a.act_column_name + ) + select case + when exp_col_pos is null and act_col_pos is not null then '+' + when exp_col_pos is not null and act_col_pos is null then '-' + when exp_col_type_compare != act_col_type_compare then 't' + else 'p' + end as diff_type, + exp_column_name, exp_col_type, exp_col_pos, + act_column_name, act_col_type, act_col_pos + from joined_cols + --column is unexpected (extra) or missing + where act_col_pos is null or exp_col_pos is null + --column type is not matching (except CHAR/VARCHAR2) + or act_col_type_compare != exp_col_type_compare]' + || case when a_order_enforced then q'[ + --column position is not matching (both when excluded extra/missing columns as well as when they are included) + or (a_pos_nn != e_pos_nn and exp_col_pos != act_col_pos)]' + else + null + end ||q'[ + order by exp_col_pos, act_col_pos]' + bulk collect into l_results using a_expected, a_actual; + return l_results; + end; + + function generate_not_equal_stmt( + a_data_info ut_cursor_column, a_pk_table ut_varchar2_list + ) return varchar2 + is + l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); + l_index integer; + l_sql_stmt varchar2(32767); + l_exists boolean := false; + begin + l_index := l_pk_tab.first; + if l_pk_tab.count > 0 then + loop + if a_data_info.access_path = l_pk_tab(l_index) then + l_exists := true; + end if; + exit when l_index = l_pk_tab.count or (a_data_info.access_path = l_pk_tab(l_index)); + l_index := a_pk_table.next(l_index); + end loop; + end if; + if not(l_exists) then + l_sql_stmt := ' (decode(a.'||a_data_info.transformed_name||','||' e.'||a_data_info.transformed_name||',1,0) = 0)'; + end if; + return l_sql_stmt; + end; + + function generate_join_by_stmt( + a_data_info ut_cursor_column, a_pk_table ut_varchar2_list + ) return varchar2 + is + l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); + l_index integer; + l_sql_stmt varchar2(32767); + begin + if l_pk_tab.count <> 0 then + l_index:= l_pk_tab.first; + loop + if l_pk_tab(l_index) in (a_data_info.access_path, a_data_info.parent_name) then + --When then table is nested and join is on whole table + l_sql_stmt := l_sql_stmt ||' a.'||a_data_info.transformed_name||q'[ = ]'||' e.'||a_data_info.transformed_name; + end if; + exit when (a_data_info.access_path = l_pk_tab(l_index)) or l_index = l_pk_tab.count; + l_index := l_pk_tab.next(l_index); + end loop; + end if; + return l_sql_stmt; + end; + + function generate_equal_sql(a_col_name in varchar2) return varchar2 is + begin + return ' decode(a.'||a_col_name||','||' e.'||a_col_name||',1,0) = 1 '; + end; + + function generate_partition_stmt( + a_data_info ut_cursor_column, a_pk_table in ut_varchar2_list, a_alias varchar2 := 'ucd.' + ) return varchar2 + is + l_index integer; + l_sql_stmt varchar2(32767); + begin + if a_pk_table is not empty then + l_index:= a_pk_table.first; + loop + if a_pk_table(l_index) in (a_data_info.access_path, a_data_info.parent_name) then + --When then table is nested and join is on whole table + l_sql_stmt := l_sql_stmt ||a_alias||a_data_info.transformed_name; + end if; + exit when (a_data_info.access_path = a_pk_table(l_index)) or l_index = a_pk_table.count; + l_index := a_pk_table.next(l_index); + end loop; + else + l_sql_stmt := a_alias||a_data_info.transformed_name; + end if; + return l_sql_stmt; + end; + + function generate_select_stmt(a_data_info ut_cursor_column, a_alias varchar2 := 'ucd.') + return varchar2 + is + l_alias varchar2(10) := a_alias; + l_col_syntax varchar2(4000); + l_ut_owner varchar2(250) := ut_utils.ut_owner; + begin + if a_data_info.is_sql_diffable = 0 then + l_col_syntax := l_ut_owner ||'.ut_compound_data_helper.get_hash('||l_alias||a_data_info.transformed_name||'.getClobVal()) as '||a_data_info.transformed_name ; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type = 'DATE' then + l_col_syntax := 'to_date('||l_alias||a_data_info.transformed_name||') as '|| a_data_info.transformed_name; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP') then + l_col_syntax := 'to_timestamp('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_format||''') as '|| a_data_info.transformed_name; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH TIME ZONE') then + l_col_syntax := 'to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') as '|| a_data_info.transformed_name; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH LOCAL TIME ZONE') then + l_col_syntax := ' cast( to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') AS TIMESTAMP WITH LOCAL TIME ZONE) as '|| a_data_info.transformed_name; + else + l_col_syntax := l_alias||a_data_info.transformed_name||' as '|| a_data_info.transformed_name; + end if; + return l_col_syntax; + end; + + function generate_xmltab_stmt(a_data_info ut_cursor_column) return varchar2 is + l_col_type varchar2(4000); + begin + if a_data_info.is_sql_diffable = 0 then + l_col_type := 'XMLTYPE'; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('DATE','TIMESTAMP','TIMESTAMP WITH TIME ZONE', + 'TIMESTAMP WITH LOCAL TIME ZONE') then + l_col_type := 'VARCHAR2(50)'; + elsif a_data_info.is_sql_diffable = 1 and type_no_length(a_data_info.column_type) then + l_col_type := a_data_info.column_type; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('VARCHAR2','CHAR') then + l_col_type := 'VARCHAR2('||greatest(a_data_info.column_len,4000)||')'; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('NUMBER') then + --We cannot use a precision and scale as dbms_sql.describe_columns3 return precision 0 for dual table + -- there is also no need for that as we not process data but only read and compare as they are stored + l_col_type := a_data_info.column_type; + else + l_col_type := a_data_info.column_type + ||case when a_data_info.column_len is not null + then '('||a_data_info.column_len||')' + else null + end; + end if; + return a_data_info.transformed_name||' '||l_col_type||q'[ PATH ']'||a_data_info.access_path||q'[']'; + end; + + procedure gen_sql_pieces_out_of_cursor( + a_data_info ut_cursor_column_tab, + a_pk_table ut_varchar2_list, + a_unordered boolean, + a_xml_stmt out nocopy clob, + a_select_stmt out nocopy clob, + a_partition_stmt out nocopy clob, + a_join_by_stmt out nocopy clob, + a_not_equal_stmt out nocopy clob + ) is + l_partition_tmp clob; + l_xmltab_list ut_varchar2_list := ut_varchar2_list(); + l_select_list ut_varchar2_list := ut_varchar2_list(); + l_partition_list ut_varchar2_list := ut_varchar2_list(); + l_equal_list ut_varchar2_list := ut_varchar2_list(); + l_join_by_list ut_varchar2_list := ut_varchar2_list(); + l_not_equal_list ut_varchar2_list := ut_varchar2_list(); + + procedure add_element_to_list(a_list in out ut_varchar2_list, a_list_element in varchar2) + is + begin + if a_list_element is not null then + a_list.extend; + a_list(a_list.last) := a_list_element; + end if; + end; + + begin + if a_data_info is not empty then + for i in 1..a_data_info.count loop + if a_data_info(i).has_nested_col = 0 then + --Get XMLTABLE column list + add_element_to_list(l_xmltab_list,generate_xmltab_stmt(a_data_info(i))); + --Get Select statment list of columns + add_element_to_list(l_select_list, generate_select_stmt(a_data_info(i))); + --Get columns by which we partition + add_element_to_list(l_partition_list,generate_partition_stmt(a_data_info(i), a_pk_table)); + --Get equal statement + add_element_to_list(l_equal_list,generate_equal_sql(a_data_info(i).transformed_name)); + --Generate join by stmt + add_element_to_list(l_join_by_list,generate_join_by_stmt(a_data_info(i), a_pk_table)); + --Generate not equal stmt + add_element_to_list(l_not_equal_list,generate_not_equal_stmt(a_data_info(i), a_pk_table)); + end if; + end loop; + + a_xml_stmt := nullif(','||ut_utils.table_to_clob(l_xmltab_list, ' , '),','); + a_select_stmt := nullif(','||ut_utils.table_to_clob(l_select_list, ' , '),','); + l_partition_tmp := ut_utils.table_to_clob(l_partition_list, ' , '); + ut_utils.append_to_clob(a_partition_stmt,' row_number() over (partition by '||l_partition_tmp||' order by '||l_partition_tmp||' ) '); + + if a_pk_table.count > 0 then + -- If key defined do the join or these and where on diffrences + a_join_by_stmt := ut_utils.table_to_clob(l_join_by_list, ' and '); + elsif a_unordered then + -- If no key defined do the join on all columns + a_join_by_stmt := ' e.dup_no = a.dup_no and '||ut_utils.table_to_clob(l_equal_list, ' and '); + else + -- Else join on rownumber + a_join_by_stmt := 'a.item_no = e.item_no '; + end if; + a_not_equal_stmt := ut_utils.table_to_clob(l_not_equal_list, ' or '); + else + --Partition by piece when no data + ut_utils.append_to_clob(a_partition_stmt,' 1 '); + a_join_by_stmt := 'a.item_no = e.item_no '; + end if; + end; + + function gen_compare_sql( + a_other ut_data_value_refcursor, + a_join_by_list ut_varchar2_list, + a_unordered boolean, + a_inclusion_type boolean, + a_is_negated boolean + ) return clob is + l_compare_sql clob; + l_xmltable_stmt clob; + l_select_stmt clob; + l_partition_stmt clob; + l_join_on_stmt clob; + l_not_equal_stmt clob; + l_where_stmt clob; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_join_by_list ut_varchar2_list; + + function get_join_type(a_inclusion_compare in boolean,a_negated in boolean) return varchar2 is + begin + return + case + when a_inclusion_compare and not(a_negated) then ' right outer join ' + when a_inclusion_compare and a_negated then ' inner join ' + else ' full outer join ' + end; + end; + + function get_item_no(a_unordered boolean) return varchar2 is + begin + return + case + when a_unordered then 'row_number() over ( order by nvl(e.item_no,a.item_no))' + else 'nvl(e.item_no,a.item_no) ' + end; + end; + + begin + /** + * We already estabilished cursor equality so now we add anydata root if we compare anydata + * to join by. + */ + l_join_by_list := + case + when a_other is of (ut_data_value_anydata) then ut_utils.add_prefix(a_join_by_list, a_other.cursor_details.get_root) + else a_join_by_list + end; + + dbms_lob.createtemporary(l_compare_sql, true); + --Initiate a SQL template with placeholders + ut_utils.append_to_clob(l_compare_sql, g_compare_sql_template); + --Generate a pieceso of dynamic SQL that will substitute placeholders + gen_sql_pieces_out_of_cursor( + a_other.cursor_details.cursor_columns_info, l_join_by_list, a_unordered, + l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_join_on_stmt, + l_not_equal_stmt + ); + + l_compare_sql := replace(l_compare_sql,'{:duplicate_number:}',l_partition_stmt); + l_compare_sql := replace(l_compare_sql,'{:columns:}',l_select_stmt); + l_compare_sql := replace(l_compare_sql,'{:ut3_owner:}',l_ut_owner); + l_compare_sql := replace(l_compare_sql,'{:xml_to_columns:}',l_xmltable_stmt); + l_compare_sql := replace(l_compare_sql,'{:item_no:}',get_item_no(a_unordered)); + l_compare_sql := replace(l_compare_sql,'{:join_type:}',get_join_type(a_inclusion_type,a_is_negated)); + l_compare_sql := replace(l_compare_sql,'{:join_condition:}',l_join_on_stmt); + + if l_not_equal_stmt is not null and ((l_join_by_list.count > 0 and not a_is_negated) or (not a_unordered)) then + ut_utils.append_to_clob(l_where_stmt,' ( '||l_not_equal_stmt||' ) or '); + end if; + --If its inclusion we expect a actual set to fully match and have no extra elements over expected + if a_inclusion_type then + ut_utils.append_to_clob(l_where_stmt,case when a_is_negated then ' 1 = 1 ' else ' ( a.data_id is null ) ' end); + else + ut_utils.append_to_clob(l_where_stmt,' (a.data_id is null or e.data_id is null) '); + end if; + + l_compare_sql := replace(l_compare_sql,'{:where_condition:}',l_where_stmt); + return l_compare_sql; + end; + + function get_column_extract_path(a_cursor_info ut_cursor_column_tab) return ut_varchar2_list is + l_column_list ut_varchar2_list := ut_varchar2_list(); + begin + for i in 1..a_cursor_info.count loop + l_column_list.extend; + l_column_list(l_column_list.last) := a_cursor_info(i).access_path; + end loop; + return l_column_list; + end; + + function get_rows_diff_by_sql( + a_act_cursor_info ut_cursor_column_tab, a_exp_cursor_info ut_cursor_column_tab, + a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, + a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false, + a_extract_path varchar2 + ) return tt_row_diffs is + l_act_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_act_cursor_info)); + l_exp_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_exp_cursor_info)); + l_join_xpath varchar2(32767) := ut_utils.to_xpath(a_join_by_list); + l_results tt_row_diffs; + l_sql varchar2(32767); + begin + l_sql := q'[ + with exp as ( + select + exp_item_data, exp_data_id, item_no rn, rownum col_no, pk_value, + s.column_value col, s.column_value.getRootElement() col_name, + nvl(s.column_value.getclobval(),empty_clob()) col_val + from ( + select + exp_data_id, extract( ucd.exp_item_data, :column_path ) exp_item_data, item_no, + replace( extract( ucd.exp_item_data, :join_by ).getclobval(), chr(10) ) pk_value + from ut_compound_data_diff_tmp ucd + where diff_id = :diff_id + and ucd.exp_data_id = :self_guid + ) i, + table( xmlsequence( extract(i.exp_item_data,:extract_path) ) ) s + ), + act as ( + select + act_item_data, act_data_id, item_no rn, rownum col_no, pk_value, + s.column_value col, s.column_value.getRootElement() col_name, + nvl(s.column_value.getclobval(),empty_clob()) col_val + from ( + select + act_data_id, extract( ucd.act_item_data, :column_path ) act_item_data, item_no, + replace( extract( ucd.act_item_data, :join_by ).getclobval(), chr(10) ) pk_value + from ut_compound_data_diff_tmp ucd + where diff_id = :diff_id + and ucd.act_data_id = :other_guid + ) i, + table( xmlsequence( extract(i.act_item_data,:extract_path) ) ) s + ) + select rn, diff_type, diffed_row, pk_value pk_value + from ( + select rn, diff_type, diffed_row, pk_value, + case when diff_type = 'Actual:' then 1 else 2 end rnk, + 1 final_order, + col_name + from ( ]' + || case when a_unordered then q'[ + select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, pk_value, col_name + from ( + select nvl(exp.rn, act.rn) rn, + nvl(exp.pk_value, act.pk_value) pk_value, + exp.col exp_item, + act.col act_item, + nvl(exp.col_name,act.col_name) col_name + from exp + join act + on exp.rn = act.rn and exp.col_name = act.col_name + 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:') ) ]' + else q'[ + select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value, col_name + 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, + max(nvl(exp.col_name,act.col_name)) col_name + from exp exp + join act act + on exp.rn = act.rn and exp.col_name = act.col_name + where dbms_lob.compare(exp.col_val, act.col_val) != 0 + group by (exp.rn, act.rn) + ) + unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') ) ]' + end ||q'[ + ) + union all + select + item_no as rn, + case when exp_data_id is null then 'Extra:' else 'Missing:' end as diff_type, + xmlserialize( + content ( + extract( (case when exp_data_id is null then act_item_data else exp_item_data end),'/*/*') + ) no indent + ) diffed_row, + nvl2( + :join_by, + replace( + extract( case when exp_data_id is null then act_item_data else exp_item_data end, :join_by ).getclobval(), + chr(10) + ), + null + ) pk_value, + case when exp_data_id is null then 1 else 2 end rnk, + 2 final_order, + null col_name + from ut_compound_data_diff_tmp i + where diff_id = :diff_id + and act_data_id is null or exp_data_id is null + ) + order by final_order,]' + ||case when a_enforce_column_order or (not(a_enforce_column_order) and not(a_unordered)) then + q'[ + case when final_order = 1 then rn else rnk end, + case when final_order = 1 then rnk else rn end + ]' + when a_unordered then + q'[ + case when final_order = 1 then col_name else to_char(rnk) end, + case when final_order = 1 then to_char(rn) else col_name end, + case when final_order = 1 then to_char(rnk) else col_name end + ]' + else + null + end; + execute immediate l_sql + bulk collect into l_results + using l_exp_extract_xpath, l_join_xpath, a_diff_id, a_expected_dataset_guid,a_extract_path, + l_act_extract_xpath, l_join_xpath, a_diff_id, a_actual_dataset_guid,a_extract_path, + l_join_xpath, l_join_xpath, a_diff_id; + return l_results; + 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); + end; + + function get_hash(a_data clob, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is + begin + return dbms_crypto.hash(a_data, a_hash_type); + end; + + function get_fixed_size_hash(a_string varchar2, a_base integer :=0,a_size integer := 9999999) return number is + begin + return dbms_utility.get_hash_value(a_string,a_base,a_size); + end; + + procedure insert_diffs_result(a_diff_tab t_diff_tab, a_diff_id raw) is + begin + forall idx in 1..a_diff_tab.count save exceptions + insert into ut_compound_data_diff_tmp + ( diff_id, act_item_data, act_data_id, exp_item_data, exp_data_id, item_no, duplicate_no ) + values + (a_diff_id, + xmlelement( name "ROW", a_diff_tab(idx).act_item_data), a_diff_tab(idx).act_data_id, + xmlelement( name "ROW", a_diff_tab(idx).exp_item_data), a_diff_tab(idx).exp_data_id, + a_diff_tab(idx).item_no, a_diff_tab(idx).dup_no); + exception + when ut_utils.ex_failure_for_all then + raise_application_error(ut_utils.gc_dml_for_all,'Failure to insert a diff tmp data.'); + end; + + procedure set_rows_diff(a_rows_diff integer) is + begin + g_diff_count := a_rows_diff; + end; + + procedure cleanup_diff is + begin + g_diff_count := 0; + end; + + function get_rows_diff_count return integer is + begin + return g_diff_count; + end; + + function is_sql_compare_allowed(a_type_name varchar2) + return boolean is + l_assert boolean; + begin + --clob/blob/xmltype/object/nestedcursor/nestedtable + if a_type_name IN (g_type_name_map(dbms_sql.blob_type), + g_type_name_map(dbms_sql.clob_type), + g_type_name_map(dbms_sql.long_type), + g_type_name_map(dbms_sql.long_raw_type), + g_type_name_map(dbms_sql.bfile_type), + g_anytype_name_map(dbms_types.typecode_namedcollection)) + then + l_assert := false; + else + l_assert := true; + end if; + return l_assert; + end; + + function get_column_type_desc(a_type_code in integer, a_dbms_sql_desc in boolean) + return varchar2 is + begin + return + case + when a_dbms_sql_desc then g_type_name_map(a_type_code) + else g_anytype_name_map(a_type_code) + end; + end; + + function get_compare_cursor(a_diff_cursor_text in clob,a_self_id raw, a_other_id raw) return sys_refcursor is + l_diff_cursor sys_refcursor; + begin + open l_diff_cursor for a_diff_cursor_text using a_self_id, a_other_id; + return l_diff_cursor; + end; + + function create_err_cursor_msg(a_error_stack varchar2) return varchar2 is + begin + return 'SQL exception thrown when fetching data from cursor:'|| + ut_utils.remove_error_from_stack(sqlerrm,ut_utils.gc_xml_processing)||chr(10)|| + ut_expectation_processor.who_called_expectation(a_error_stack)|| + 'Check the query and data for errors.'; + end; + + function type_no_length ( a_type_name varchar2) return boolean is + begin + return case + when g_type_no_length_map.exists(a_type_name) then + true + else + false + end; + end; + +begin + g_anytype_name_map(dbms_types.typecode_date) := 'DATE'; + g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; + g_anytype_name_map(3 /*INTEGER in object type*/) := '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_anytype_name_map(dbms_types.typecode_varray) := 'VARRRAY'; + g_anytype_name_map(dbms_types.typecode_table) := 'TABLE'; + g_anytype_name_map(dbms_types.typecode_namedcollection) := 'NAMEDCOLLECTION'; + g_anytype_name_map(dbms_types.typecode_object) := 'OBJECT'; + + 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'; + g_type_name_map( dbms_sql.user_defined_type ) := 'USER_DEFINED_TYPE'; + g_type_name_map( dbms_sql.ref_type ) := 'REF_TYPE'; + + + /** + * List of types that have no length but can produce a max_len from desc_cursor function. + */ + g_type_no_length_map('ROWID') := 'ROWID'; + g_type_no_length_map('INTERVAL DAY TO SECOND') := 'INTERVAL DAY TO SECOND'; + g_type_no_length_map('INTERVAL YEAR TO MONTH') := 'INTERVAL YEAR TO MONTH'; + g_type_no_length_map('BINARY_DOUBLE') := 'BINARY_DOUBLE'; + g_type_no_length_map('BINARY_FLOAT') := 'BINARY_FLOAT'; +end; +/ diff --git a/source/expectations/data_values/ut_cursor_column.tpb b/source/expectations/data_values/ut_cursor_column.tpb index 3c6931a5d..835a7ee04 100644 --- a/source/expectations/data_values/ut_cursor_column.tpb +++ b/source/expectations/data_values/ut_cursor_column.tpb @@ -1,69 +1,70 @@ -create or replace type body ut_cursor_column as - - member procedure init( - self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer - ) is - begin - self.parent_name := a_parent_name; --Name of the parent if its nested - self.hierarchy_level := a_hierarchy_level; --Hierarchy level - self.column_position := a_col_position; --Position of the column in cursor/ type - self.column_len := a_col_max_len; --length of column - self.column_precision := a_col_precision; - self.column_scale := a_col_scale; - self.column_name := TRIM( BOTH '''' FROM a_col_name); --name of the column - self.column_type_name := coalesce(a_col_type_name,a_col_type); --type name e.g. test_dummy_object or varchar2 - self.xml_valid_name := ut_utils.get_valid_xml_name(self.column_name); - self.display_path := case when a_access_path is null then - self.column_name - else - a_access_path||'/'||self.column_name - end; --Access path used for incldue exclude eg/ TEST_DUMMY_OBJECT/VARCHAR2 - self.access_path := case when a_access_path is null then - self.xml_valid_name - else - a_access_path||'/'||self.xml_valid_name - end; --Access path used for incldue exclude eg/ TEST_DUMMY_OBJECT/VARCHAR2 - self.transformed_name := case when length(self.xml_valid_name) > 30 then - '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' - when self.parent_name is null then - '"'||self.xml_valid_name||'"' - else - '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' - end; --when is nestd we need to hash name to make sure we dont exceed 30 char - self.column_type := a_col_type; --column type e.g. user_defined , varchar2 - self.column_schema := a_col_schema_name; -- schema name - self.is_sql_diffable := case - when lower(self.column_type) = 'user_defined_type' then - 0 - -- Due to bug in 11g/12.1 collection fails on varchar 4000+ - when (lower(self.column_type) in ('varchar2','char')) and (self.column_len > 4000) then - 0 - else - ut_utils.boolean_to_int(ut_compound_data_helper.is_sql_compare_allowed(self.column_type)) - end; --can we directly compare or do we need to hash value - self.is_collection := a_collection; - self.has_nested_col := case when lower(self.column_type) = 'user_defined_type' and self.is_collection = 0 then 1 else 0 end; - end; - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer - ) return self as result is - begin - init(a_col_name, a_col_schema_name, a_col_type_name, a_col_max_len, a_parent_name,a_hierarchy_level, a_col_position, - a_col_type, a_collection,a_access_path,a_col_precision,a_col_scale); - return; - end; - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result is - begin - return; - end; -end; -/ +create or replace type body ut_cursor_column as + + member procedure init( + self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer + ) is + begin + self.parent_name := a_parent_name; --Name of the parent if its nested + self.hierarchy_level := a_hierarchy_level; --Hierarchy level + self.column_position := a_col_position; --Position of the column in cursor/ type + self.column_len := a_col_max_len; --length of column + self.column_precision := a_col_precision; + self.column_scale := a_col_scale; + self.column_name := TRIM( BOTH '''' FROM a_col_name); --name of the column + self.column_type_name := coalesce(a_col_type_name,a_col_type); --type name e.g. test_dummy_object or varchar2 + self.xml_valid_name := ut_utils.get_valid_xml_name(self.column_name); + self.display_path := case when a_access_path is null then + self.column_name + else + a_access_path||'/'||self.column_name + end; --Access path used for incldue exclude eg/ TEST_DUMMY_OBJECT/VARCHAR2 + self.access_path := case when a_access_path is null then + self.xml_valid_name + else + a_access_path||'/'||self.xml_valid_name + end; --Access path used for XMLTABLE query + self.filter_path := '/'||self.access_path; --Filter path will differ from access path in anydata type + self.transformed_name := case when length(self.xml_valid_name) > 30 then + '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' + when self.parent_name is null then + '"'||self.xml_valid_name||'"' + else + '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.xml_valid_name)||'"' + end; --when is nestd we need to hash name to make sure we dont exceed 30 char + self.column_type := a_col_type; --column type e.g. user_defined , varchar2 + self.column_schema := a_col_schema_name; -- schema name + self.is_sql_diffable := case + when lower(self.column_type) = 'user_defined_type' then + 0 + -- Due to bug in 11g/12.1 collection fails on varchar 4000+ + when (lower(self.column_type) in ('varchar2','char')) and (self.column_len > 4000) then + 0 + else + ut_utils.boolean_to_int(ut_compound_data_helper.is_sql_compare_allowed(self.column_type)) + end; --can we directly compare or do we need to hash value + self.is_collection := a_collection; + self.has_nested_col := case when lower(self.column_type) = 'user_defined_type' and self.is_collection = 0 then 1 else 0 end; + end; + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer + ) return self as result is + begin + init(a_col_name, a_col_schema_name, a_col_type_name, a_col_max_len, a_parent_name,a_hierarchy_level, a_col_position, + a_col_type, a_collection,a_access_path,a_col_precision,a_col_scale); + return; + end; + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result is + begin + return; + end; +end; +/ diff --git a/source/expectations/data_values/ut_cursor_column.tps b/source/expectations/data_values/ut_cursor_column.tps index db9cbd3ae..da3c004f2 100644 --- a/source/expectations/data_values/ut_cursor_column.tps +++ b/source/expectations/data_values/ut_cursor_column.tps @@ -1,51 +1,52 @@ -create or replace type ut_cursor_column force authid current_user as object ( - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - parent_name varchar2(4000), - access_path varchar2(4000), - display_path varchar2(4000), - has_nested_col number(1,0), - transformed_name varchar2(2000), - hierarchy_level number, - column_position number, - xml_valid_name varchar2(2000), - column_name varchar2(2000), - column_type varchar2(128), - column_type_name varchar2(128), - column_schema varchar2(128), - column_len integer, - column_precision integer, - column_scale integer, - is_sql_diffable number(1, 0), - is_collection number(1, 0), - - member procedure init(self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer), - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer, a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer) - return self as result, - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result -) -/ +create or replace type ut_cursor_column authid current_user as object ( + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + parent_name varchar2(4000), + access_path varchar2(4000), + filter_path varchar2(4000), + display_path varchar2(4000), + has_nested_col number(1,0), + transformed_name varchar2(2000), + hierarchy_level number, + column_position number, + xml_valid_name varchar2(2000), + column_name varchar2(2000), + column_type varchar2(128), + column_type_name varchar2(128), + column_schema varchar2(128), + column_len integer, + column_precision integer, + column_scale integer, + is_sql_diffable number(1, 0), + is_collection number(1, 0), + + member procedure init(self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer), + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer, a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer) + return self as result, + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result +) +/ diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb index 99e1d4edc..2a70a51dd 100644 --- a/source/expectations/data_values/ut_cursor_details.tpb +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -1,286 +1,257 @@ -create or replace type body ut_cursor_details as - - member function equals( a_other ut_cursor_details, a_match_options ut_matcher_options ) return boolean is - l_diffs integer; - begin - select count(1) into l_diffs - from table(self.cursor_columns_info) a - full outer join table(a_other.cursor_columns_info) e - on decode(a.parent_name,e.parent_name,1,0)= 1 - and a.column_name = e.column_name - and replace(a.column_type,'VARCHAR2','CHAR') = replace(e.column_type,'VARCHAR2','CHAR') - and ( a.column_position = e.column_position or a_match_options.columns_are_unordered_flag = 1 ) - where a.column_name is null or e.column_name is null; - return l_diffs = 0; - end; - - member procedure desc_compound_data( - self in out nocopy ut_cursor_details, a_compound_data anytype, - a_parent_name in varchar2, a_level in integer, a_access_path in varchar2 - ) is - l_idx pls_integer := 1; - l_elements_info ut_metadata.t_anytype_members_rec; - l_element_info ut_metadata.t_anytype_elem_info_rec; - l_is_collection boolean; - begin - l_elements_info := ut_metadata.get_anytype_members_info( a_compound_data ); - l_is_collection := ut_metadata.is_collection(l_elements_info.type_code); - if l_elements_info.elements_count is null then - l_element_info := ut_metadata.get_attr_elem_info( a_compound_data ); - self.cursor_columns_info.extend; - self.cursor_columns_info(cursor_columns_info.last) := - ut_cursor_column( - l_elements_info.type_name, - l_elements_info.schema_name, - null, - l_elements_info.length, - a_parent_name, - a_level, - l_idx, - ut_compound_data_helper.get_column_type_desc(l_elements_info.type_code,false), - ut_utils.boolean_to_int(l_is_collection), - a_access_path, - l_elements_info.precision, - l_elements_info.scale - ); - if l_element_info.attr_elt_type is not null then - desc_compound_data( - l_element_info.attr_elt_type, l_elements_info.type_name, - a_level + 1, a_access_path || '/' || l_elements_info.type_name - ); - end if; - else - while l_idx <= l_elements_info.elements_count loop - l_element_info := ut_metadata.get_attr_elem_info( a_compound_data, l_idx ); - - self.cursor_columns_info.extend; - self.cursor_columns_info(cursor_columns_info.last) := - ut_cursor_column( - l_element_info.attribute_name, - l_elements_info.schema_name, - null, - l_element_info.length, - a_parent_name, - a_level, - l_idx, - ut_compound_data_helper.get_column_type_desc(l_element_info.type_code,false), - ut_utils.boolean_to_int(l_is_collection), - a_access_path, - l_elements_info.precision, - l_elements_info.scale - ); - if l_element_info.attr_elt_type is not null then - desc_compound_data( - l_element_info.attr_elt_type, l_element_info.attribute_name, - a_level + 1, a_access_path || '/' || l_element_info.attribute_name - ); - end if; - l_idx := l_idx + 1; - end loop; - end if; - end; - - constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result is - begin - self.cursor_columns_info := ut_cursor_column_tab(); - return; - end; - - constructor function ut_cursor_details( - self in out nocopy ut_cursor_details, - a_cursor_number in number - ) return self as result is - l_columns_count pls_integer; - l_columns_desc dbms_sql.desc_tab3; - l_is_collection boolean; - l_hierarchy_level integer := 1; - begin - self.cursor_columns_info := ut_cursor_column_tab(); - dbms_sql.describe_columns3(a_cursor_number, l_columns_count, l_columns_desc); - - /** - * Due to a bug with object being part of cursor in ANYDATA scenario - * oracle fails to revert number to cursor. We ar using dbms_sql.close cursor to close it - * to avoid leaving open cursors behind. - * a_cursor := dbms_sql.to_refcursor(l_cursor_number); - **/ - for pos in 1 .. l_columns_count loop - l_is_collection := ut_metadata.is_collection( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ); - self.cursor_columns_info.extend; - self.cursor_columns_info(self.cursor_columns_info.last) := - ut_cursor_column( - l_columns_desc(pos).col_name, - l_columns_desc(pos).col_schema_name, - l_columns_desc(pos).col_type_name, - l_columns_desc(pos).col_max_len, - null, - l_hierarchy_level, - pos, - ut_compound_data_helper.get_column_type_desc(l_columns_desc(pos).col_type,true), - ut_utils.boolean_to_int(l_is_collection), - null, - l_columns_desc(pos).col_precision, - l_columns_desc(pos).col_scale - ); - - if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then - desc_compound_data( - ut_metadata.get_user_defined_type( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ), - l_columns_desc(pos).col_name, - l_hierarchy_level + 1, - l_columns_desc(pos).col_name - ); - end if; - end loop; - return; - end; - - member function contains_collection return boolean is - l_collection_elements number; - begin - select count(1) into l_collection_elements - from table(cursor_columns_info) c - where c.is_collection = 1 and rownum = 1; - return l_collection_elements > 0; - end; - - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is - l_result ut_varchar2_list; - l_root varchar2(125); - begin - if self.is_anydata = 1 then - l_root := get_root; - end if; - --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') - select fl.column_value - bulk collect into l_result - from table(a_expected_columns) fl - where not exists ( - select 1 from table(self.cursor_columns_info) c - where regexp_like(c.access_path,'^/?'|| - case - when self.is_anydata = 1 then - l_root||'/'||trim (leading '/' from fl.column_value) - else - fl.column_value - end||'($|/.*)' - ) - ) - order by fl.column_value; - return l_result; - end; - - member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options) is - l_result ut_cursor_details := self; - l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); - l_column ut_cursor_column; - l_root varchar2(125); - c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; - begin - if l_result.cursor_columns_info is not null then - - if self.is_anydata = 1 then - l_root := get_root; - end if; - - --limit columns to those on the include items minus exclude items - if a_match_options.include.items.count > 0 then - -- if include - exclude = 0 then keep all columns - if a_match_options.include.items != a_match_options.exclude.items then - with included_columns as ( - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.include.items) - minus - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.exclude.items) - ) - select value(x) - bulk collect into l_result.cursor_columns_info - from table(self.cursor_columns_info) x - where exists( - select 1 from included_columns f where regexp_like(x.access_path,'^/?'|| - case - when self.is_anydata = 1 then - l_root||'/'||trim(leading '/' from f.col_names) - else - f.col_names - end||'($|/.*)' - ) - ) - or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; - end if; - elsif a_match_options.exclude.items.count > 0 then - with excluded_columns as ( - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.exclude.items) - ) - select value(x) - bulk collect into l_result.cursor_columns_info - from table(self.cursor_columns_info) x - where not exists( - select 1 from excluded_columns f where regexp_like(x.access_path,'^/?'|| - case - when self.is_anydata = 1 then - l_root||'/'||trim(leading '/' from f.col_names) - else - f.col_names - end||'($|/.*)' ) - ); - end if; - - --Rewrite column order after columns been excluded - for i in ( - select parent_name, access_path, display_path, has_nested_col, - transformed_name, hierarchy_level, - rownum as new_position, xml_valid_name, - column_name, column_type, column_type_name, column_schema, - column_len, column_precision ,column_scale ,is_sql_diffable, is_collection,value(x) col_info - from table(l_result.cursor_columns_info) x - order by x.column_position asc - ) loop - l_column := i.col_info; - l_column.column_position := i.new_position; - l_column_tab.extend; - l_column_tab(l_column_tab.last) := l_column; - end loop; - - l_result.cursor_columns_info := l_column_tab; - self := l_result; - end if; - end; - - member function get_xml_children(a_parent_name varchar2 := null) return xmltype is - l_result xmltype; - begin - select xmlagg(xmlelement(evalname t.column_name,t.column_type_name)) - into l_result - from table(self.cursor_columns_info) t - where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) - having count(*) > 0; - return l_result; - end; - - member procedure has_anydata(self in out nocopy ut_cursor_details, a_is_anydata in boolean :=false) is - begin - self.is_anydata := case when nvl(a_is_anydata,false) then 1 else 0 end; - end; - - member function has_anydata return boolean is - begin - return ut_utils.int_to_boolean(nvl(self.is_anydata,0)); - end; - - member function get_root return varchar2 is - l_root varchar2(250); - begin - if self.cursor_columns_info.count > 0 then - select x.access_path into l_root from table(self.cursor_columns_info) x - where x.hierarchy_level = 1; - else - l_root := null; - end if; - return l_root; - end; - -end; -/ +create or replace type body ut_cursor_details as + + member function equals( a_other ut_cursor_details, a_match_options ut_matcher_options ) return boolean is + l_diffs integer; + begin + select count(1) into l_diffs + from table(self.cursor_columns_info) a + full outer join table(a_other.cursor_columns_info) e + on decode(a.parent_name,e.parent_name,1,0)= 1 + and a.column_name = e.column_name + and replace(a.column_type,'VARCHAR2','CHAR') = replace(e.column_type,'VARCHAR2','CHAR') + and ( a.column_position = e.column_position or a_match_options.columns_are_unordered_flag = 1 ) + where a.column_name is null or e.column_name is null; + return l_diffs = 0; + end; + + member procedure desc_compound_data( + self in out nocopy ut_cursor_details, a_compound_data anytype, + a_parent_name in varchar2, a_level in integer, a_access_path in varchar2 + ) is + l_idx pls_integer := 1; + l_elements_info ut_metadata.t_anytype_members_rec; + l_element_info ut_metadata.t_anytype_elem_info_rec; + l_is_collection boolean; + begin + l_elements_info := ut_metadata.get_anytype_members_info( a_compound_data ); + l_is_collection := ut_metadata.is_collection(l_elements_info.type_code); + if l_elements_info.elements_count is null then + l_element_info := ut_metadata.get_attr_elem_info( a_compound_data ); + self.cursor_columns_info.extend; + self.cursor_columns_info(cursor_columns_info.last) := + ut_cursor_column( + l_elements_info.type_name, + l_elements_info.schema_name, + null, + l_elements_info.length, + a_parent_name, + a_level, + l_idx, + ut_compound_data_helper.get_column_type_desc(l_elements_info.type_code,false), + ut_utils.boolean_to_int(l_is_collection), + a_access_path, + l_elements_info.precision, + l_elements_info.scale + ); + if l_element_info.attr_elt_type is not null then + desc_compound_data( + l_element_info.attr_elt_type, l_elements_info.type_name, + a_level + 1, a_access_path || '/' || l_elements_info.type_name + ); + end if; + else + while l_idx <= l_elements_info.elements_count loop + l_element_info := ut_metadata.get_attr_elem_info( a_compound_data, l_idx ); + + self.cursor_columns_info.extend; + self.cursor_columns_info(cursor_columns_info.last) := + ut_cursor_column( + l_element_info.attribute_name, + l_elements_info.schema_name, + null, + l_element_info.length, + a_parent_name, + a_level, + l_idx, + ut_compound_data_helper.get_column_type_desc(l_element_info.type_code,false), + ut_utils.boolean_to_int(l_is_collection), + a_access_path, + l_elements_info.precision, + l_elements_info.scale + ); + if l_element_info.attr_elt_type is not null then + desc_compound_data( + l_element_info.attr_elt_type, l_element_info.attribute_name, + a_level + 1, a_access_path || '/' || l_element_info.attribute_name + ); + end if; + l_idx := l_idx + 1; + end loop; + end if; + end; + + constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result is + begin + self.cursor_columns_info := ut_cursor_column_tab(); + return; + end; + + constructor function ut_cursor_details( + self in out nocopy ut_cursor_details, + a_cursor_number in number + ) return self as result is + l_columns_count pls_integer; + l_columns_desc dbms_sql.desc_tab3; + l_is_collection boolean; + l_hierarchy_level integer := 1; + begin + self.cursor_columns_info := ut_cursor_column_tab(); + self.is_anydata := 0; + dbms_sql.describe_columns3(a_cursor_number, l_columns_count, l_columns_desc); + + /** + * Due to a bug with object being part of cursor in ANYDATA scenario + * oracle fails to revert number to cursor. We ar using dbms_sql.close cursor to close it + * to avoid leaving open cursors behind. + * a_cursor := dbms_sql.to_refcursor(l_cursor_number); + **/ + for pos in 1 .. l_columns_count loop + l_is_collection := ut_metadata.is_collection( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ); + self.cursor_columns_info.extend; + self.cursor_columns_info(self.cursor_columns_info.last) := + ut_cursor_column( + l_columns_desc(pos).col_name, + l_columns_desc(pos).col_schema_name, + l_columns_desc(pos).col_type_name, + l_columns_desc(pos).col_max_len, + null, + l_hierarchy_level, + pos, + ut_compound_data_helper.get_column_type_desc(l_columns_desc(pos).col_type,true), + ut_utils.boolean_to_int(l_is_collection), + null, + l_columns_desc(pos).col_precision, + l_columns_desc(pos).col_scale + ); + + if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then + desc_compound_data( + ut_metadata.get_user_defined_type( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ), + l_columns_desc(pos).col_name, + l_hierarchy_level + 1, + l_columns_desc(pos).col_name + ); + end if; + end loop; + return; + end; + + member function contains_collection return boolean is + l_collection_elements number; + begin + select count(1) into l_collection_elements + from table(cursor_columns_info) c + where c.is_collection = 1 and rownum = 1; + return l_collection_elements > 0; + end; + + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is + l_result ut_varchar2_list; + begin + --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') + select fl.column_value + bulk collect into l_result + from table(a_expected_columns) fl + where not exists ( + select 1 from table(self.cursor_columns_info) c + where regexp_like(c.filter_path,'^/?'||fl.column_value||'($|/.*)' ) + ) + order by fl.column_value; + return l_result; + end; + + member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options) is + l_result ut_cursor_details := self; + l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); + l_column ut_cursor_column; + c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; + begin + if l_result.cursor_columns_info is not null then + + --limit columns to those on the include items minus exclude items + if a_match_options.include.items.count > 0 then + -- if include - exclude = 0 then keep all columns + if a_match_options.include.items != a_match_options.exclude.items then + with included_columns as ( + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.include.items) + minus + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.exclude.items) + ) + select value(x) + bulk collect into l_result.cursor_columns_info + from table(self.cursor_columns_info) x + where exists( + select 1 from included_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) + ) + or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; + end if; + elsif a_match_options.exclude.items.count > 0 then + with excluded_columns as ( + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.exclude.items) + ) + select value(x) + bulk collect into l_result.cursor_columns_info + from table(self.cursor_columns_info) x + where not exists( + select 1 from excluded_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) + ); + end if; + + --Rewrite column order after columns been excluded + for i in ( + select parent_name, access_path, display_path, has_nested_col, + transformed_name, hierarchy_level, + rownum as new_position, xml_valid_name, + column_name, column_type, column_type_name, column_schema, + column_len, column_precision ,column_scale ,is_sql_diffable, is_collection,value(x) col_info + from table(l_result.cursor_columns_info) x + order by x.column_position asc + ) loop + l_column := i.col_info; + l_column.column_position := i.new_position; + l_column_tab.extend; + l_column_tab(l_column_tab.last) := l_column; + end loop; + + l_result.cursor_columns_info := l_column_tab; + self := l_result; + end if; + end; + + member function get_xml_children(a_parent_name varchar2 := null) return xmltype is + l_result xmltype; + begin + select xmlagg(xmlelement(evalname t.column_name,t.column_type_name)) + into l_result + from table(self.cursor_columns_info) t + where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) + having count(*) > 0; + return l_result; + end; + + member function get_root return varchar2 is + l_root varchar2(250); + begin + if self.cursor_columns_info.count > 0 then + select x.access_path into l_root from table(self.cursor_columns_info) x + where x.hierarchy_level = 1; + else + l_root := null; + end if; + return l_root; + end; + + member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) is + l_root varchar2(250) := get_root(); + begin + self.is_anydata := 1; + for i in 1..cursor_columns_info.count loop + self.cursor_columns_info(i).filter_path := ut_utils.strip_prefix(self.cursor_columns_info(i).access_path,l_root); + end loop; + end; + +end; +/ diff --git a/source/expectations/data_values/ut_cursor_details.tps b/source/expectations/data_values/ut_cursor_details.tps index ce5aefbe7..e6c80a3b5 100644 --- a/source/expectations/data_values/ut_cursor_details.tps +++ b/source/expectations/data_values/ut_cursor_details.tps @@ -1,42 +1,41 @@ -create or replace type ut_cursor_details force authid current_user as object ( - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - cursor_columns_info ut_cursor_column_tab, - - /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ - is_anydata number(1,0), - constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, - constructor function ut_cursor_details( - self in out nocopy ut_cursor_details,a_cursor_number in number - ) return self as result, - member function equals(a_other ut_cursor_details, a_match_options ut_matcher_options) return boolean, - member procedure desc_compound_data( - self in out nocopy ut_cursor_details, - a_compound_data anytype, - a_parent_name in varchar2, - a_level in integer, - a_access_path in varchar2 - ), - member function contains_collection return boolean, - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, - member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), - member function get_xml_children(a_parent_name varchar2 := null) return xmltype, - member procedure has_anydata(self in out nocopy ut_cursor_details, a_is_anydata in boolean := false), - member function has_anydata return boolean, - member function get_root return varchar2 -) -/ +create or replace type ut_cursor_details authid current_user as object ( + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + cursor_columns_info ut_cursor_column_tab, + + /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ + is_anydata number(1,0), + constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, + constructor function ut_cursor_details( + self in out nocopy ut_cursor_details,a_cursor_number in number + ) return self as result, + member function equals(a_other ut_cursor_details, a_match_options ut_matcher_options) return boolean, + member procedure desc_compound_data( + self in out nocopy ut_cursor_details, + a_compound_data anytype, + a_parent_name in varchar2, + a_level in integer, + a_access_path in varchar2 + ), + member function contains_collection return boolean, + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, + member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), + member function get_xml_children(a_parent_name varchar2 := null) return xmltype, + member function get_root return varchar2, + member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) +) +/ diff --git a/source/expectations/data_values/ut_data_value_anydata.tpb b/source/expectations/data_values/ut_data_value_anydata.tpb index fa59ad67c..808e52197 100644 --- a/source/expectations/data_values/ut_data_value_anydata.tpb +++ b/source/expectations/data_values/ut_data_value_anydata.tpb @@ -1,145 +1,143 @@ -create or replace type body ut_data_value_anydata as - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - overriding member function get_object_info return varchar2 is - begin - return self.data_type || case when self.compound_type = 'collection' then ' [ count = '||self.elements_count||' ]' else null end; - end; - - member function get_extract_path(a_data_value anydata) return varchar2 is - l_path varchar2(10); - begin - if self.compound_type = 'object' then - l_path := '/*/*'; - else - case when ut_metadata.has_collection_members(a_data_value) then - l_path := '/*/*'; - else - l_path := '/*'; - end case; - end if; - return l_path; - end; - - member function get_cursor_sql_from_anydata(a_data_value anydata) return varchar2 is - l_cursor_sql varchar2(32767); - begin - l_cursor_sql := ' - declare - l_data '||self.data_type||'; - l_value anydata := :a_value; - l_status integer; - l_tmp_refcursor sys_refcursor; - begin - l_status := l_value.get'||self.compound_type||'(l_data); '|| - case when self.compound_type = 'collection' then - q'[ open :l_tmp_refcursor for select value(x) as "]'|| - ut_metadata.get_object_name(ut_metadata.get_collection_element(a_data_value))|| - q'[" from table(l_data) x;]' - else - q'[ open :l_tmp_refcursor for select l_data as "]'||ut_metadata.get_object_name(self.data_type)|| - q'[" from dual;]' - end || - 'end;'; - return l_cursor_sql; - end; - - member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata) is - l_refcursor sys_refcursor; - l_ctx number; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - cursor_not_open exception; - l_cursor_number number; - l_anydata_sql varchar2(32767); - begin - self.data_type := ut_metadata.get_anydata_typename(a_value); - self.compound_type := get_instance(a_value); - self.is_data_null := ut_metadata.is_anytype_null(a_value,self.compound_type); - self.data_id := sys_guid(); - self.self_type := $$plsql_unit; - self.cursor_details := ut_cursor_details(); - - ut_compound_data_helper.cleanup_diff; - - if not self.is_null() then - self.extract_path := get_extract_path(a_value); - l_anydata_sql := get_cursor_sql_from_anydata(a_value); - execute immediate l_anydata_sql using in a_value, in out l_refcursor; - if l_refcursor%isopen then - self.extract_cursor(l_refcursor); - l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); - self.cursor_details := ut_cursor_details(l_cursor_number); - self.cursor_details.has_anydata(true); - dbms_sql.close_cursor(l_cursor_number); - elsif not l_refcursor%isopen then - raise cursor_not_open; - end if; - end if; - exception - when cursor_not_open then - raise_application_error(-20155, 'Cursor is not open'); - when others then - if l_refcursor%isopen then - close l_refcursor; - end if; - raise; - end; - - member function get_instance(a_data_value anydata) return varchar2 is - l_result varchar2(30); - begin - l_result := ut_metadata.get_anydata_compound_type(a_data_value); - if l_result not in ('object','collection') then - raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL'); - end if; - return l_result; - end; - - constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata, a_value anydata) return self as result - is - begin - init(a_value); - return; - end; - - overriding member function compare_implementation( - a_other ut_data_value, - a_match_options ut_matcher_options, - a_inclusion_compare boolean := false, - a_is_negated boolean := false - ) return integer is - l_result integer := 0; - begin - if not a_other is of (ut_data_value_anydata) then - raise value_error; - end if; - l_result := l_result + (self as ut_data_value_refcursor).compare_implementation(a_other,a_match_options,a_inclusion_compare,a_is_negated); - return l_result; - end; - - overriding member function is_empty return boolean is - begin - if self.compound_type = 'collection' then - return self.elements_count = 0; - else - raise value_error; - end if; - end; - -end; -/ +create or replace type body ut_data_value_anydata as + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + overriding member function get_object_info return varchar2 is + begin + return self.data_type || case when self.compound_type = 'collection' then ' [ count = '||self.elements_count||' ]' else null end; + end; + + member function get_extract_path(a_data_value anydata) return varchar2 is + l_path varchar2(10); + begin + if self.compound_type = 'object' then + l_path := '/*/*'; + else + case when ut_metadata.has_collection_members(a_data_value) then + l_path := '/*/*'; + else + l_path := '/*'; + end case; + end if; + return l_path; + end; + + member function get_cursor_sql_from_anydata(a_data_value anydata) return varchar2 is + l_cursor_sql varchar2(32767); + begin + l_cursor_sql := ' + declare + l_data '||self.data_type||'; + l_value anydata := :a_value; + l_status integer; + l_tmp_refcursor sys_refcursor; + begin + l_status := l_value.get'||self.compound_type||'(l_data); '|| + case when self.compound_type = 'collection' then + q'[ open :l_tmp_refcursor for select value(x) as "]'|| + ut_metadata.get_object_name(ut_metadata.get_collection_element(a_data_value))|| + q'[" from table(l_data) x;]' + else + q'[ open :l_tmp_refcursor for select l_data as "]'||ut_metadata.get_object_name(self.data_type)|| + q'[" from dual;]' + end || + 'end;'; + return l_cursor_sql; + end; + + member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata) is + l_refcursor sys_refcursor; + cursor_not_open exception; + l_cursor_number number; + l_anydata_sql varchar2(32767); + begin + self.data_type := ut_metadata.get_anydata_typename(a_value); + self.compound_type := get_instance(a_value); + self.is_data_null := ut_metadata.is_anytype_null(a_value,self.compound_type); + self.data_id := sys_guid(); + self.self_type := $$plsql_unit; + self.cursor_details := ut_cursor_details(); + + ut_compound_data_helper.cleanup_diff; + + if not self.is_null() then + self.extract_path := get_extract_path(a_value); + l_anydata_sql := get_cursor_sql_from_anydata(a_value); + execute immediate l_anydata_sql using in a_value, in out l_refcursor; + if l_refcursor%isopen then + self.extract_cursor(l_refcursor); + l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); + self.cursor_details := ut_cursor_details(l_cursor_number); + self.cursor_details.strip_root_from_anydata; + dbms_sql.close_cursor(l_cursor_number); + elsif not l_refcursor%isopen then + raise cursor_not_open; + end if; + end if; + exception + when cursor_not_open then + raise_application_error(-20155, 'Cursor is not open'); + when others then + if l_refcursor%isopen then + close l_refcursor; + end if; + raise; + end; + + member function get_instance(a_data_value anydata) return varchar2 is + l_result varchar2(30); + begin + l_result := ut_metadata.get_anydata_compound_type(a_data_value); + if l_result not in ('object','collection') then + raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL'); + end if; + return l_result; + end; + + constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata, a_value anydata) return self as result + is + begin + init(a_value); + return; + end; + + overriding member function compare_implementation( + a_other ut_data_value, + a_match_options ut_matcher_options, + a_inclusion_compare boolean := false, + a_is_negated boolean := false + ) return integer is + l_result integer := 0; + begin + if not a_other is of (ut_data_value_anydata) then + raise value_error; + end if; + l_result := l_result + (self as ut_data_value_refcursor).compare_implementation(a_other,a_match_options,a_inclusion_compare,a_is_negated); + return l_result; + end; + + overriding member function is_empty return boolean is + begin + if self.compound_type = 'collection' then + return self.elements_count = 0; + else + raise value_error; + end if; + end; + +end; +/ diff --git a/source/expectations/data_values/ut_data_value_refcursor.tpb b/source/expectations/data_values/ut_data_value_refcursor.tpb index 2aac626e3..b93158cf1 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tpb +++ b/source/expectations/data_values/ut_data_value_refcursor.tpb @@ -1,399 +1,398 @@ -create or replace type body ut_data_value_refcursor as - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) - return self as result is - begin - init(a_value); - return; - end; - - member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) - is - c_bulk_rows constant integer := 10000; - l_cursor sys_refcursor := a_value; - l_ctx number; - l_xml xmltype; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_set_id integer := 0; - l_elements_count number := 0; - begin - -- We use DBMS_XMLGEN in order to: - -- 1) be able to process data in bulks (set of rows) - -- 2) be able to influence the ROWSET/ROW tags - -- 3) be able to influence the way NULL values are handled (empty TAG) - -- 4) be able to influence the way TIMESTAMP is formatted. - -- Due to Oracle feature/bug, it is not possible to change the DATE formatting of cursor data - -- AFTER the cursor was opened. - -- The only solution for this is to change NLS settings before opening the cursor. - -- - -- This would work fine if we could use DBMS_XMLGEN.restartQuery. - -- The restartQuery fails however if PLSQL variables of TIMESTAMP/INTERVAL or CLOB/BLOB are used. - ut_expectation_processor.set_xml_nls_params(); - l_ctx := dbms_xmlgen.newContext(l_cursor); - dbms_xmlgen.setNullHandling(l_ctx, dbms_xmlgen.empty_tag); - dbms_xmlgen.setMaxRows(l_ctx, c_bulk_rows); - loop - l_xml := dbms_xmlgen.getxmltype(l_ctx); - exit when dbms_xmlgen.getNumRowsProcessed(l_ctx) = 0; - --Bug in oracle 12.2+ where XML binary storage trimming insignificant whitespaces. - $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then - l_xml := xmltype( replace(l_xml.getClobVal(),' 0 then - ut_utils.append_to_clob( l_result, self.cursor_details.get_xml_children().getclobval() ); - end if; - ut_utils.append_to_clob(l_result,chr(10)||(self as ut_compound_data_value).to_string()); - l_result_string := ut_utils.to_string(l_result,null); - dbms_lob.freetemporary(l_result); - end if; - return l_result_string; - end; - - overriding member function diff( a_other ut_data_value, a_match_options ut_matcher_options ) return varchar2 is - l_result clob; - l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); - l_result_string varchar2(32767); - l_other ut_data_value_refcursor; - l_self ut_data_value_refcursor := self; - l_column_diffs ut_compound_data_helper.tt_column_diffs; - - l_other_cols ut_cursor_column_tab; - l_self_cols ut_cursor_column_tab; - - l_act_missing_pk ut_varchar2_list := ut_varchar2_list(); - l_exp_missing_pk ut_varchar2_list := ut_varchar2_list(); - - c_max_rows integer := ut_utils.gc_diff_max_rows; - l_diff_id ut_compound_data_helper.t_hash; - l_diff_row_count integer; - l_row_diffs ut_compound_data_helper.tt_row_diffs; - l_message varchar2(32767); - - function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is - begin - return - case a_col.diff_type - when '-' then - ' Column <'||a_col.expected_name||'> [data-type: '||a_col.expected_type||'] is missing. Expected column position: '||a_col.expected_pos||'.' - when '+' then - ' Column <'||a_col.actual_name||'> [position: '||a_col.actual_pos||', data-type: '||a_col.actual_type||'] is not expected in results.' - when 't' then - ' Column <'||a_col.actual_name||'> data-type is invalid. Expected: '||a_col.expected_type||',' ||' actual: '||a_col.actual_type||'.' - when 'p' then - ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' - end; - end; - - function remove_incomparable_cols( - a_cursor_details ut_cursor_column_tab, a_column_diffs ut_compound_data_helper.tt_column_diffs - ) return ut_cursor_column_tab is - l_missing_cols ut_varchar2_list := ut_varchar2_list(); - l_result ut_cursor_column_tab; - begin - for i in 1 .. a_column_diffs.count loop - if a_column_diffs(i).diff_type in ('-','+') then - l_missing_cols.extend; - l_missing_cols(l_missing_cols.last) := coalesce(a_column_diffs(i).expected_name, a_column_diffs(i).actual_name); - end if; - end loop; - select value(i) bulk collect into l_result - from table(a_cursor_details) i - where i.access_path not in ( - select c.column_value - from table(l_missing_cols) c - ); - return l_result; - end; - - function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs, a_is_unordered boolean) return varchar2 is - begin - if a_is_unordered then - if 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; - else - return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; - else - return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; - 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); - l_other.cursor_details.filter_columns(a_match_options); - l_self.cursor_details.filter_columns(a_match_options); - - l_other_cols := l_other.cursor_details.cursor_columns_info; - l_self_cols := l_self.cursor_details.cursor_columns_info; - - dbms_lob.createtemporary(l_result,true); - --diff columns - if not l_self.is_null and not l_other.is_null then - l_column_diffs := ut_compound_data_helper.get_columns_diff( - l_self.cursor_details.cursor_columns_info, - l_other.cursor_details.cursor_columns_info, - a_match_options.ordered_columns() - ); - - if l_column_diffs.count > 0 then - ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); - l_other_cols := remove_incomparable_cols( l_other_cols, l_column_diffs ); - l_self_cols := remove_incomparable_cols( l_self_cols, l_column_diffs ); - for i in 1 .. l_column_diffs.count loop - l_results.extend; - l_results(l_results.last) := get_col_diff_text(l_column_diffs(i)); - end loop; - ut_utils.append_to_clob(l_result, l_results); - end if; - end if; - - --check for missing pk - if a_match_options.join_by.items.count > 0 then - l_act_missing_pk := l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); - l_exp_missing_pk := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); - end if; - - --diff rows and row elements if the pk is not missing - if l_act_missing_pk.count + l_exp_missing_pk.count = 0 then - l_diff_id := ut_compound_data_helper.get_hash( l_self.data_id || l_other.data_id ); - - -- First tell how many rows are different - l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; - if l_diff_row_count > 0 then - l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( - l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, - l_diff_id, - case - when - l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) - else - a_match_options.join_by.items - end, - a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path - ); - 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 - ||' ]'||chr(10)|| case when l_row_diffs.count = 0 then ' All rows are different as the columns are not matching.' else null end; - ut_utils.append_to_clob( l_result, l_message ); - l_results := ut_utils.t_clob_tab(); - for i in 1 .. l_row_diffs.count loop - l_results.extend; - l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_match_options.unordered); - end loop; - ut_utils.append_to_clob(l_result,l_results); - else - l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.'; - ut_utils.append_to_clob( l_result, l_message ); - end if; - else - ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); - - for i in 1 .. l_exp_missing_pk.count loop - ut_utils.append_to_clob(l_result, ' Join key '||l_exp_missing_pk(i)||' does not exists in expected'||chr(10)); - end loop; - - for i in 1 .. l_act_missing_pk.count loop - ut_utils.append_to_clob(l_result, ' Join key '||l_act_missing_pk(i)||' does not exists in actual'||chr(10)); - end loop; - - if l_self.cursor_details.contains_collection() or l_other.cursor_details.contains_collection() then - ut_utils.append_to_clob(l_result,' Please make sure that your join clause is not refferring to collection element'|| chr(10)); - end if; - - 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) return integer is - begin - return compare_implementation( a_other, null ); - end; - - member function compare_implementation( - a_other ut_data_value, - a_match_options ut_matcher_options, - a_inclusion_compare boolean := false, - a_is_negated boolean := false - ) return integer is - l_result integer := 0; - l_self ut_data_value_refcursor := self; - l_other ut_data_value_refcursor; - l_diff_cursor_text clob; - - function compare_data( - a_self ut_data_value_refcursor, - a_other ut_data_value_refcursor, - a_diff_cursor_text clob - ) return integer is - l_diff_id ut_compound_data_helper.t_hash; - l_result integer; - --We will start with number od differences being displayed. - l_cursor sys_refcursor; - l_diff_tab ut_compound_data_helper.t_diff_tab; - l_diif_rowcount integer :=0; - begin - l_diff_id := ut_compound_data_helper.get_hash(a_self.data_id||a_other.data_id); - - begin - l_cursor := ut_compound_data_helper.get_compare_cursor(a_diff_cursor_text, - a_self.data_id, a_other.data_id); - --fetch and save rows for display of diff - fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_diff_max_rows; - exception when others then - if l_cursor%isopen then - close l_cursor; - end if; - raise; - end; - - ut_compound_data_helper.insert_diffs_result( l_diff_tab, l_diff_id ); - --fetch rows for count only - loop - exit when l_diff_tab.count = 0; - l_diif_rowcount := l_diif_rowcount + l_diff_tab.count; - fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_bc_fetch_limit; - end loop; - - ut_compound_data_helper.set_rows_diff(l_diif_rowcount); - - --result is OK only if both are same - if l_diif_rowcount = 0 and a_self.is_null = a_other.is_null then - l_result := 0; - else - l_result := 1; - end if; - close l_cursor; - return l_result; - 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); - l_other.cursor_details.filter_columns( a_match_options ); - l_self.cursor_details.filter_columns( a_match_options ); - - if a_match_options.join_by.items.count > 0 then - l_result := - l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count - + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; - end if; - - if l_result = 0 then - if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then - l_result := 1; - end if; - - l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( - l_other, - a_match_options.join_by.items, - a_match_options.unordered(), - a_inclusion_compare, - a_is_negated - ); - l_result := l_result + compare_data( l_self, l_other, l_diff_cursor_text ); - end if; - return l_result; - end; - - overriding member function is_empty return boolean is - begin - return self.elements_count = 0; - end; - -end; -/ +create or replace type body ut_data_value_refcursor as + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) + return self as result is + begin + init(a_value); + return; + end; + + member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) + is + c_bulk_rows constant integer := 10000; + l_cursor sys_refcursor := a_value; + l_ctx number; + l_xml xmltype; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_set_id integer := 0; + l_elements_count number := 0; + begin + -- We use DBMS_XMLGEN in order to: + -- 1) be able to process data in bulks (set of rows) + -- 2) be able to influence the ROWSET/ROW tags + -- 3) be able to influence the way NULL values are handled (empty TAG) + -- 4) be able to influence the way TIMESTAMP is formatted. + -- Due to Oracle feature/bug, it is not possible to change the DATE formatting of cursor data + -- AFTER the cursor was opened. + -- The only solution for this is to change NLS settings before opening the cursor. + -- + -- This would work fine if we could use DBMS_XMLGEN.restartQuery. + -- The restartQuery fails however if PLSQL variables of TIMESTAMP/INTERVAL or CLOB/BLOB are used. + ut_expectation_processor.set_xml_nls_params(); + l_ctx := dbms_xmlgen.newContext(l_cursor); + dbms_xmlgen.setNullHandling(l_ctx, dbms_xmlgen.empty_tag); + dbms_xmlgen.setMaxRows(l_ctx, c_bulk_rows); + loop + l_xml := dbms_xmlgen.getxmltype(l_ctx); + exit when dbms_xmlgen.getNumRowsProcessed(l_ctx) = 0; + --Bug in oracle 12.2+ where XML binary storage trimming insignificant whitespaces. + $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then + l_xml := xmltype( replace(l_xml.getClobVal(),' 0 then + ut_utils.append_to_clob( l_result, self.cursor_details.get_xml_children().getclobval() ); + end if; + ut_utils.append_to_clob(l_result,chr(10)||(self as ut_compound_data_value).to_string()); + l_result_string := ut_utils.to_string(l_result,null); + dbms_lob.freetemporary(l_result); + end if; + return l_result_string; + end; + + overriding member function diff( a_other ut_data_value, a_match_options ut_matcher_options ) return varchar2 is + l_result clob; + l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); + l_result_string varchar2(32767); + l_other ut_data_value_refcursor; + l_self ut_data_value_refcursor := self; + l_column_diffs ut_compound_data_helper.tt_column_diffs; + + l_other_cols ut_cursor_column_tab; + l_self_cols ut_cursor_column_tab; + + l_act_missing_pk ut_varchar2_list := ut_varchar2_list(); + l_exp_missing_pk ut_varchar2_list := ut_varchar2_list(); + + c_max_rows integer := ut_utils.gc_diff_max_rows; + l_diff_id ut_compound_data_helper.t_hash; + l_diff_row_count integer; + l_row_diffs ut_compound_data_helper.tt_row_diffs; + l_message varchar2(32767); + + function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is + begin + return + case a_col.diff_type + when '-' then + ' Column <'||a_col.expected_name||'> [data-type: '||a_col.expected_type||'] is missing. Expected column position: '||a_col.expected_pos||'.' + when '+' then + ' Column <'||a_col.actual_name||'> [position: '||a_col.actual_pos||', data-type: '||a_col.actual_type||'] is not expected in results.' + when 't' then + ' Column <'||a_col.actual_name||'> data-type is invalid. Expected: '||a_col.expected_type||',' ||' actual: '||a_col.actual_type||'.' + when 'p' then + ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' + end; + end; + + function remove_incomparable_cols( + a_cursor_details ut_cursor_column_tab, a_column_diffs ut_compound_data_helper.tt_column_diffs + ) return ut_cursor_column_tab is + l_missing_cols ut_varchar2_list := ut_varchar2_list(); + l_result ut_cursor_column_tab; + begin + for i in 1 .. a_column_diffs.count loop + if a_column_diffs(i).diff_type in ('-','+') then + l_missing_cols.extend; + l_missing_cols(l_missing_cols.last) := coalesce(a_column_diffs(i).expected_name, a_column_diffs(i).actual_name); + end if; + end loop; + select value(i) bulk collect into l_result + from table(a_cursor_details) i + where i.access_path not in ( + select c.column_value + from table(l_missing_cols) c + ); + return l_result; + end; + + function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs, a_is_unordered boolean) return varchar2 is + begin + if a_is_unordered then + if 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; + else + return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + else + return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + 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); + l_other.cursor_details.filter_columns(a_match_options); + l_self.cursor_details.filter_columns(a_match_options); + + l_other_cols := l_other.cursor_details.cursor_columns_info; + l_self_cols := l_self.cursor_details.cursor_columns_info; + + dbms_lob.createtemporary(l_result,true); + --diff columns + if not l_self.is_null and not l_other.is_null then + l_column_diffs := ut_compound_data_helper.get_columns_diff( + l_self.cursor_details.cursor_columns_info, + l_other.cursor_details.cursor_columns_info, + a_match_options.ordered_columns() + ); + + if l_column_diffs.count > 0 then + ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); + l_other_cols := remove_incomparable_cols( l_other_cols, l_column_diffs ); + l_self_cols := remove_incomparable_cols( l_self_cols, l_column_diffs ); + for i in 1 .. l_column_diffs.count loop + l_results.extend; + l_results(l_results.last) := get_col_diff_text(l_column_diffs(i)); + end loop; + ut_utils.append_to_clob(l_result, l_results); + end if; + end if; + + --check for missing pk + if a_match_options.join_by.items.count > 0 then + l_act_missing_pk := l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); + l_exp_missing_pk := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); + end if; + + --diff rows and row elements if the pk is not missing + if l_act_missing_pk.count + l_exp_missing_pk.count = 0 then + l_diff_id := ut_compound_data_helper.get_hash( l_self.data_id || l_other.data_id ); + + -- First tell how many rows are different + l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; + if l_diff_row_count > 0 then + l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( + l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, + l_diff_id, + case + when + l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) + else + a_match_options.join_by.items + end, + a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path + ); + 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 + ||' ]'||chr(10)|| case when l_row_diffs.count = 0 then ' All rows are different as the columns are not matching.' else null end; + ut_utils.append_to_clob( l_result, l_message ); + l_results := ut_utils.t_clob_tab(); + for i in 1 .. l_row_diffs.count loop + l_results.extend; + l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_match_options.unordered); + end loop; + ut_utils.append_to_clob(l_result,l_results); + else + l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.'; + ut_utils.append_to_clob( l_result, l_message ); + end if; + else + ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); + + for i in 1 .. l_exp_missing_pk.count loop + ut_utils.append_to_clob(l_result, ' Join key '||l_exp_missing_pk(i)||' does not exists in expected'||chr(10)); + end loop; + + for i in 1 .. l_act_missing_pk.count loop + ut_utils.append_to_clob(l_result, ' Join key '||l_act_missing_pk(i)||' does not exists in actual'||chr(10)); + end loop; + + if l_self.cursor_details.contains_collection() or l_other.cursor_details.contains_collection() then + ut_utils.append_to_clob(l_result,' Please make sure that your join clause is not refferring to collection element'|| chr(10)); + end if; + + 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) return integer is + begin + return compare_implementation( a_other, null ); + end; + + member function compare_implementation( + a_other ut_data_value, + a_match_options ut_matcher_options, + a_inclusion_compare boolean := false, + a_is_negated boolean := false + ) return integer is + l_result integer := 0; + l_self ut_data_value_refcursor := self; + l_other ut_data_value_refcursor; + l_diff_cursor_text clob; + + function compare_data( + a_self ut_data_value_refcursor, + a_other ut_data_value_refcursor, + a_diff_cursor_text clob + ) return integer is + l_diff_id ut_compound_data_helper.t_hash; + l_result integer; + --We will start with number od differences being displayed. + l_cursor sys_refcursor; + l_diff_tab ut_compound_data_helper.t_diff_tab; + l_diif_rowcount integer :=0; + begin + l_diff_id := ut_compound_data_helper.get_hash(a_self.data_id||a_other.data_id); + + begin + l_cursor := ut_compound_data_helper.get_compare_cursor(a_diff_cursor_text, + a_self.data_id, a_other.data_id); + --fetch and save rows for display of diff + fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_diff_max_rows; + exception when others then + if l_cursor%isopen then + close l_cursor; + end if; + raise; + end; + + ut_compound_data_helper.insert_diffs_result( l_diff_tab, l_diff_id ); + --fetch rows for count only + loop + exit when l_diff_tab.count = 0; + l_diif_rowcount := l_diif_rowcount + l_diff_tab.count; + fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_bc_fetch_limit; + end loop; + + ut_compound_data_helper.set_rows_diff(l_diif_rowcount); + + --result is OK only if both are same + if l_diif_rowcount = 0 and a_self.is_null = a_other.is_null then + l_result := 0; + else + l_result := 1; + end if; + close l_cursor; + return l_result; + 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); + l_other.cursor_details.filter_columns( a_match_options ); + l_self.cursor_details.filter_columns( a_match_options ); + + if a_match_options.join_by.items.count > 0 then + l_result := + l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count + + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; + end if; + + if l_result = 0 then + if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then + l_result := 1; + end if; + + l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( + l_other, + a_match_options.join_by.items, + a_match_options.unordered(), + a_inclusion_compare, + a_is_negated + ); + l_result := l_result + compare_data( l_self, l_other, l_diff_cursor_text ); + end if; + return l_result; + end; + + overriding member function is_empty return boolean is + begin + return self.elements_count = 0; + end; + +end; +/ From d609ee8d678e018076e9b54370c24e043087591a Mon Sep 17 00:00:00 2001 From: LUKASZ104 Date: Fri, 24 May 2019 15:26:41 +0100 Subject: [PATCH 6/8] PHASE 2 --- source/expectations/data_values/ut_cursor_details.tpb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb index 2a70a51dd..0c617a226 100644 --- a/source/expectations/data_values/ut_cursor_details.tpb +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -249,7 +249,7 @@ create or replace type body ut_cursor_details as begin self.is_anydata := 1; for i in 1..cursor_columns_info.count loop - self.cursor_columns_info(i).filter_path := ut_utils.strip_prefix(self.cursor_columns_info(i).access_path,l_root); + self.cursor_columns_info(i).filter_path := '/'||ut_utils.strip_prefix(self.cursor_columns_info(i).access_path,l_root); end loop; end; From be7fd082f07c5ff6c1cded7e2e1643e97bd1f0d4 Mon Sep 17 00:00:00 2001 From: lwasylow Date: Tue, 4 Jun 2019 13:01:16 +0100 Subject: [PATCH 7/8] update tests to be less complicated --- docs/userguide/advanced_data_comparison.md | 69 ++++++++++++---------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/docs/userguide/advanced_data_comparison.md b/docs/userguide/advanced_data_comparison.md index 39c35ebed..042bb847f 100644 --- a/docs/userguide/advanced_data_comparison.md +++ b/docs/userguide/advanced_data_comparison.md @@ -129,6 +129,14 @@ end; Example of `include / exclude` for anydata.convertCollection ```plsql +create or replace type person as object( + name varchar2(100), + age integer +) +/ +create or replace type people as table of person +/ + create or replace package ut_anydata_inc_exc IS --%suite(Anydata) @@ -138,46 +146,38 @@ create or replace package ut_anydata_inc_exc IS --%test(Anydata exclude) procedure ut_anydata_test_exc; - + + --%test(Fail on age) + procedure ut_fail_anydata_test; + end ut_anydata_inc_exc; / create or replace package body ut_anydata_inc_exc IS procedure ut_anydata_test_inc IS - l_actual ut3_tester_helper.test_dummy_object_list; - l_expected ut3_tester_helper.test_dummy_object_list; + l_actual people := people(person('Matt',45)); + l_expected people :=people(person('Matt',47)); begin - --Arrange - select ut3_tester_helper.test_dummy_object( rownum, 'Something Name'||rownum, rownum) - bulk collect into l_actual - from dual connect by level <=2 - order by rownum asc; - select ut3_tester_helper.test_dummy_object( rownum, 'Something '||rownum, rownum) - bulk collect into l_expected - from dual connect by level <=2 - order by rownum asc; - --Act - ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).include('ID,Value'); + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).include('NAME'); end; - + procedure ut_anydata_test_exc IS - l_actual ut3_tester_helper.test_dummy_object_list; - l_expected ut3_tester_helper.test_dummy_object_list; + l_actual people := people(person('Matt',45)); + l_expected people :=people(person('Matt',47)); begin --Arrange - select ut3_tester_helper.test_dummy_object( rownum, 'Something Name'||rownum, rownum) - bulk collect into l_actual - from dual connect by level <=2 - order by rownum asc; - select ut3_tester_helper.test_dummy_object( rownum, 'Something '||rownum, rownum) - bulk collect into l_expected - from dual connect by level <=2 - order by rownum asc; - --Act - ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).exclude('name'); + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).exclude('AGE'); end; + procedure ut_fail_anydata_test IS + l_actual people := people(person('Matt',45)); + l_expected people :=people(person('Matt',47)); + begin + --Arrange + ut3.ut.expect(anydata.convertCollection(l_actual)).to_equal(anydata.convertCollection(l_expected)).include('AGE'); + end; + end ut_anydata_inc_exc; / @@ -187,11 +187,18 @@ will result in : ```sql Anydata - Anydata include [.07 sec] - Anydata exclude [.058 sec] + Anydata include [.044 sec] + Anydata exclude [.035 sec] + Fail on age [.058 sec] (FAILED - 1) + +Failures: -Finished in .131218 seconds -2 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s) + 1) ut_fail_anydata_test + Actual: ut3.people [ count = 1 ] was expected to equal: ut3.people [ count = 1 ] + Diff: + Rows: [ 1 differences ] + Row No. 1 - Actual: 45 + Row No. 1 - Expected: 47 ``` From 0007298715a28fa5d2e0da3cd4460395b9ff5a39 Mon Sep 17 00:00:00 2001 From: lwasylow Date: Wed, 5 Jun 2019 08:48:02 +0100 Subject: [PATCH 8/8] Fixing encoding issue --- source/core/ut_utils.pks | 2 +- .../data_values/ut_compound_data_helper.pkb | 1388 ++++++++--------- .../data_values/ut_cursor_column.tps | 104 +- .../data_values/ut_cursor_details.tpb | 513 +++--- .../data_values/ut_cursor_details.tps | 82 +- .../data_values/ut_data_value_anydata.tpb | 285 ++-- .../data_values/ut_data_value_refcursor.tpb | 795 +++++----- 7 files changed, 1583 insertions(+), 1586 deletions(-) diff --git a/source/core/ut_utils.pks b/source/core/ut_utils.pks index 4969fbd70..792bccb52 100644 --- a/source/core/ut_utils.pks +++ b/source/core/ut_utils.pks @@ -413,5 +413,5 @@ create or replace package ut_utils authid definer is function strip_prefix(a_item varchar2, a_prefix varchar2, a_connector varchar2 := '/') return varchar2; -end ut_utils; + end ut_utils; / diff --git a/source/expectations/data_values/ut_compound_data_helper.pkb b/source/expectations/data_values/ut_compound_data_helper.pkb index 759e3ad70..1d5686f03 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pkb +++ b/source/expectations/data_values/ut_compound_data_helper.pkb @@ -1,694 +1,694 @@ -create or replace package body ut_compound_data_helper is - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - g_diff_count integer; - type t_type_name_map is table of varchar2(128) index by binary_integer; - type t_types_no_length is table of varchar2(128) index by varchar2(128); - g_type_name_map t_type_name_map; - g_anytype_name_map t_type_name_map; - g_type_no_length_map t_types_no_length; - - g_compare_sql_template varchar2(4000) := - q'[ - with exp as ( - select - ucd.*, - {:duplicate_number:} dup_no - from ( - select - ucd.item_data - ,x.data_id data_id - ,position + x.item_no item_no - {:columns:} - from {:ut3_owner:}.ut_compound_data_tmp x, - xmltable('/ROWSET/ROW' passing x.item_data columns - item_data xmltype path '*' - ,position for ordinality - {:xml_to_columns:} ) ucd - where data_id = :exp_guid - ) ucd - ) - , act as ( - select - ucd.*, - {:duplicate_number:} dup_no - from ( - select - ucd.item_data - ,x.data_id data_id - ,position + x.item_no item_no - {:columns:} - from {:ut3_owner:}.ut_compound_data_tmp x, - xmltable('/ROWSET/ROW' passing x.item_data columns - item_data xmltype path '*' - ,position for ordinality - {:xml_to_columns:} ) ucd - where data_id = :act_guid - ) ucd - ) - select - a.item_data as act_item_data, - a.data_id act_data_id, - e.item_data as exp_item_data, - e.data_id exp_data_id, - {:item_no:} as item_no, - nvl(e.dup_no,a.dup_no) dup_no - from act a {:join_type:} exp e on ( {:join_condition:} ) - where {:where_condition:}]'; - - function get_columns_diff( - a_expected ut_cursor_column_tab, - a_actual ut_cursor_column_tab, - a_order_enforced boolean := false - ) return tt_column_diffs is - l_results tt_column_diffs; - begin - execute immediate q'[with - expected_cols as ( - select display_path exp_column_name,column_position exp_col_pos, - replace(column_type_name,'VARCHAR2','CHAR') exp_col_type_compare, column_type_name exp_col_type - from table(:a_expected) - where parent_name is null and hierarchy_level = 1 and column_name is not null - ), - actual_cols as ( - select display_path act_column_name,column_position act_col_pos, - replace(column_type_name,'VARCHAR2','CHAR') act_col_type_compare, column_type_name act_col_type - from table(:a_actual) - where parent_name is null and hierarchy_level = 1 and column_name is not null - ), - joined_cols as ( - select e.*,a.*]' - || case when a_order_enforced then ', - row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by a.act_col_pos) a_pos_nn, - row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by e.exp_col_pos) e_pos_nn' - else - null - end ||q'[ - from expected_cols e - full outer join actual_cols a - on e.exp_column_name = a.act_column_name - ) - select case - when exp_col_pos is null and act_col_pos is not null then '+' - when exp_col_pos is not null and act_col_pos is null then '-' - when exp_col_type_compare != act_col_type_compare then 't' - else 'p' - end as diff_type, - exp_column_name, exp_col_type, exp_col_pos, - act_column_name, act_col_type, act_col_pos - from joined_cols - --column is unexpected (extra) or missing - where act_col_pos is null or exp_col_pos is null - --column type is not matching (except CHAR/VARCHAR2) - or act_col_type_compare != exp_col_type_compare]' - || case when a_order_enforced then q'[ - --column position is not matching (both when excluded extra/missing columns as well as when they are included) - or (a_pos_nn != e_pos_nn and exp_col_pos != act_col_pos)]' - else - null - end ||q'[ - order by exp_col_pos, act_col_pos]' - bulk collect into l_results using a_expected, a_actual; - return l_results; - end; - - function generate_not_equal_stmt( - a_data_info ut_cursor_column, a_pk_table ut_varchar2_list - ) return varchar2 - is - l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); - l_index integer; - l_sql_stmt varchar2(32767); - l_exists boolean := false; - begin - l_index := l_pk_tab.first; - if l_pk_tab.count > 0 then - loop - if a_data_info.access_path = l_pk_tab(l_index) then - l_exists := true; - end if; - exit when l_index = l_pk_tab.count or (a_data_info.access_path = l_pk_tab(l_index)); - l_index := a_pk_table.next(l_index); - end loop; - end if; - if not(l_exists) then - l_sql_stmt := ' (decode(a.'||a_data_info.transformed_name||','||' e.'||a_data_info.transformed_name||',1,0) = 0)'; - end if; - return l_sql_stmt; - end; - - function generate_join_by_stmt( - a_data_info ut_cursor_column, a_pk_table ut_varchar2_list - ) return varchar2 - is - l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); - l_index integer; - l_sql_stmt varchar2(32767); - begin - if l_pk_tab.count <> 0 then - l_index:= l_pk_tab.first; - loop - if l_pk_tab(l_index) in (a_data_info.access_path, a_data_info.parent_name) then - --When then table is nested and join is on whole table - l_sql_stmt := l_sql_stmt ||' a.'||a_data_info.transformed_name||q'[ = ]'||' e.'||a_data_info.transformed_name; - end if; - exit when (a_data_info.access_path = l_pk_tab(l_index)) or l_index = l_pk_tab.count; - l_index := l_pk_tab.next(l_index); - end loop; - end if; - return l_sql_stmt; - end; - - function generate_equal_sql(a_col_name in varchar2) return varchar2 is - begin - return ' decode(a.'||a_col_name||','||' e.'||a_col_name||',1,0) = 1 '; - end; - - function generate_partition_stmt( - a_data_info ut_cursor_column, a_pk_table in ut_varchar2_list, a_alias varchar2 := 'ucd.' - ) return varchar2 - is - l_index integer; - l_sql_stmt varchar2(32767); - begin - if a_pk_table is not empty then - l_index:= a_pk_table.first; - loop - if a_pk_table(l_index) in (a_data_info.access_path, a_data_info.parent_name) then - --When then table is nested and join is on whole table - l_sql_stmt := l_sql_stmt ||a_alias||a_data_info.transformed_name; - end if; - exit when (a_data_info.access_path = a_pk_table(l_index)) or l_index = a_pk_table.count; - l_index := a_pk_table.next(l_index); - end loop; - else - l_sql_stmt := a_alias||a_data_info.transformed_name; - end if; - return l_sql_stmt; - end; - - function generate_select_stmt(a_data_info ut_cursor_column, a_alias varchar2 := 'ucd.') - return varchar2 - is - l_alias varchar2(10) := a_alias; - l_col_syntax varchar2(4000); - l_ut_owner varchar2(250) := ut_utils.ut_owner; - begin - if a_data_info.is_sql_diffable = 0 then - l_col_syntax := l_ut_owner ||'.ut_compound_data_helper.get_hash('||l_alias||a_data_info.transformed_name||'.getClobVal()) as '||a_data_info.transformed_name ; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type = 'DATE' then - l_col_syntax := 'to_date('||l_alias||a_data_info.transformed_name||') as '|| a_data_info.transformed_name; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP') then - l_col_syntax := 'to_timestamp('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_format||''') as '|| a_data_info.transformed_name; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH TIME ZONE') then - l_col_syntax := 'to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') as '|| a_data_info.transformed_name; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH LOCAL TIME ZONE') then - l_col_syntax := ' cast( to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') AS TIMESTAMP WITH LOCAL TIME ZONE) as '|| a_data_info.transformed_name; - else - l_col_syntax := l_alias||a_data_info.transformed_name||' as '|| a_data_info.transformed_name; - end if; - return l_col_syntax; - end; - - function generate_xmltab_stmt(a_data_info ut_cursor_column) return varchar2 is - l_col_type varchar2(4000); - begin - if a_data_info.is_sql_diffable = 0 then - l_col_type := 'XMLTYPE'; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('DATE','TIMESTAMP','TIMESTAMP WITH TIME ZONE', - 'TIMESTAMP WITH LOCAL TIME ZONE') then - l_col_type := 'VARCHAR2(50)'; - elsif a_data_info.is_sql_diffable = 1 and type_no_length(a_data_info.column_type) then - l_col_type := a_data_info.column_type; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('VARCHAR2','CHAR') then - l_col_type := 'VARCHAR2('||greatest(a_data_info.column_len,4000)||')'; - elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('NUMBER') then - --We cannot use a precision and scale as dbms_sql.describe_columns3 return precision 0 for dual table - -- there is also no need for that as we not process data but only read and compare as they are stored - l_col_type := a_data_info.column_type; - else - l_col_type := a_data_info.column_type - ||case when a_data_info.column_len is not null - then '('||a_data_info.column_len||')' - else null - end; - end if; - return a_data_info.transformed_name||' '||l_col_type||q'[ PATH ']'||a_data_info.access_path||q'[']'; - end; - - procedure gen_sql_pieces_out_of_cursor( - a_data_info ut_cursor_column_tab, - a_pk_table ut_varchar2_list, - a_unordered boolean, - a_xml_stmt out nocopy clob, - a_select_stmt out nocopy clob, - a_partition_stmt out nocopy clob, - a_join_by_stmt out nocopy clob, - a_not_equal_stmt out nocopy clob - ) is - l_partition_tmp clob; - l_xmltab_list ut_varchar2_list := ut_varchar2_list(); - l_select_list ut_varchar2_list := ut_varchar2_list(); - l_partition_list ut_varchar2_list := ut_varchar2_list(); - l_equal_list ut_varchar2_list := ut_varchar2_list(); - l_join_by_list ut_varchar2_list := ut_varchar2_list(); - l_not_equal_list ut_varchar2_list := ut_varchar2_list(); - - procedure add_element_to_list(a_list in out ut_varchar2_list, a_list_element in varchar2) - is - begin - if a_list_element is not null then - a_list.extend; - a_list(a_list.last) := a_list_element; - end if; - end; - - begin - if a_data_info is not empty then - for i in 1..a_data_info.count loop - if a_data_info(i).has_nested_col = 0 then - --Get XMLTABLE column list - add_element_to_list(l_xmltab_list,generate_xmltab_stmt(a_data_info(i))); - --Get Select statment list of columns - add_element_to_list(l_select_list, generate_select_stmt(a_data_info(i))); - --Get columns by which we partition - add_element_to_list(l_partition_list,generate_partition_stmt(a_data_info(i), a_pk_table)); - --Get equal statement - add_element_to_list(l_equal_list,generate_equal_sql(a_data_info(i).transformed_name)); - --Generate join by stmt - add_element_to_list(l_join_by_list,generate_join_by_stmt(a_data_info(i), a_pk_table)); - --Generate not equal stmt - add_element_to_list(l_not_equal_list,generate_not_equal_stmt(a_data_info(i), a_pk_table)); - end if; - end loop; - - a_xml_stmt := nullif(','||ut_utils.table_to_clob(l_xmltab_list, ' , '),','); - a_select_stmt := nullif(','||ut_utils.table_to_clob(l_select_list, ' , '),','); - l_partition_tmp := ut_utils.table_to_clob(l_partition_list, ' , '); - ut_utils.append_to_clob(a_partition_stmt,' row_number() over (partition by '||l_partition_tmp||' order by '||l_partition_tmp||' ) '); - - if a_pk_table.count > 0 then - -- If key defined do the join or these and where on diffrences - a_join_by_stmt := ut_utils.table_to_clob(l_join_by_list, ' and '); - elsif a_unordered then - -- If no key defined do the join on all columns - a_join_by_stmt := ' e.dup_no = a.dup_no and '||ut_utils.table_to_clob(l_equal_list, ' and '); - else - -- Else join on rownumber - a_join_by_stmt := 'a.item_no = e.item_no '; - end if; - a_not_equal_stmt := ut_utils.table_to_clob(l_not_equal_list, ' or '); - else - --Partition by piece when no data - ut_utils.append_to_clob(a_partition_stmt,' 1 '); - a_join_by_stmt := 'a.item_no = e.item_no '; - end if; - end; - - function gen_compare_sql( - a_other ut_data_value_refcursor, - a_join_by_list ut_varchar2_list, - a_unordered boolean, - a_inclusion_type boolean, - a_is_negated boolean - ) return clob is - l_compare_sql clob; - l_xmltable_stmt clob; - l_select_stmt clob; - l_partition_stmt clob; - l_join_on_stmt clob; - l_not_equal_stmt clob; - l_where_stmt clob; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_join_by_list ut_varchar2_list; - - function get_join_type(a_inclusion_compare in boolean,a_negated in boolean) return varchar2 is - begin - return - case - when a_inclusion_compare and not(a_negated) then ' right outer join ' - when a_inclusion_compare and a_negated then ' inner join ' - else ' full outer join ' - end; - end; - - function get_item_no(a_unordered boolean) return varchar2 is - begin - return - case - when a_unordered then 'row_number() over ( order by nvl(e.item_no,a.item_no))' - else 'nvl(e.item_no,a.item_no) ' - end; - end; - - begin - /** - * We already estabilished cursor equality so now we add anydata root if we compare anydata - * to join by. - */ - l_join_by_list := - case - when a_other is of (ut_data_value_anydata) then ut_utils.add_prefix(a_join_by_list, a_other.cursor_details.get_root) - else a_join_by_list - end; - - dbms_lob.createtemporary(l_compare_sql, true); - --Initiate a SQL template with placeholders - ut_utils.append_to_clob(l_compare_sql, g_compare_sql_template); - --Generate a pieceso of dynamic SQL that will substitute placeholders - gen_sql_pieces_out_of_cursor( - a_other.cursor_details.cursor_columns_info, l_join_by_list, a_unordered, - l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_join_on_stmt, - l_not_equal_stmt - ); - - l_compare_sql := replace(l_compare_sql,'{:duplicate_number:}',l_partition_stmt); - l_compare_sql := replace(l_compare_sql,'{:columns:}',l_select_stmt); - l_compare_sql := replace(l_compare_sql,'{:ut3_owner:}',l_ut_owner); - l_compare_sql := replace(l_compare_sql,'{:xml_to_columns:}',l_xmltable_stmt); - l_compare_sql := replace(l_compare_sql,'{:item_no:}',get_item_no(a_unordered)); - l_compare_sql := replace(l_compare_sql,'{:join_type:}',get_join_type(a_inclusion_type,a_is_negated)); - l_compare_sql := replace(l_compare_sql,'{:join_condition:}',l_join_on_stmt); - - if l_not_equal_stmt is not null and ((l_join_by_list.count > 0 and not a_is_negated) or (not a_unordered)) then - ut_utils.append_to_clob(l_where_stmt,' ( '||l_not_equal_stmt||' ) or '); - end if; - --If its inclusion we expect a actual set to fully match and have no extra elements over expected - if a_inclusion_type then - ut_utils.append_to_clob(l_where_stmt,case when a_is_negated then ' 1 = 1 ' else ' ( a.data_id is null ) ' end); - else - ut_utils.append_to_clob(l_where_stmt,' (a.data_id is null or e.data_id is null) '); - end if; - - l_compare_sql := replace(l_compare_sql,'{:where_condition:}',l_where_stmt); - return l_compare_sql; - end; - - function get_column_extract_path(a_cursor_info ut_cursor_column_tab) return ut_varchar2_list is - l_column_list ut_varchar2_list := ut_varchar2_list(); - begin - for i in 1..a_cursor_info.count loop - l_column_list.extend; - l_column_list(l_column_list.last) := a_cursor_info(i).access_path; - end loop; - return l_column_list; - end; - - function get_rows_diff_by_sql( - a_act_cursor_info ut_cursor_column_tab, a_exp_cursor_info ut_cursor_column_tab, - a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, - a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false, - a_extract_path varchar2 - ) return tt_row_diffs is - l_act_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_act_cursor_info)); - l_exp_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_exp_cursor_info)); - l_join_xpath varchar2(32767) := ut_utils.to_xpath(a_join_by_list); - l_results tt_row_diffs; - l_sql varchar2(32767); - begin - l_sql := q'[ - with exp as ( - select - exp_item_data, exp_data_id, item_no rn, rownum col_no, pk_value, - s.column_value col, s.column_value.getRootElement() col_name, - nvl(s.column_value.getclobval(),empty_clob()) col_val - from ( - select - exp_data_id, extract( ucd.exp_item_data, :column_path ) exp_item_data, item_no, - replace( extract( ucd.exp_item_data, :join_by ).getclobval(), chr(10) ) pk_value - from ut_compound_data_diff_tmp ucd - where diff_id = :diff_id - and ucd.exp_data_id = :self_guid - ) i, - table( xmlsequence( extract(i.exp_item_data,:extract_path) ) ) s - ), - act as ( - select - act_item_data, act_data_id, item_no rn, rownum col_no, pk_value, - s.column_value col, s.column_value.getRootElement() col_name, - nvl(s.column_value.getclobval(),empty_clob()) col_val - from ( - select - act_data_id, extract( ucd.act_item_data, :column_path ) act_item_data, item_no, - replace( extract( ucd.act_item_data, :join_by ).getclobval(), chr(10) ) pk_value - from ut_compound_data_diff_tmp ucd - where diff_id = :diff_id - and ucd.act_data_id = :other_guid - ) i, - table( xmlsequence( extract(i.act_item_data,:extract_path) ) ) s - ) - select rn, diff_type, diffed_row, pk_value pk_value - from ( - select rn, diff_type, diffed_row, pk_value, - case when diff_type = 'Actual:' then 1 else 2 end rnk, - 1 final_order, - col_name - from ( ]' - || case when a_unordered then q'[ - select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, pk_value, col_name - from ( - select nvl(exp.rn, act.rn) rn, - nvl(exp.pk_value, act.pk_value) pk_value, - exp.col exp_item, - act.col act_item, - nvl(exp.col_name,act.col_name) col_name - from exp - join act - on exp.rn = act.rn and exp.col_name = act.col_name - 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:') ) ]' - else q'[ - select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value, col_name - 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, - max(nvl(exp.col_name,act.col_name)) col_name - from exp exp - join act act - on exp.rn = act.rn and exp.col_name = act.col_name - where dbms_lob.compare(exp.col_val, act.col_val) != 0 - group by (exp.rn, act.rn) - ) - unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') ) ]' - end ||q'[ - ) - union all - select - item_no as rn, - case when exp_data_id is null then 'Extra:' else 'Missing:' end as diff_type, - xmlserialize( - content ( - extract( (case when exp_data_id is null then act_item_data else exp_item_data end),'/*/*') - ) no indent - ) diffed_row, - nvl2( - :join_by, - replace( - extract( case when exp_data_id is null then act_item_data else exp_item_data end, :join_by ).getclobval(), - chr(10) - ), - null - ) pk_value, - case when exp_data_id is null then 1 else 2 end rnk, - 2 final_order, - null col_name - from ut_compound_data_diff_tmp i - where diff_id = :diff_id - and act_data_id is null or exp_data_id is null - ) - order by final_order,]' - ||case when a_enforce_column_order or (not(a_enforce_column_order) and not(a_unordered)) then - q'[ - case when final_order = 1 then rn else rnk end, - case when final_order = 1 then rnk else rn end - ]' - when a_unordered then - q'[ - case when final_order = 1 then col_name else to_char(rnk) end, - case when final_order = 1 then to_char(rn) else col_name end, - case when final_order = 1 then to_char(rnk) else col_name end - ]' - else - null - end; - execute immediate l_sql - bulk collect into l_results - using l_exp_extract_xpath, l_join_xpath, a_diff_id, a_expected_dataset_guid,a_extract_path, - l_act_extract_xpath, l_join_xpath, a_diff_id, a_actual_dataset_guid,a_extract_path, - l_join_xpath, l_join_xpath, a_diff_id; - return l_results; - 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); - end; - - function get_hash(a_data clob, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is - begin - return dbms_crypto.hash(a_data, a_hash_type); - end; - - function get_fixed_size_hash(a_string varchar2, a_base integer :=0,a_size integer := 9999999) return number is - begin - return dbms_utility.get_hash_value(a_string,a_base,a_size); - end; - - procedure insert_diffs_result(a_diff_tab t_diff_tab, a_diff_id raw) is - begin - forall idx in 1..a_diff_tab.count save exceptions - insert into ut_compound_data_diff_tmp - ( diff_id, act_item_data, act_data_id, exp_item_data, exp_data_id, item_no, duplicate_no ) - values - (a_diff_id, - xmlelement( name "ROW", a_diff_tab(idx).act_item_data), a_diff_tab(idx).act_data_id, - xmlelement( name "ROW", a_diff_tab(idx).exp_item_data), a_diff_tab(idx).exp_data_id, - a_diff_tab(idx).item_no, a_diff_tab(idx).dup_no); - exception - when ut_utils.ex_failure_for_all then - raise_application_error(ut_utils.gc_dml_for_all,'Failure to insert a diff tmp data.'); - end; - - procedure set_rows_diff(a_rows_diff integer) is - begin - g_diff_count := a_rows_diff; - end; - - procedure cleanup_diff is - begin - g_diff_count := 0; - end; - - function get_rows_diff_count return integer is - begin - return g_diff_count; - end; - - function is_sql_compare_allowed(a_type_name varchar2) - return boolean is - l_assert boolean; - begin - --clob/blob/xmltype/object/nestedcursor/nestedtable - if a_type_name IN (g_type_name_map(dbms_sql.blob_type), - g_type_name_map(dbms_sql.clob_type), - g_type_name_map(dbms_sql.long_type), - g_type_name_map(dbms_sql.long_raw_type), - g_type_name_map(dbms_sql.bfile_type), - g_anytype_name_map(dbms_types.typecode_namedcollection)) - then - l_assert := false; - else - l_assert := true; - end if; - return l_assert; - end; - - function get_column_type_desc(a_type_code in integer, a_dbms_sql_desc in boolean) - return varchar2 is - begin - return - case - when a_dbms_sql_desc then g_type_name_map(a_type_code) - else g_anytype_name_map(a_type_code) - end; - end; - - function get_compare_cursor(a_diff_cursor_text in clob,a_self_id raw, a_other_id raw) return sys_refcursor is - l_diff_cursor sys_refcursor; - begin - open l_diff_cursor for a_diff_cursor_text using a_self_id, a_other_id; - return l_diff_cursor; - end; - - function create_err_cursor_msg(a_error_stack varchar2) return varchar2 is - begin - return 'SQL exception thrown when fetching data from cursor:'|| - ut_utils.remove_error_from_stack(sqlerrm,ut_utils.gc_xml_processing)||chr(10)|| - ut_expectation_processor.who_called_expectation(a_error_stack)|| - 'Check the query and data for errors.'; - end; - - function type_no_length ( a_type_name varchar2) return boolean is - begin - return case - when g_type_no_length_map.exists(a_type_name) then - true - else - false - end; - end; - -begin - g_anytype_name_map(dbms_types.typecode_date) := 'DATE'; - g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; - g_anytype_name_map(3 /*INTEGER in object type*/) := '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_anytype_name_map(dbms_types.typecode_varray) := 'VARRRAY'; - g_anytype_name_map(dbms_types.typecode_table) := 'TABLE'; - g_anytype_name_map(dbms_types.typecode_namedcollection) := 'NAMEDCOLLECTION'; - g_anytype_name_map(dbms_types.typecode_object) := 'OBJECT'; - - 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'; - g_type_name_map( dbms_sql.user_defined_type ) := 'USER_DEFINED_TYPE'; - g_type_name_map( dbms_sql.ref_type ) := 'REF_TYPE'; - - - /** - * List of types that have no length but can produce a max_len from desc_cursor function. - */ - g_type_no_length_map('ROWID') := 'ROWID'; - g_type_no_length_map('INTERVAL DAY TO SECOND') := 'INTERVAL DAY TO SECOND'; - g_type_no_length_map('INTERVAL YEAR TO MONTH') := 'INTERVAL YEAR TO MONTH'; - g_type_no_length_map('BINARY_DOUBLE') := 'BINARY_DOUBLE'; - g_type_no_length_map('BINARY_FLOAT') := 'BINARY_FLOAT'; -end; -/ +create or replace package body ut_compound_data_helper is + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + g_diff_count integer; + type t_type_name_map is table of varchar2(128) index by binary_integer; + type t_types_no_length is table of varchar2(128) index by varchar2(128); + g_type_name_map t_type_name_map; + g_anytype_name_map t_type_name_map; + g_type_no_length_map t_types_no_length; + + g_compare_sql_template varchar2(4000) := + q'[ + with exp as ( + select + ucd.*, + {:duplicate_number:} dup_no + from ( + select + ucd.item_data + ,x.data_id data_id + ,position + x.item_no item_no + {:columns:} + from {:ut3_owner:}.ut_compound_data_tmp x, + xmltable('/ROWSET/ROW' passing x.item_data columns + item_data xmltype path '*' + ,position for ordinality + {:xml_to_columns:} ) ucd + where data_id = :exp_guid + ) ucd + ) + , act as ( + select + ucd.*, + {:duplicate_number:} dup_no + from ( + select + ucd.item_data + ,x.data_id data_id + ,position + x.item_no item_no + {:columns:} + from {:ut3_owner:}.ut_compound_data_tmp x, + xmltable('/ROWSET/ROW' passing x.item_data columns + item_data xmltype path '*' + ,position for ordinality + {:xml_to_columns:} ) ucd + where data_id = :act_guid + ) ucd + ) + select + a.item_data as act_item_data, + a.data_id act_data_id, + e.item_data as exp_item_data, + e.data_id exp_data_id, + {:item_no:} as item_no, + nvl(e.dup_no,a.dup_no) dup_no + from act a {:join_type:} exp e on ( {:join_condition:} ) + where {:where_condition:}]'; + + function get_columns_diff( + a_expected ut_cursor_column_tab, + a_actual ut_cursor_column_tab, + a_order_enforced boolean := false + ) return tt_column_diffs is + l_results tt_column_diffs; + begin + execute immediate q'[with + expected_cols as ( + select display_path exp_column_name,column_position exp_col_pos, + replace(column_type_name,'VARCHAR2','CHAR') exp_col_type_compare, column_type_name exp_col_type + from table(:a_expected) + where parent_name is null and hierarchy_level = 1 and column_name is not null + ), + actual_cols as ( + select display_path act_column_name,column_position act_col_pos, + replace(column_type_name,'VARCHAR2','CHAR') act_col_type_compare, column_type_name act_col_type + from table(:a_actual) + where parent_name is null and hierarchy_level = 1 and column_name is not null + ), + joined_cols as ( + select e.*,a.*]' + || case when a_order_enforced then ', + row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by a.act_col_pos) a_pos_nn, + row_number() over(partition by case when a.act_col_pos + e.exp_col_pos is not null then 1 end order by e.exp_col_pos) e_pos_nn' + else + null + end ||q'[ + from expected_cols e + full outer join actual_cols a + on e.exp_column_name = a.act_column_name + ) + select case + when exp_col_pos is null and act_col_pos is not null then '+' + when exp_col_pos is not null and act_col_pos is null then '-' + when exp_col_type_compare != act_col_type_compare then 't' + else 'p' + end as diff_type, + exp_column_name, exp_col_type, exp_col_pos, + act_column_name, act_col_type, act_col_pos + from joined_cols + --column is unexpected (extra) or missing + where act_col_pos is null or exp_col_pos is null + --column type is not matching (except CHAR/VARCHAR2) + or act_col_type_compare != exp_col_type_compare]' + || case when a_order_enforced then q'[ + --column position is not matching (both when excluded extra/missing columns as well as when they are included) + or (a_pos_nn != e_pos_nn and exp_col_pos != act_col_pos)]' + else + null + end ||q'[ + order by exp_col_pos, act_col_pos]' + bulk collect into l_results using a_expected, a_actual; + return l_results; + end; + + function generate_not_equal_stmt( + a_data_info ut_cursor_column, a_pk_table ut_varchar2_list + ) return varchar2 + is + l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); + l_index integer; + l_sql_stmt varchar2(32767); + l_exists boolean := false; + begin + l_index := l_pk_tab.first; + if l_pk_tab.count > 0 then + loop + if a_data_info.access_path = l_pk_tab(l_index) then + l_exists := true; + end if; + exit when l_index = l_pk_tab.count or (a_data_info.access_path = l_pk_tab(l_index)); + l_index := a_pk_table.next(l_index); + end loop; + end if; + if not(l_exists) then + l_sql_stmt := ' (decode(a.'||a_data_info.transformed_name||','||' e.'||a_data_info.transformed_name||',1,0) = 0)'; + end if; + return l_sql_stmt; + end; + + function generate_join_by_stmt( + a_data_info ut_cursor_column, a_pk_table ut_varchar2_list + ) return varchar2 + is + l_pk_tab ut_varchar2_list := coalesce(a_pk_table,ut_varchar2_list()); + l_index integer; + l_sql_stmt varchar2(32767); + begin + if l_pk_tab.count <> 0 then + l_index:= l_pk_tab.first; + loop + if l_pk_tab(l_index) in (a_data_info.access_path, a_data_info.parent_name) then + --When then table is nested and join is on whole table + l_sql_stmt := l_sql_stmt ||' a.'||a_data_info.transformed_name||q'[ = ]'||' e.'||a_data_info.transformed_name; + end if; + exit when (a_data_info.access_path = l_pk_tab(l_index)) or l_index = l_pk_tab.count; + l_index := l_pk_tab.next(l_index); + end loop; + end if; + return l_sql_stmt; + end; + + function generate_equal_sql(a_col_name in varchar2) return varchar2 is + begin + return ' decode(a.'||a_col_name||','||' e.'||a_col_name||',1,0) = 1 '; + end; + + function generate_partition_stmt( + a_data_info ut_cursor_column, a_pk_table in ut_varchar2_list, a_alias varchar2 := 'ucd.' + ) return varchar2 + is + l_index integer; + l_sql_stmt varchar2(32767); + begin + if a_pk_table is not empty then + l_index:= a_pk_table.first; + loop + if a_pk_table(l_index) in (a_data_info.access_path, a_data_info.parent_name) then + --When then table is nested and join is on whole table + l_sql_stmt := l_sql_stmt ||a_alias||a_data_info.transformed_name; + end if; + exit when (a_data_info.access_path = a_pk_table(l_index)) or l_index = a_pk_table.count; + l_index := a_pk_table.next(l_index); + end loop; + else + l_sql_stmt := a_alias||a_data_info.transformed_name; + end if; + return l_sql_stmt; + end; + + function generate_select_stmt(a_data_info ut_cursor_column, a_alias varchar2 := 'ucd.') + return varchar2 + is + l_alias varchar2(10) := a_alias; + l_col_syntax varchar2(4000); + l_ut_owner varchar2(250) := ut_utils.ut_owner; + begin + if a_data_info.is_sql_diffable = 0 then + l_col_syntax := l_ut_owner ||'.ut_compound_data_helper.get_hash('||l_alias||a_data_info.transformed_name||'.getClobVal()) as '||a_data_info.transformed_name ; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type = 'DATE' then + l_col_syntax := 'to_date('||l_alias||a_data_info.transformed_name||') as '|| a_data_info.transformed_name; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP') then + l_col_syntax := 'to_timestamp('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_format||''') as '|| a_data_info.transformed_name; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH TIME ZONE') then + l_col_syntax := 'to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') as '|| a_data_info.transformed_name; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP WITH LOCAL TIME ZONE') then + l_col_syntax := ' cast( to_timestamp_tz('||l_alias||a_data_info.transformed_name||','''||ut_utils.gc_timestamp_tz_format||''') AS TIMESTAMP WITH LOCAL TIME ZONE) as '|| a_data_info.transformed_name; + else + l_col_syntax := l_alias||a_data_info.transformed_name||' as '|| a_data_info.transformed_name; + end if; + return l_col_syntax; + end; + + function generate_xmltab_stmt(a_data_info ut_cursor_column) return varchar2 is + l_col_type varchar2(4000); + begin + if a_data_info.is_sql_diffable = 0 then + l_col_type := 'XMLTYPE'; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('DATE','TIMESTAMP','TIMESTAMP WITH TIME ZONE', + 'TIMESTAMP WITH LOCAL TIME ZONE') then + l_col_type := 'VARCHAR2(50)'; + elsif a_data_info.is_sql_diffable = 1 and type_no_length(a_data_info.column_type) then + l_col_type := a_data_info.column_type; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('VARCHAR2','CHAR') then + l_col_type := 'VARCHAR2('||greatest(a_data_info.column_len,4000)||')'; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('NUMBER') then + --We cannot use a precision and scale as dbms_sql.describe_columns3 return precision 0 for dual table + -- there is also no need for that as we not process data but only read and compare as they are stored + l_col_type := a_data_info.column_type; + else + l_col_type := a_data_info.column_type + ||case when a_data_info.column_len is not null + then '('||a_data_info.column_len||')' + else null + end; + end if; + return a_data_info.transformed_name||' '||l_col_type||q'[ PATH ']'||a_data_info.access_path||q'[']'; + end; + + procedure gen_sql_pieces_out_of_cursor( + a_data_info ut_cursor_column_tab, + a_pk_table ut_varchar2_list, + a_unordered boolean, + a_xml_stmt out nocopy clob, + a_select_stmt out nocopy clob, + a_partition_stmt out nocopy clob, + a_join_by_stmt out nocopy clob, + a_not_equal_stmt out nocopy clob + ) is + l_partition_tmp clob; + l_xmltab_list ut_varchar2_list := ut_varchar2_list(); + l_select_list ut_varchar2_list := ut_varchar2_list(); + l_partition_list ut_varchar2_list := ut_varchar2_list(); + l_equal_list ut_varchar2_list := ut_varchar2_list(); + l_join_by_list ut_varchar2_list := ut_varchar2_list(); + l_not_equal_list ut_varchar2_list := ut_varchar2_list(); + + procedure add_element_to_list(a_list in out ut_varchar2_list, a_list_element in varchar2) + is + begin + if a_list_element is not null then + a_list.extend; + a_list(a_list.last) := a_list_element; + end if; + end; + + begin + if a_data_info is not empty then + for i in 1..a_data_info.count loop + if a_data_info(i).has_nested_col = 0 then + --Get XMLTABLE column list + add_element_to_list(l_xmltab_list,generate_xmltab_stmt(a_data_info(i))); + --Get Select statment list of columns + add_element_to_list(l_select_list, generate_select_stmt(a_data_info(i))); + --Get columns by which we partition + add_element_to_list(l_partition_list,generate_partition_stmt(a_data_info(i), a_pk_table)); + --Get equal statement + add_element_to_list(l_equal_list,generate_equal_sql(a_data_info(i).transformed_name)); + --Generate join by stmt + add_element_to_list(l_join_by_list,generate_join_by_stmt(a_data_info(i), a_pk_table)); + --Generate not equal stmt + add_element_to_list(l_not_equal_list,generate_not_equal_stmt(a_data_info(i), a_pk_table)); + end if; + end loop; + + a_xml_stmt := nullif(','||ut_utils.table_to_clob(l_xmltab_list, ' , '),','); + a_select_stmt := nullif(','||ut_utils.table_to_clob(l_select_list, ' , '),','); + l_partition_tmp := ut_utils.table_to_clob(l_partition_list, ' , '); + ut_utils.append_to_clob(a_partition_stmt,' row_number() over (partition by '||l_partition_tmp||' order by '||l_partition_tmp||' ) '); + + if a_pk_table.count > 0 then + -- If key defined do the join or these and where on diffrences + a_join_by_stmt := ut_utils.table_to_clob(l_join_by_list, ' and '); + elsif a_unordered then + -- If no key defined do the join on all columns + a_join_by_stmt := ' e.dup_no = a.dup_no and '||ut_utils.table_to_clob(l_equal_list, ' and '); + else + -- Else join on rownumber + a_join_by_stmt := 'a.item_no = e.item_no '; + end if; + a_not_equal_stmt := ut_utils.table_to_clob(l_not_equal_list, ' or '); + else + --Partition by piece when no data + ut_utils.append_to_clob(a_partition_stmt,' 1 '); + a_join_by_stmt := 'a.item_no = e.item_no '; + end if; + end; + + function gen_compare_sql( + a_other ut_data_value_refcursor, + a_join_by_list ut_varchar2_list, + a_unordered boolean, + a_inclusion_type boolean, + a_is_negated boolean + ) return clob is + l_compare_sql clob; + l_xmltable_stmt clob; + l_select_stmt clob; + l_partition_stmt clob; + l_join_on_stmt clob; + l_not_equal_stmt clob; + l_where_stmt clob; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_join_by_list ut_varchar2_list; + + function get_join_type(a_inclusion_compare in boolean,a_negated in boolean) return varchar2 is + begin + return + case + when a_inclusion_compare and not(a_negated) then ' right outer join ' + when a_inclusion_compare and a_negated then ' inner join ' + else ' full outer join ' + end; + end; + + function get_item_no(a_unordered boolean) return varchar2 is + begin + return + case + when a_unordered then 'row_number() over ( order by nvl(e.item_no,a.item_no))' + else 'nvl(e.item_no,a.item_no) ' + end; + end; + + begin + /** + * We already estabilished cursor equality so now we add anydata root if we compare anydata + * to join by. + */ + l_join_by_list := + case + when a_other is of (ut_data_value_anydata) then ut_utils.add_prefix(a_join_by_list, a_other.cursor_details.get_root) + else a_join_by_list + end; + + dbms_lob.createtemporary(l_compare_sql, true); + --Initiate a SQL template with placeholders + ut_utils.append_to_clob(l_compare_sql, g_compare_sql_template); + --Generate a pieceso of dynamic SQL that will substitute placeholders + gen_sql_pieces_out_of_cursor( + a_other.cursor_details.cursor_columns_info, l_join_by_list, a_unordered, + l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_join_on_stmt, + l_not_equal_stmt + ); + + l_compare_sql := replace(l_compare_sql,'{:duplicate_number:}',l_partition_stmt); + l_compare_sql := replace(l_compare_sql,'{:columns:}',l_select_stmt); + l_compare_sql := replace(l_compare_sql,'{:ut3_owner:}',l_ut_owner); + l_compare_sql := replace(l_compare_sql,'{:xml_to_columns:}',l_xmltable_stmt); + l_compare_sql := replace(l_compare_sql,'{:item_no:}',get_item_no(a_unordered)); + l_compare_sql := replace(l_compare_sql,'{:join_type:}',get_join_type(a_inclusion_type,a_is_negated)); + l_compare_sql := replace(l_compare_sql,'{:join_condition:}',l_join_on_stmt); + + if l_not_equal_stmt is not null and ((l_join_by_list.count > 0 and not a_is_negated) or (not a_unordered)) then + ut_utils.append_to_clob(l_where_stmt,' ( '||l_not_equal_stmt||' ) or '); + end if; + --If its inclusion we expect a actual set to fully match and have no extra elements over expected + if a_inclusion_type then + ut_utils.append_to_clob(l_where_stmt,case when a_is_negated then ' 1 = 1 ' else ' ( a.data_id is null ) ' end); + else + ut_utils.append_to_clob(l_where_stmt,' (a.data_id is null or e.data_id is null) '); + end if; + + l_compare_sql := replace(l_compare_sql,'{:where_condition:}',l_where_stmt); + return l_compare_sql; + end; + + function get_column_extract_path(a_cursor_info ut_cursor_column_tab) return ut_varchar2_list is + l_column_list ut_varchar2_list := ut_varchar2_list(); + begin + for i in 1..a_cursor_info.count loop + l_column_list.extend; + l_column_list(l_column_list.last) := a_cursor_info(i).access_path; + end loop; + return l_column_list; + end; + + function get_rows_diff_by_sql( + a_act_cursor_info ut_cursor_column_tab, a_exp_cursor_info ut_cursor_column_tab, + a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, + a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false, + a_extract_path varchar2 + ) return tt_row_diffs is + l_act_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_act_cursor_info)); + l_exp_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_exp_cursor_info)); + l_join_xpath varchar2(32767) := ut_utils.to_xpath(a_join_by_list); + l_results tt_row_diffs; + l_sql varchar2(32767); + begin + l_sql := q'[ + with exp as ( + select + exp_item_data, exp_data_id, item_no rn, rownum col_no, pk_value, + s.column_value col, s.column_value.getRootElement() col_name, + nvl(s.column_value.getclobval(),empty_clob()) col_val + from ( + select + exp_data_id, extract( ucd.exp_item_data, :column_path ) exp_item_data, item_no, + replace( extract( ucd.exp_item_data, :join_by ).getclobval(), chr(10) ) pk_value + from ut_compound_data_diff_tmp ucd + where diff_id = :diff_id + and ucd.exp_data_id = :self_guid + ) i, + table( xmlsequence( extract(i.exp_item_data,:extract_path) ) ) s + ), + act as ( + select + act_item_data, act_data_id, item_no rn, rownum col_no, pk_value, + s.column_value col, s.column_value.getRootElement() col_name, + nvl(s.column_value.getclobval(),empty_clob()) col_val + from ( + select + act_data_id, extract( ucd.act_item_data, :column_path ) act_item_data, item_no, + replace( extract( ucd.act_item_data, :join_by ).getclobval(), chr(10) ) pk_value + from ut_compound_data_diff_tmp ucd + where diff_id = :diff_id + and ucd.act_data_id = :other_guid + ) i, + table( xmlsequence( extract(i.act_item_data,:extract_path) ) ) s + ) + select rn, diff_type, diffed_row, pk_value pk_value + from ( + select rn, diff_type, diffed_row, pk_value, + case when diff_type = 'Actual:' then 1 else 2 end rnk, + 1 final_order, + col_name + from ( ]' + || case when a_unordered then q'[ + select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, pk_value, col_name + from ( + select nvl(exp.rn, act.rn) rn, + nvl(exp.pk_value, act.pk_value) pk_value, + exp.col exp_item, + act.col act_item, + nvl(exp.col_name,act.col_name) col_name + from exp + join act + on exp.rn = act.rn and exp.col_name = act.col_name + 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:') ) ]' + else q'[ + select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value, col_name + 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, + max(nvl(exp.col_name,act.col_name)) col_name + from exp exp + join act act + on exp.rn = act.rn and exp.col_name = act.col_name + where dbms_lob.compare(exp.col_val, act.col_val) != 0 + group by (exp.rn, act.rn) + ) + unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') ) ]' + end ||q'[ + ) + union all + select + item_no as rn, + case when exp_data_id is null then 'Extra:' else 'Missing:' end as diff_type, + xmlserialize( + content ( + extract( (case when exp_data_id is null then act_item_data else exp_item_data end),'/*/*') + ) no indent + ) diffed_row, + nvl2( + :join_by, + replace( + extract( case when exp_data_id is null then act_item_data else exp_item_data end, :join_by ).getclobval(), + chr(10) + ), + null + ) pk_value, + case when exp_data_id is null then 1 else 2 end rnk, + 2 final_order, + null col_name + from ut_compound_data_diff_tmp i + where diff_id = :diff_id + and act_data_id is null or exp_data_id is null + ) + order by final_order,]' + ||case when a_enforce_column_order or (not(a_enforce_column_order) and not(a_unordered)) then + q'[ + case when final_order = 1 then rn else rnk end, + case when final_order = 1 then rnk else rn end + ]' + when a_unordered then + q'[ + case when final_order = 1 then col_name else to_char(rnk) end, + case when final_order = 1 then to_char(rn) else col_name end, + case when final_order = 1 then to_char(rnk) else col_name end + ]' + else + null + end; + execute immediate l_sql + bulk collect into l_results + using l_exp_extract_xpath, l_join_xpath, a_diff_id, a_expected_dataset_guid,a_extract_path, + l_act_extract_xpath, l_join_xpath, a_diff_id, a_actual_dataset_guid,a_extract_path, + l_join_xpath, l_join_xpath, a_diff_id; + return l_results; + 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); + end; + + function get_hash(a_data clob, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is + begin + return dbms_crypto.hash(a_data, a_hash_type); + end; + + function get_fixed_size_hash(a_string varchar2, a_base integer :=0,a_size integer := 9999999) return number is + begin + return dbms_utility.get_hash_value(a_string,a_base,a_size); + end; + + procedure insert_diffs_result(a_diff_tab t_diff_tab, a_diff_id raw) is + begin + forall idx in 1..a_diff_tab.count save exceptions + insert into ut_compound_data_diff_tmp + ( diff_id, act_item_data, act_data_id, exp_item_data, exp_data_id, item_no, duplicate_no ) + values + (a_diff_id, + xmlelement( name "ROW", a_diff_tab(idx).act_item_data), a_diff_tab(idx).act_data_id, + xmlelement( name "ROW", a_diff_tab(idx).exp_item_data), a_diff_tab(idx).exp_data_id, + a_diff_tab(idx).item_no, a_diff_tab(idx).dup_no); + exception + when ut_utils.ex_failure_for_all then + raise_application_error(ut_utils.gc_dml_for_all,'Failure to insert a diff tmp data.'); + end; + + procedure set_rows_diff(a_rows_diff integer) is + begin + g_diff_count := a_rows_diff; + end; + + procedure cleanup_diff is + begin + g_diff_count := 0; + end; + + function get_rows_diff_count return integer is + begin + return g_diff_count; + end; + + function is_sql_compare_allowed(a_type_name varchar2) + return boolean is + l_assert boolean; + begin + --clob/blob/xmltype/object/nestedcursor/nestedtable + if a_type_name IN (g_type_name_map(dbms_sql.blob_type), + g_type_name_map(dbms_sql.clob_type), + g_type_name_map(dbms_sql.long_type), + g_type_name_map(dbms_sql.long_raw_type), + g_type_name_map(dbms_sql.bfile_type), + g_anytype_name_map(dbms_types.typecode_namedcollection)) + then + l_assert := false; + else + l_assert := true; + end if; + return l_assert; + end; + + function get_column_type_desc(a_type_code in integer, a_dbms_sql_desc in boolean) + return varchar2 is + begin + return + case + when a_dbms_sql_desc then g_type_name_map(a_type_code) + else g_anytype_name_map(a_type_code) + end; + end; + + function get_compare_cursor(a_diff_cursor_text in clob,a_self_id raw, a_other_id raw) return sys_refcursor is + l_diff_cursor sys_refcursor; + begin + open l_diff_cursor for a_diff_cursor_text using a_self_id, a_other_id; + return l_diff_cursor; + end; + + function create_err_cursor_msg(a_error_stack varchar2) return varchar2 is + begin + return 'SQL exception thrown when fetching data from cursor:'|| + ut_utils.remove_error_from_stack(sqlerrm,ut_utils.gc_xml_processing)||chr(10)|| + ut_expectation_processor.who_called_expectation(a_error_stack)|| + 'Check the query and data for errors.'; + end; + + function type_no_length ( a_type_name varchar2) return boolean is + begin + return case + when g_type_no_length_map.exists(a_type_name) then + true + else + false + end; + end; + +begin + g_anytype_name_map(dbms_types.typecode_date) := 'DATE'; + g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; + g_anytype_name_map(3 /*INTEGER in object type*/) := '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_anytype_name_map(dbms_types.typecode_varray) := 'VARRRAY'; + g_anytype_name_map(dbms_types.typecode_table) := 'TABLE'; + g_anytype_name_map(dbms_types.typecode_namedcollection) := 'NAMEDCOLLECTION'; + g_anytype_name_map(dbms_types.typecode_object) := 'OBJECT'; + + 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'; + g_type_name_map( dbms_sql.user_defined_type ) := 'USER_DEFINED_TYPE'; + g_type_name_map( dbms_sql.ref_type ) := 'REF_TYPE'; + + + /** + * List of types that have no length but can produce a max_len from desc_cursor function. + */ + g_type_no_length_map('ROWID') := 'ROWID'; + g_type_no_length_map('INTERVAL DAY TO SECOND') := 'INTERVAL DAY TO SECOND'; + g_type_no_length_map('INTERVAL YEAR TO MONTH') := 'INTERVAL YEAR TO MONTH'; + g_type_no_length_map('BINARY_DOUBLE') := 'BINARY_DOUBLE'; + g_type_no_length_map('BINARY_FLOAT') := 'BINARY_FLOAT'; +end; +/ diff --git a/source/expectations/data_values/ut_cursor_column.tps b/source/expectations/data_values/ut_cursor_column.tps index da3c004f2..93114c431 100644 --- a/source/expectations/data_values/ut_cursor_column.tps +++ b/source/expectations/data_values/ut_cursor_column.tps @@ -1,52 +1,52 @@ -create or replace type ut_cursor_column authid current_user as object ( - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - parent_name varchar2(4000), - access_path varchar2(4000), - filter_path varchar2(4000), - display_path varchar2(4000), - has_nested_col number(1,0), - transformed_name varchar2(2000), - hierarchy_level number, - column_position number, - xml_valid_name varchar2(2000), - column_name varchar2(2000), - column_type varchar2(128), - column_type_name varchar2(128), - column_schema varchar2(128), - column_len integer, - column_precision integer, - column_scale integer, - is_sql_diffable number(1, 0), - is_collection number(1, 0), - - member procedure init(self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer), - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column, - a_col_name varchar2, a_col_schema_name varchar2, - a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, - a_col_position integer, a_col_type in varchar2, a_collection integer, a_access_path in varchar2, a_col_precision in integer, - a_col_scale integer) - return self as result, - - constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result -) -/ +create or replace type ut_cursor_column authid current_user as object ( + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + parent_name varchar2(4000), + access_path varchar2(4000), + filter_path varchar2(4000), + display_path varchar2(4000), + has_nested_col number(1,0), + transformed_name varchar2(2000), + hierarchy_level number, + column_position number, + xml_valid_name varchar2(2000), + column_name varchar2(2000), + column_type varchar2(128), + column_type_name varchar2(128), + column_schema varchar2(128), + column_len integer, + column_precision integer, + column_scale integer, + is_sql_diffable number(1, 0), + is_collection number(1, 0), + + member procedure init(self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer,a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer), + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column, + a_col_name varchar2, a_col_schema_name varchar2, + a_col_type_name varchar2, a_col_max_len integer, a_parent_name varchar2 := null, a_hierarchy_level integer := 1, + a_col_position integer, a_col_type in varchar2, a_collection integer, a_access_path in varchar2, a_col_precision in integer, + a_col_scale integer) + return self as result, + + constructor function ut_cursor_column( self in out nocopy ut_cursor_column) return self as result +) +/ diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb index 0c617a226..20313ed96 100644 --- a/source/expectations/data_values/ut_cursor_details.tpb +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -1,257 +1,256 @@ -create or replace type body ut_cursor_details as - - member function equals( a_other ut_cursor_details, a_match_options ut_matcher_options ) return boolean is - l_diffs integer; - begin - select count(1) into l_diffs - from table(self.cursor_columns_info) a - full outer join table(a_other.cursor_columns_info) e - on decode(a.parent_name,e.parent_name,1,0)= 1 - and a.column_name = e.column_name - and replace(a.column_type,'VARCHAR2','CHAR') = replace(e.column_type,'VARCHAR2','CHAR') - and ( a.column_position = e.column_position or a_match_options.columns_are_unordered_flag = 1 ) - where a.column_name is null or e.column_name is null; - return l_diffs = 0; - end; - - member procedure desc_compound_data( - self in out nocopy ut_cursor_details, a_compound_data anytype, - a_parent_name in varchar2, a_level in integer, a_access_path in varchar2 - ) is - l_idx pls_integer := 1; - l_elements_info ut_metadata.t_anytype_members_rec; - l_element_info ut_metadata.t_anytype_elem_info_rec; - l_is_collection boolean; - begin - l_elements_info := ut_metadata.get_anytype_members_info( a_compound_data ); - l_is_collection := ut_metadata.is_collection(l_elements_info.type_code); - if l_elements_info.elements_count is null then - l_element_info := ut_metadata.get_attr_elem_info( a_compound_data ); - self.cursor_columns_info.extend; - self.cursor_columns_info(cursor_columns_info.last) := - ut_cursor_column( - l_elements_info.type_name, - l_elements_info.schema_name, - null, - l_elements_info.length, - a_parent_name, - a_level, - l_idx, - ut_compound_data_helper.get_column_type_desc(l_elements_info.type_code,false), - ut_utils.boolean_to_int(l_is_collection), - a_access_path, - l_elements_info.precision, - l_elements_info.scale - ); - if l_element_info.attr_elt_type is not null then - desc_compound_data( - l_element_info.attr_elt_type, l_elements_info.type_name, - a_level + 1, a_access_path || '/' || l_elements_info.type_name - ); - end if; - else - while l_idx <= l_elements_info.elements_count loop - l_element_info := ut_metadata.get_attr_elem_info( a_compound_data, l_idx ); - - self.cursor_columns_info.extend; - self.cursor_columns_info(cursor_columns_info.last) := - ut_cursor_column( - l_element_info.attribute_name, - l_elements_info.schema_name, - null, - l_element_info.length, - a_parent_name, - a_level, - l_idx, - ut_compound_data_helper.get_column_type_desc(l_element_info.type_code,false), - ut_utils.boolean_to_int(l_is_collection), - a_access_path, - l_elements_info.precision, - l_elements_info.scale - ); - if l_element_info.attr_elt_type is not null then - desc_compound_data( - l_element_info.attr_elt_type, l_element_info.attribute_name, - a_level + 1, a_access_path || '/' || l_element_info.attribute_name - ); - end if; - l_idx := l_idx + 1; - end loop; - end if; - end; - - constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result is - begin - self.cursor_columns_info := ut_cursor_column_tab(); - return; - end; - - constructor function ut_cursor_details( - self in out nocopy ut_cursor_details, - a_cursor_number in number - ) return self as result is - l_columns_count pls_integer; - l_columns_desc dbms_sql.desc_tab3; - l_is_collection boolean; - l_hierarchy_level integer := 1; - begin - self.cursor_columns_info := ut_cursor_column_tab(); - self.is_anydata := 0; - dbms_sql.describe_columns3(a_cursor_number, l_columns_count, l_columns_desc); - - /** - * Due to a bug with object being part of cursor in ANYDATA scenario - * oracle fails to revert number to cursor. We ar using dbms_sql.close cursor to close it - * to avoid leaving open cursors behind. - * a_cursor := dbms_sql.to_refcursor(l_cursor_number); - **/ - for pos in 1 .. l_columns_count loop - l_is_collection := ut_metadata.is_collection( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ); - self.cursor_columns_info.extend; - self.cursor_columns_info(self.cursor_columns_info.last) := - ut_cursor_column( - l_columns_desc(pos).col_name, - l_columns_desc(pos).col_schema_name, - l_columns_desc(pos).col_type_name, - l_columns_desc(pos).col_max_len, - null, - l_hierarchy_level, - pos, - ut_compound_data_helper.get_column_type_desc(l_columns_desc(pos).col_type,true), - ut_utils.boolean_to_int(l_is_collection), - null, - l_columns_desc(pos).col_precision, - l_columns_desc(pos).col_scale - ); - - if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then - desc_compound_data( - ut_metadata.get_user_defined_type( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ), - l_columns_desc(pos).col_name, - l_hierarchy_level + 1, - l_columns_desc(pos).col_name - ); - end if; - end loop; - return; - end; - - member function contains_collection return boolean is - l_collection_elements number; - begin - select count(1) into l_collection_elements - from table(cursor_columns_info) c - where c.is_collection = 1 and rownum = 1; - return l_collection_elements > 0; - end; - - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is - l_result ut_varchar2_list; - begin - --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') - select fl.column_value - bulk collect into l_result - from table(a_expected_columns) fl - where not exists ( - select 1 from table(self.cursor_columns_info) c - where regexp_like(c.filter_path,'^/?'||fl.column_value||'($|/.*)' ) - ) - order by fl.column_value; - return l_result; - end; - - member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options) is - l_result ut_cursor_details := self; - l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); - l_column ut_cursor_column; - c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; - begin - if l_result.cursor_columns_info is not null then - - --limit columns to those on the include items minus exclude items - if a_match_options.include.items.count > 0 then - -- if include - exclude = 0 then keep all columns - if a_match_options.include.items != a_match_options.exclude.items then - with included_columns as ( - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.include.items) - minus - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.exclude.items) - ) - select value(x) - bulk collect into l_result.cursor_columns_info - from table(self.cursor_columns_info) x - where exists( - select 1 from included_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) - ) - or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; - end if; - elsif a_match_options.exclude.items.count > 0 then - with excluded_columns as ( - select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names - from table(a_match_options.exclude.items) - ) - select value(x) - bulk collect into l_result.cursor_columns_info - from table(self.cursor_columns_info) x - where not exists( - select 1 from excluded_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) - ); - end if; - - --Rewrite column order after columns been excluded - for i in ( - select parent_name, access_path, display_path, has_nested_col, - transformed_name, hierarchy_level, - rownum as new_position, xml_valid_name, - column_name, column_type, column_type_name, column_schema, - column_len, column_precision ,column_scale ,is_sql_diffable, is_collection,value(x) col_info - from table(l_result.cursor_columns_info) x - order by x.column_position asc - ) loop - l_column := i.col_info; - l_column.column_position := i.new_position; - l_column_tab.extend; - l_column_tab(l_column_tab.last) := l_column; - end loop; - - l_result.cursor_columns_info := l_column_tab; - self := l_result; - end if; - end; - - member function get_xml_children(a_parent_name varchar2 := null) return xmltype is - l_result xmltype; - begin - select xmlagg(xmlelement(evalname t.column_name,t.column_type_name)) - into l_result - from table(self.cursor_columns_info) t - where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) - having count(*) > 0; - return l_result; - end; - - member function get_root return varchar2 is - l_root varchar2(250); - begin - if self.cursor_columns_info.count > 0 then - select x.access_path into l_root from table(self.cursor_columns_info) x - where x.hierarchy_level = 1; - else - l_root := null; - end if; - return l_root; - end; - - member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) is - l_root varchar2(250) := get_root(); - begin - self.is_anydata := 1; - for i in 1..cursor_columns_info.count loop - self.cursor_columns_info(i).filter_path := '/'||ut_utils.strip_prefix(self.cursor_columns_info(i).access_path,l_root); - end loop; - end; - -end; -/ +create or replace type body ut_cursor_details as + + member function equals( a_other ut_cursor_details, a_match_options ut_matcher_options ) return boolean is + l_diffs integer; + begin + select count(1) into l_diffs + from table(self.cursor_columns_info) a + full outer join table(a_other.cursor_columns_info) e + on decode(a.parent_name,e.parent_name,1,0)= 1 + and a.column_name = e.column_name + and replace(a.column_type,'VARCHAR2','CHAR') = replace(e.column_type,'VARCHAR2','CHAR') + and ( a.column_position = e.column_position or a_match_options.columns_are_unordered_flag = 1 ) + where a.column_name is null or e.column_name is null; + return l_diffs = 0; + end; + + member procedure desc_compound_data( + self in out nocopy ut_cursor_details, a_compound_data anytype, + a_parent_name in varchar2, a_level in integer, a_access_path in varchar2 + ) is + l_idx pls_integer := 1; + l_elements_info ut_metadata.t_anytype_members_rec; + l_element_info ut_metadata.t_anytype_elem_info_rec; + l_is_collection boolean; + begin + l_elements_info := ut_metadata.get_anytype_members_info( a_compound_data ); + l_is_collection := ut_metadata.is_collection(l_elements_info.type_code); + if l_elements_info.elements_count is null then + l_element_info := ut_metadata.get_attr_elem_info( a_compound_data ); + self.cursor_columns_info.extend; + self.cursor_columns_info(cursor_columns_info.last) := + ut_cursor_column( + l_elements_info.type_name, + l_elements_info.schema_name, + null, + l_elements_info.length, + a_parent_name, + a_level, + l_idx, + ut_compound_data_helper.get_column_type_desc(l_elements_info.type_code,false), + ut_utils.boolean_to_int(l_is_collection), + a_access_path, + l_elements_info.precision, + l_elements_info.scale + ); + if l_element_info.attr_elt_type is not null then + desc_compound_data( + l_element_info.attr_elt_type, l_elements_info.type_name, + a_level + 1, a_access_path || '/' || l_elements_info.type_name + ); + end if; + else + while l_idx <= l_elements_info.elements_count loop + l_element_info := ut_metadata.get_attr_elem_info( a_compound_data, l_idx ); + + self.cursor_columns_info.extend; + self.cursor_columns_info(cursor_columns_info.last) := + ut_cursor_column( + l_element_info.attribute_name, + l_elements_info.schema_name, + null, + l_element_info.length, + a_parent_name, + a_level, + l_idx, + ut_compound_data_helper.get_column_type_desc(l_element_info.type_code,false), + ut_utils.boolean_to_int(l_is_collection), + a_access_path, + l_elements_info.precision, + l_elements_info.scale + ); + if l_element_info.attr_elt_type is not null then + desc_compound_data( + l_element_info.attr_elt_type, l_element_info.attribute_name, + a_level + 1, a_access_path || '/' || l_element_info.attribute_name + ); + end if; + l_idx := l_idx + 1; + end loop; + end if; + end; + + constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result is + begin + self.cursor_columns_info := ut_cursor_column_tab(); + return; + end; + + constructor function ut_cursor_details( + self in out nocopy ut_cursor_details, + a_cursor_number in number + ) return self as result is + l_columns_count pls_integer; + l_columns_desc dbms_sql.desc_tab3; + l_is_collection boolean; + l_hierarchy_level integer := 1; + begin + self.cursor_columns_info := ut_cursor_column_tab(); + self.is_anydata := 0; + dbms_sql.describe_columns3(a_cursor_number, l_columns_count, l_columns_desc); + + /** + * Due to a bug with object being part of cursor in ANYDATA scenario + * oracle fails to revert number to cursor. We ar using dbms_sql.close cursor to close it + * to avoid leaving open cursors behind. + * a_cursor := dbms_sql.to_refcursor(l_cursor_number); + **/ + for pos in 1 .. l_columns_count loop + l_is_collection := ut_metadata.is_collection( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ); + self.cursor_columns_info.extend; + self.cursor_columns_info(self.cursor_columns_info.last) := + ut_cursor_column( + l_columns_desc(pos).col_name, + l_columns_desc(pos).col_schema_name, + l_columns_desc(pos).col_type_name, + l_columns_desc(pos).col_max_len, + null, + l_hierarchy_level, + pos, + ut_compound_data_helper.get_column_type_desc(l_columns_desc(pos).col_type,true), + ut_utils.boolean_to_int(l_is_collection), + null, + l_columns_desc(pos).col_precision, + l_columns_desc(pos).col_scale + ); + + if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then + desc_compound_data( + ut_metadata.get_user_defined_type( l_columns_desc(pos).col_schema_name, l_columns_desc(pos).col_type_name ), + l_columns_desc(pos).col_name, + l_hierarchy_level + 1, + l_columns_desc(pos).col_name + ); + end if; + end loop; + return; + end; + + member function contains_collection return boolean is + l_collection_elements number; + begin + select count(1) into l_collection_elements + from table(cursor_columns_info) c + where c.is_collection = 1 and rownum = 1; + return l_collection_elements > 0; + end; + + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list is + l_result ut_varchar2_list; + begin + --regexp_replace(c.access_path,'^\/?([^\/]+\/){1}') + select fl.column_value + bulk collect into l_result + from table(a_expected_columns) fl + where not exists ( + select 1 from table(self.cursor_columns_info) c + where regexp_like(c.filter_path,'^/?'||fl.column_value||'($|/.*)' ) + ) + order by fl.column_value; + return l_result; + end; + + member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options) is + l_result ut_cursor_details := self; + l_column_tab ut_cursor_column_tab := ut_cursor_column_tab(); + l_column ut_cursor_column; + c_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; + begin + if l_result.cursor_columns_info is not null then + + --limit columns to those on the include items minus exclude items + if a_match_options.include.items.count > 0 then + -- if include - exclude = 0 then keep all columns + if a_match_options.include.items != a_match_options.exclude.items then + with included_columns as ( + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.include.items) + minus + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.exclude.items) + ) + select value(x) + bulk collect into l_result.cursor_columns_info + from table(self.cursor_columns_info) x + where exists( + select 1 from included_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) + ) + or x.hierarchy_level = case when self.is_anydata = 1 then 1 else 0 end ; + end if; + elsif a_match_options.exclude.items.count > 0 then + with excluded_columns as ( + select regexp_replace( column_value, c_xpath_extract_reg, '\5' ) col_names + from table(a_match_options.exclude.items) + ) + select value(x) + bulk collect into l_result.cursor_columns_info + from table(self.cursor_columns_info) x + where not exists( + select 1 from excluded_columns f where regexp_like(x.filter_path,'^/?'||f.col_names||'($|/.*)' ) + ); + end if; + + --Rewrite column order after columns been excluded + for i in ( + select parent_name, access_path, display_path, has_nested_col, + transformed_name, hierarchy_level, + rownum as new_position, xml_valid_name, + column_name, column_type, column_type_name, column_schema, + column_len, column_precision ,column_scale ,is_sql_diffable, is_collection,value(x) col_info + from table(l_result.cursor_columns_info) x + order by x.column_position asc + ) loop + l_column := i.col_info; + l_column.column_position := i.new_position; + l_column_tab.extend; + l_column_tab(l_column_tab.last) := l_column; + end loop; + + l_result.cursor_columns_info := l_column_tab; + self := l_result; + end if; + end; + + member function get_xml_children(a_parent_name varchar2 := null) return xmltype is + l_result xmltype; + begin + select xmlagg(xmlelement(evalname t.column_name,t.column_type_name)) + into l_result + from table(self.cursor_columns_info) t + where (a_parent_name is null and parent_name is null and hierarchy_level = 1 and column_name is not null) + having count(*) > 0; + return l_result; + end; + + member function get_root return varchar2 is + l_root varchar2(250); + begin + if self.cursor_columns_info.count > 0 then + select x.access_path into l_root from table(self.cursor_columns_info) x + where x.hierarchy_level = 1; + else + l_root := null; + end if; + return l_root; + end; + + member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) is + l_root varchar2(250) := get_root(); + begin + self.is_anydata := 1; + for i in 1..cursor_columns_info.count loop + self.cursor_columns_info(i).filter_path := '/'||ut_utils.strip_prefix(self.cursor_columns_info(i).access_path,l_root); + end loop; + end; +end; +/ diff --git a/source/expectations/data_values/ut_cursor_details.tps b/source/expectations/data_values/ut_cursor_details.tps index e6c80a3b5..43e70b123 100644 --- a/source/expectations/data_values/ut_cursor_details.tps +++ b/source/expectations/data_values/ut_cursor_details.tps @@ -1,41 +1,41 @@ -create or replace type ut_cursor_details authid current_user as object ( - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - cursor_columns_info ut_cursor_column_tab, - - /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ - is_anydata number(1,0), - constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, - constructor function ut_cursor_details( - self in out nocopy ut_cursor_details,a_cursor_number in number - ) return self as result, - member function equals(a_other ut_cursor_details, a_match_options ut_matcher_options) return boolean, - member procedure desc_compound_data( - self in out nocopy ut_cursor_details, - a_compound_data anytype, - a_parent_name in varchar2, - a_level in integer, - a_access_path in varchar2 - ), - member function contains_collection return boolean, - member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, - member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), - member function get_xml_children(a_parent_name varchar2 := null) return xmltype, - member function get_root return varchar2, - member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) -) -/ +create or replace type ut_cursor_details authid current_user as object ( + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + cursor_columns_info ut_cursor_column_tab, + + /*if type is anydata we need to skip level 1 on joinby / inlude / exclude as its artificial cursor*/ + is_anydata number(1,0), + constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result, + constructor function ut_cursor_details( + self in out nocopy ut_cursor_details,a_cursor_number in number + ) return self as result, + member function equals(a_other ut_cursor_details, a_match_options ut_matcher_options) return boolean, + member procedure desc_compound_data( + self in out nocopy ut_cursor_details, + a_compound_data anytype, + a_parent_name in varchar2, + a_level in integer, + a_access_path in varchar2 + ), + member function contains_collection return boolean, + member function get_missing_join_by_columns( a_expected_columns ut_varchar2_list ) return ut_varchar2_list, + member procedure filter_columns(self in out nocopy ut_cursor_details, a_match_options ut_matcher_options), + member function get_xml_children(a_parent_name varchar2 := null) return xmltype, + member function get_root return varchar2, + member procedure strip_root_from_anydata(self in out nocopy ut_cursor_details) +) +/ diff --git a/source/expectations/data_values/ut_data_value_anydata.tpb b/source/expectations/data_values/ut_data_value_anydata.tpb index 808e52197..fe9a6a1bc 100644 --- a/source/expectations/data_values/ut_data_value_anydata.tpb +++ b/source/expectations/data_values/ut_data_value_anydata.tpb @@ -1,143 +1,142 @@ -create or replace type body ut_data_value_anydata as - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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. - */ - - overriding member function get_object_info return varchar2 is - begin - return self.data_type || case when self.compound_type = 'collection' then ' [ count = '||self.elements_count||' ]' else null end; - end; - - member function get_extract_path(a_data_value anydata) return varchar2 is - l_path varchar2(10); - begin - if self.compound_type = 'object' then - l_path := '/*/*'; - else - case when ut_metadata.has_collection_members(a_data_value) then - l_path := '/*/*'; - else - l_path := '/*'; - end case; - end if; - return l_path; - end; - - member function get_cursor_sql_from_anydata(a_data_value anydata) return varchar2 is - l_cursor_sql varchar2(32767); - begin - l_cursor_sql := ' - declare - l_data '||self.data_type||'; - l_value anydata := :a_value; - l_status integer; - l_tmp_refcursor sys_refcursor; - begin - l_status := l_value.get'||self.compound_type||'(l_data); '|| - case when self.compound_type = 'collection' then - q'[ open :l_tmp_refcursor for select value(x) as "]'|| - ut_metadata.get_object_name(ut_metadata.get_collection_element(a_data_value))|| - q'[" from table(l_data) x;]' - else - q'[ open :l_tmp_refcursor for select l_data as "]'||ut_metadata.get_object_name(self.data_type)|| - q'[" from dual;]' - end || - 'end;'; - return l_cursor_sql; - end; - - member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata) is - l_refcursor sys_refcursor; - cursor_not_open exception; - l_cursor_number number; - l_anydata_sql varchar2(32767); - begin - self.data_type := ut_metadata.get_anydata_typename(a_value); - self.compound_type := get_instance(a_value); - self.is_data_null := ut_metadata.is_anytype_null(a_value,self.compound_type); - self.data_id := sys_guid(); - self.self_type := $$plsql_unit; - self.cursor_details := ut_cursor_details(); - - ut_compound_data_helper.cleanup_diff; - - if not self.is_null() then - self.extract_path := get_extract_path(a_value); - l_anydata_sql := get_cursor_sql_from_anydata(a_value); - execute immediate l_anydata_sql using in a_value, in out l_refcursor; - if l_refcursor%isopen then - self.extract_cursor(l_refcursor); - l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); - self.cursor_details := ut_cursor_details(l_cursor_number); - self.cursor_details.strip_root_from_anydata; - dbms_sql.close_cursor(l_cursor_number); - elsif not l_refcursor%isopen then - raise cursor_not_open; - end if; - end if; - exception - when cursor_not_open then - raise_application_error(-20155, 'Cursor is not open'); - when others then - if l_refcursor%isopen then - close l_refcursor; - end if; - raise; - end; - - member function get_instance(a_data_value anydata) return varchar2 is - l_result varchar2(30); - begin - l_result := ut_metadata.get_anydata_compound_type(a_data_value); - if l_result not in ('object','collection') then - raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL'); - end if; - return l_result; - end; - - constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata, a_value anydata) return self as result - is - begin - init(a_value); - return; - end; - - overriding member function compare_implementation( - a_other ut_data_value, - a_match_options ut_matcher_options, - a_inclusion_compare boolean := false, - a_is_negated boolean := false - ) return integer is - l_result integer := 0; - begin - if not a_other is of (ut_data_value_anydata) then - raise value_error; - end if; - l_result := l_result + (self as ut_data_value_refcursor).compare_implementation(a_other,a_match_options,a_inclusion_compare,a_is_negated); - return l_result; - end; - - overriding member function is_empty return boolean is - begin - if self.compound_type = 'collection' then - return self.elements_count = 0; - else - raise value_error; - end if; - end; - -end; -/ +create or replace type body ut_data_value_anydata as + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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. + */ + + overriding member function get_object_info return varchar2 is + begin + return self.data_type || case when self.compound_type = 'collection' then ' [ count = '||self.elements_count||' ]' else null end; + end; + + member function get_extract_path(a_data_value anydata) return varchar2 is + l_path varchar2(10); + begin + if self.compound_type = 'object' then + l_path := '/*/*'; + else + case when ut_metadata.has_collection_members(a_data_value) then + l_path := '/*/*'; + else + l_path := '/*'; + end case; + end if; + return l_path; + end; + + member function get_cursor_sql_from_anydata(a_data_value anydata) return varchar2 is + l_cursor_sql varchar2(32767); + begin + l_cursor_sql := ' + declare + l_data '||self.data_type||'; + l_value anydata := :a_value; + l_status integer; + l_tmp_refcursor sys_refcursor; + begin + l_status := l_value.get'||self.compound_type||'(l_data); '|| + case when self.compound_type = 'collection' then + q'[ open :l_tmp_refcursor for select value(x) as "]'|| + ut_metadata.get_object_name(ut_metadata.get_collection_element(a_data_value))|| + q'[" from table(l_data) x;]' + else + q'[ open :l_tmp_refcursor for select l_data as "]'||ut_metadata.get_object_name(self.data_type)|| + q'[" from dual;]' + end || + 'end;'; + return l_cursor_sql; + end; + + member procedure init(self in out nocopy ut_data_value_anydata, a_value anydata) is + l_refcursor sys_refcursor; + cursor_not_open exception; + l_cursor_number number; + l_anydata_sql varchar2(32767); + begin + self.data_type := ut_metadata.get_anydata_typename(a_value); + self.compound_type := get_instance(a_value); + self.is_data_null := ut_metadata.is_anytype_null(a_value,self.compound_type); + self.data_id := sys_guid(); + self.self_type := $$plsql_unit; + self.cursor_details := ut_cursor_details(); + + ut_compound_data_helper.cleanup_diff; + + if not self.is_null() then + self.extract_path := get_extract_path(a_value); + l_anydata_sql := get_cursor_sql_from_anydata(a_value); + execute immediate l_anydata_sql using in a_value, in out l_refcursor; + if l_refcursor%isopen then + self.extract_cursor(l_refcursor); + l_cursor_number := dbms_sql.to_cursor_number(l_refcursor); + self.cursor_details := ut_cursor_details(l_cursor_number); + self.cursor_details.strip_root_from_anydata; + dbms_sql.close_cursor(l_cursor_number); + elsif not l_refcursor%isopen then + raise cursor_not_open; + end if; + end if; + exception + when cursor_not_open then + raise_application_error(-20155, 'Cursor is not open'); + when others then + if l_refcursor%isopen then + close l_refcursor; + end if; + raise; + end; + + member function get_instance(a_data_value anydata) return varchar2 is + l_result varchar2(30); + begin + l_result := ut_metadata.get_anydata_compound_type(a_data_value); + if l_result not in ('object','collection') then + raise_application_error(-20000, 'Data type '||a_data_value.gettypename||' in ANYDATA is not supported by utPLSQL'); + end if; + return l_result; + end; + + constructor function ut_data_value_anydata(self in out nocopy ut_data_value_anydata, a_value anydata) return self as result + is + begin + init(a_value); + return; + end; + + overriding member function compare_implementation( + a_other ut_data_value, + a_match_options ut_matcher_options, + a_inclusion_compare boolean := false, + a_is_negated boolean := false + ) return integer is + l_result integer := 0; + begin + if not a_other is of (ut_data_value_anydata) then + raise value_error; + end if; + l_result := l_result + (self as ut_data_value_refcursor).compare_implementation(a_other,a_match_options,a_inclusion_compare,a_is_negated); + return l_result; + end; + + overriding member function is_empty return boolean is + begin + if self.compound_type = 'collection' then + return self.elements_count = 0; + else + raise value_error; + end if; + end; +end; +/ diff --git a/source/expectations/data_values/ut_data_value_refcursor.tpb b/source/expectations/data_values/ut_data_value_refcursor.tpb index b93158cf1..d73a3e011 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tpb +++ b/source/expectations/data_values/ut_data_value_refcursor.tpb @@ -1,398 +1,397 @@ -create or replace type body ut_data_value_refcursor as - /* - utPLSQL - Version 3 - Copyright 2016 - 2018 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_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) - return self as result is - begin - init(a_value); - return; - end; - - member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) - is - c_bulk_rows constant integer := 10000; - l_cursor sys_refcursor := a_value; - l_ctx number; - l_xml xmltype; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_set_id integer := 0; - l_elements_count number := 0; - begin - -- We use DBMS_XMLGEN in order to: - -- 1) be able to process data in bulks (set of rows) - -- 2) be able to influence the ROWSET/ROW tags - -- 3) be able to influence the way NULL values are handled (empty TAG) - -- 4) be able to influence the way TIMESTAMP is formatted. - -- Due to Oracle feature/bug, it is not possible to change the DATE formatting of cursor data - -- AFTER the cursor was opened. - -- The only solution for this is to change NLS settings before opening the cursor. - -- - -- This would work fine if we could use DBMS_XMLGEN.restartQuery. - -- The restartQuery fails however if PLSQL variables of TIMESTAMP/INTERVAL or CLOB/BLOB are used. - ut_expectation_processor.set_xml_nls_params(); - l_ctx := dbms_xmlgen.newContext(l_cursor); - dbms_xmlgen.setNullHandling(l_ctx, dbms_xmlgen.empty_tag); - dbms_xmlgen.setMaxRows(l_ctx, c_bulk_rows); - loop - l_xml := dbms_xmlgen.getxmltype(l_ctx); - exit when dbms_xmlgen.getNumRowsProcessed(l_ctx) = 0; - --Bug in oracle 12.2+ where XML binary storage trimming insignificant whitespaces. - $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then - l_xml := xmltype( replace(l_xml.getClobVal(),' 0 then - ut_utils.append_to_clob( l_result, self.cursor_details.get_xml_children().getclobval() ); - end if; - ut_utils.append_to_clob(l_result,chr(10)||(self as ut_compound_data_value).to_string()); - l_result_string := ut_utils.to_string(l_result,null); - dbms_lob.freetemporary(l_result); - end if; - return l_result_string; - end; - - overriding member function diff( a_other ut_data_value, a_match_options ut_matcher_options ) return varchar2 is - l_result clob; - l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); - l_result_string varchar2(32767); - l_other ut_data_value_refcursor; - l_self ut_data_value_refcursor := self; - l_column_diffs ut_compound_data_helper.tt_column_diffs; - - l_other_cols ut_cursor_column_tab; - l_self_cols ut_cursor_column_tab; - - l_act_missing_pk ut_varchar2_list := ut_varchar2_list(); - l_exp_missing_pk ut_varchar2_list := ut_varchar2_list(); - - c_max_rows integer := ut_utils.gc_diff_max_rows; - l_diff_id ut_compound_data_helper.t_hash; - l_diff_row_count integer; - l_row_diffs ut_compound_data_helper.tt_row_diffs; - l_message varchar2(32767); - - function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is - begin - return - case a_col.diff_type - when '-' then - ' Column <'||a_col.expected_name||'> [data-type: '||a_col.expected_type||'] is missing. Expected column position: '||a_col.expected_pos||'.' - when '+' then - ' Column <'||a_col.actual_name||'> [position: '||a_col.actual_pos||', data-type: '||a_col.actual_type||'] is not expected in results.' - when 't' then - ' Column <'||a_col.actual_name||'> data-type is invalid. Expected: '||a_col.expected_type||',' ||' actual: '||a_col.actual_type||'.' - when 'p' then - ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' - end; - end; - - function remove_incomparable_cols( - a_cursor_details ut_cursor_column_tab, a_column_diffs ut_compound_data_helper.tt_column_diffs - ) return ut_cursor_column_tab is - l_missing_cols ut_varchar2_list := ut_varchar2_list(); - l_result ut_cursor_column_tab; - begin - for i in 1 .. a_column_diffs.count loop - if a_column_diffs(i).diff_type in ('-','+') then - l_missing_cols.extend; - l_missing_cols(l_missing_cols.last) := coalesce(a_column_diffs(i).expected_name, a_column_diffs(i).actual_name); - end if; - end loop; - select value(i) bulk collect into l_result - from table(a_cursor_details) i - where i.access_path not in ( - select c.column_value - from table(l_missing_cols) c - ); - return l_result; - end; - - function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs, a_is_unordered boolean) return varchar2 is - begin - if a_is_unordered then - if 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; - else - return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; - else - return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; - 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); - l_other.cursor_details.filter_columns(a_match_options); - l_self.cursor_details.filter_columns(a_match_options); - - l_other_cols := l_other.cursor_details.cursor_columns_info; - l_self_cols := l_self.cursor_details.cursor_columns_info; - - dbms_lob.createtemporary(l_result,true); - --diff columns - if not l_self.is_null and not l_other.is_null then - l_column_diffs := ut_compound_data_helper.get_columns_diff( - l_self.cursor_details.cursor_columns_info, - l_other.cursor_details.cursor_columns_info, - a_match_options.ordered_columns() - ); - - if l_column_diffs.count > 0 then - ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); - l_other_cols := remove_incomparable_cols( l_other_cols, l_column_diffs ); - l_self_cols := remove_incomparable_cols( l_self_cols, l_column_diffs ); - for i in 1 .. l_column_diffs.count loop - l_results.extend; - l_results(l_results.last) := get_col_diff_text(l_column_diffs(i)); - end loop; - ut_utils.append_to_clob(l_result, l_results); - end if; - end if; - - --check for missing pk - if a_match_options.join_by.items.count > 0 then - l_act_missing_pk := l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); - l_exp_missing_pk := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); - end if; - - --diff rows and row elements if the pk is not missing - if l_act_missing_pk.count + l_exp_missing_pk.count = 0 then - l_diff_id := ut_compound_data_helper.get_hash( l_self.data_id || l_other.data_id ); - - -- First tell how many rows are different - l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; - if l_diff_row_count > 0 then - l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( - l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, - l_diff_id, - case - when - l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) - else - a_match_options.join_by.items - end, - a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path - ); - 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 - ||' ]'||chr(10)|| case when l_row_diffs.count = 0 then ' All rows are different as the columns are not matching.' else null end; - ut_utils.append_to_clob( l_result, l_message ); - l_results := ut_utils.t_clob_tab(); - for i in 1 .. l_row_diffs.count loop - l_results.extend; - l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_match_options.unordered); - end loop; - ut_utils.append_to_clob(l_result,l_results); - else - l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.'; - ut_utils.append_to_clob( l_result, l_message ); - end if; - else - ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); - - for i in 1 .. l_exp_missing_pk.count loop - ut_utils.append_to_clob(l_result, ' Join key '||l_exp_missing_pk(i)||' does not exists in expected'||chr(10)); - end loop; - - for i in 1 .. l_act_missing_pk.count loop - ut_utils.append_to_clob(l_result, ' Join key '||l_act_missing_pk(i)||' does not exists in actual'||chr(10)); - end loop; - - if l_self.cursor_details.contains_collection() or l_other.cursor_details.contains_collection() then - ut_utils.append_to_clob(l_result,' Please make sure that your join clause is not refferring to collection element'|| chr(10)); - end if; - - 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) return integer is - begin - return compare_implementation( a_other, null ); - end; - - member function compare_implementation( - a_other ut_data_value, - a_match_options ut_matcher_options, - a_inclusion_compare boolean := false, - a_is_negated boolean := false - ) return integer is - l_result integer := 0; - l_self ut_data_value_refcursor := self; - l_other ut_data_value_refcursor; - l_diff_cursor_text clob; - - function compare_data( - a_self ut_data_value_refcursor, - a_other ut_data_value_refcursor, - a_diff_cursor_text clob - ) return integer is - l_diff_id ut_compound_data_helper.t_hash; - l_result integer; - --We will start with number od differences being displayed. - l_cursor sys_refcursor; - l_diff_tab ut_compound_data_helper.t_diff_tab; - l_diif_rowcount integer :=0; - begin - l_diff_id := ut_compound_data_helper.get_hash(a_self.data_id||a_other.data_id); - - begin - l_cursor := ut_compound_data_helper.get_compare_cursor(a_diff_cursor_text, - a_self.data_id, a_other.data_id); - --fetch and save rows for display of diff - fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_diff_max_rows; - exception when others then - if l_cursor%isopen then - close l_cursor; - end if; - raise; - end; - - ut_compound_data_helper.insert_diffs_result( l_diff_tab, l_diff_id ); - --fetch rows for count only - loop - exit when l_diff_tab.count = 0; - l_diif_rowcount := l_diif_rowcount + l_diff_tab.count; - fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_bc_fetch_limit; - end loop; - - ut_compound_data_helper.set_rows_diff(l_diif_rowcount); - - --result is OK only if both are same - if l_diif_rowcount = 0 and a_self.is_null = a_other.is_null then - l_result := 0; - else - l_result := 1; - end if; - close l_cursor; - return l_result; - 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); - l_other.cursor_details.filter_columns( a_match_options ); - l_self.cursor_details.filter_columns( a_match_options ); - - if a_match_options.join_by.items.count > 0 then - l_result := - l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count - + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; - end if; - - if l_result = 0 then - if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then - l_result := 1; - end if; - - l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( - l_other, - a_match_options.join_by.items, - a_match_options.unordered(), - a_inclusion_compare, - a_is_negated - ); - l_result := l_result + compare_data( l_self, l_other, l_diff_cursor_text ); - end if; - return l_result; - end; - - overriding member function is_empty return boolean is - begin - return self.elements_count = 0; - end; - -end; -/ +create or replace type body ut_data_value_refcursor as + /* + utPLSQL - Version 3 + Copyright 2016 - 2018 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_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) + return self as result is + begin + init(a_value); + return; + end; + + member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) + is + c_bulk_rows constant integer := 10000; + l_cursor sys_refcursor := a_value; + l_ctx number; + l_xml xmltype; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_set_id integer := 0; + l_elements_count number := 0; + begin + -- We use DBMS_XMLGEN in order to: + -- 1) be able to process data in bulks (set of rows) + -- 2) be able to influence the ROWSET/ROW tags + -- 3) be able to influence the way NULL values are handled (empty TAG) + -- 4) be able to influence the way TIMESTAMP is formatted. + -- Due to Oracle feature/bug, it is not possible to change the DATE formatting of cursor data + -- AFTER the cursor was opened. + -- The only solution for this is to change NLS settings before opening the cursor. + -- + -- This would work fine if we could use DBMS_XMLGEN.restartQuery. + -- The restartQuery fails however if PLSQL variables of TIMESTAMP/INTERVAL or CLOB/BLOB are used. + ut_expectation_processor.set_xml_nls_params(); + l_ctx := dbms_xmlgen.newContext(l_cursor); + dbms_xmlgen.setNullHandling(l_ctx, dbms_xmlgen.empty_tag); + dbms_xmlgen.setMaxRows(l_ctx, c_bulk_rows); + loop + l_xml := dbms_xmlgen.getxmltype(l_ctx); + exit when dbms_xmlgen.getNumRowsProcessed(l_ctx) = 0; + --Bug in oracle 12.2+ where XML binary storage trimming insignificant whitespaces. + $if dbms_db_version.version = 12 and dbms_db_version.release >= 2 or dbms_db_version.version > 12 $then + l_xml := xmltype( replace(l_xml.getClobVal(),' 0 then + ut_utils.append_to_clob( l_result, self.cursor_details.get_xml_children().getclobval() ); + end if; + ut_utils.append_to_clob(l_result,chr(10)||(self as ut_compound_data_value).to_string()); + l_result_string := ut_utils.to_string(l_result,null); + dbms_lob.freetemporary(l_result); + end if; + return l_result_string; + end; + + overriding member function diff( a_other ut_data_value, a_match_options ut_matcher_options ) return varchar2 is + l_result clob; + l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); + l_result_string varchar2(32767); + l_other ut_data_value_refcursor; + l_self ut_data_value_refcursor := self; + l_column_diffs ut_compound_data_helper.tt_column_diffs; + + l_other_cols ut_cursor_column_tab; + l_self_cols ut_cursor_column_tab; + + l_act_missing_pk ut_varchar2_list := ut_varchar2_list(); + l_exp_missing_pk ut_varchar2_list := ut_varchar2_list(); + + c_max_rows integer := ut_utils.gc_diff_max_rows; + l_diff_id ut_compound_data_helper.t_hash; + l_diff_row_count integer; + l_row_diffs ut_compound_data_helper.tt_row_diffs; + l_message varchar2(32767); + + function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is + begin + return + case a_col.diff_type + when '-' then + ' Column <'||a_col.expected_name||'> [data-type: '||a_col.expected_type||'] is missing. Expected column position: '||a_col.expected_pos||'.' + when '+' then + ' Column <'||a_col.actual_name||'> [position: '||a_col.actual_pos||', data-type: '||a_col.actual_type||'] is not expected in results.' + when 't' then + ' Column <'||a_col.actual_name||'> data-type is invalid. Expected: '||a_col.expected_type||',' ||' actual: '||a_col.actual_type||'.' + when 'p' then + ' Column <'||a_col.actual_name||'> is misplaced. Expected position: '||a_col.expected_pos||',' ||' actual position: '||a_col.actual_pos||'.' + end; + end; + + function remove_incomparable_cols( + a_cursor_details ut_cursor_column_tab, a_column_diffs ut_compound_data_helper.tt_column_diffs + ) return ut_cursor_column_tab is + l_missing_cols ut_varchar2_list := ut_varchar2_list(); + l_result ut_cursor_column_tab; + begin + for i in 1 .. a_column_diffs.count loop + if a_column_diffs(i).diff_type in ('-','+') then + l_missing_cols.extend; + l_missing_cols(l_missing_cols.last) := coalesce(a_column_diffs(i).expected_name, a_column_diffs(i).actual_name); + end if; + end loop; + select value(i) bulk collect into l_result + from table(a_cursor_details) i + where i.access_path not in ( + select c.column_value + from table(l_missing_cols) c + ); + return l_result; + end; + + function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs, a_is_unordered boolean) return varchar2 is + begin + if a_is_unordered then + if 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; + else + return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + else + return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; + end if; + 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); + l_other.cursor_details.filter_columns(a_match_options); + l_self.cursor_details.filter_columns(a_match_options); + + l_other_cols := l_other.cursor_details.cursor_columns_info; + l_self_cols := l_self.cursor_details.cursor_columns_info; + + dbms_lob.createtemporary(l_result,true); + --diff columns + if not l_self.is_null and not l_other.is_null then + l_column_diffs := ut_compound_data_helper.get_columns_diff( + l_self.cursor_details.cursor_columns_info, + l_other.cursor_details.cursor_columns_info, + a_match_options.ordered_columns() + ); + + if l_column_diffs.count > 0 then + ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); + l_other_cols := remove_incomparable_cols( l_other_cols, l_column_diffs ); + l_self_cols := remove_incomparable_cols( l_self_cols, l_column_diffs ); + for i in 1 .. l_column_diffs.count loop + l_results.extend; + l_results(l_results.last) := get_col_diff_text(l_column_diffs(i)); + end loop; + ut_utils.append_to_clob(l_result, l_results); + end if; + end if; + + --check for missing pk + if a_match_options.join_by.items.count > 0 then + l_act_missing_pk := l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); + l_exp_missing_pk := l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ); + end if; + + --diff rows and row elements if the pk is not missing + if l_act_missing_pk.count + l_exp_missing_pk.count = 0 then + l_diff_id := ut_compound_data_helper.get_hash( l_self.data_id || l_other.data_id ); + + -- First tell how many rows are different + l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; + if l_diff_row_count > 0 then + l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( + l_self_cols, l_other_cols, l_self.data_id, l_other.data_id, + l_diff_id, + case + when + l_self.cursor_details.is_anydata = 1 then ut_utils.add_prefix(a_match_options.join_by.items, l_self.cursor_details.get_root) + else + a_match_options.join_by.items + end, + a_match_options.unordered,a_match_options.ordered_columns(), self.extract_path + ); + 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 + ||' ]'||chr(10)|| case when l_row_diffs.count = 0 then ' All rows are different as the columns are not matching.' else null end; + ut_utils.append_to_clob( l_result, l_message ); + l_results := ut_utils.t_clob_tab(); + for i in 1 .. l_row_diffs.count loop + l_results.extend; + l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_match_options.unordered); + end loop; + ut_utils.append_to_clob(l_result,l_results); + else + l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.'; + ut_utils.append_to_clob( l_result, l_message ); + end if; + else + ut_utils.append_to_clob(l_result,chr(10) || 'Unable to join sets:' || chr(10)); + + for i in 1 .. l_exp_missing_pk.count loop + ut_utils.append_to_clob(l_result, ' Join key '||l_exp_missing_pk(i)||' does not exists in expected'||chr(10)); + end loop; + + for i in 1 .. l_act_missing_pk.count loop + ut_utils.append_to_clob(l_result, ' Join key '||l_act_missing_pk(i)||' does not exists in actual'||chr(10)); + end loop; + + if l_self.cursor_details.contains_collection() or l_other.cursor_details.contains_collection() then + ut_utils.append_to_clob(l_result,' Please make sure that your join clause is not refferring to collection element'|| chr(10)); + end if; + + 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) return integer is + begin + return compare_implementation( a_other, null ); + end; + + member function compare_implementation( + a_other ut_data_value, + a_match_options ut_matcher_options, + a_inclusion_compare boolean := false, + a_is_negated boolean := false + ) return integer is + l_result integer := 0; + l_self ut_data_value_refcursor := self; + l_other ut_data_value_refcursor; + l_diff_cursor_text clob; + + function compare_data( + a_self ut_data_value_refcursor, + a_other ut_data_value_refcursor, + a_diff_cursor_text clob + ) return integer is + l_diff_id ut_compound_data_helper.t_hash; + l_result integer; + --We will start with number od differences being displayed. + l_cursor sys_refcursor; + l_diff_tab ut_compound_data_helper.t_diff_tab; + l_diif_rowcount integer :=0; + begin + l_diff_id := ut_compound_data_helper.get_hash(a_self.data_id||a_other.data_id); + + begin + l_cursor := ut_compound_data_helper.get_compare_cursor(a_diff_cursor_text, + a_self.data_id, a_other.data_id); + --fetch and save rows for display of diff + fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_diff_max_rows; + exception when others then + if l_cursor%isopen then + close l_cursor; + end if; + raise; + end; + + ut_compound_data_helper.insert_diffs_result( l_diff_tab, l_diff_id ); + --fetch rows for count only + loop + exit when l_diff_tab.count = 0; + l_diif_rowcount := l_diif_rowcount + l_diff_tab.count; + fetch l_cursor bulk collect into l_diff_tab limit ut_utils.gc_bc_fetch_limit; + end loop; + + ut_compound_data_helper.set_rows_diff(l_diif_rowcount); + + --result is OK only if both are same + if l_diif_rowcount = 0 and a_self.is_null = a_other.is_null then + l_result := 0; + else + l_result := 1; + end if; + close l_cursor; + return l_result; + 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); + l_other.cursor_details.filter_columns( a_match_options ); + l_self.cursor_details.filter_columns( a_match_options ); + + if a_match_options.join_by.items.count > 0 then + l_result := + l_self.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count + + l_other.cursor_details.get_missing_join_by_columns( a_match_options.join_by.items ).count; + end if; + + if l_result = 0 then + if not l_self.is_null() and not l_other.is_null() and not l_self.cursor_details.equals( l_other.cursor_details, a_match_options ) then + l_result := 1; + end if; + l_diff_cursor_text := ut_compound_data_helper.gen_compare_sql( + l_other, + a_match_options.join_by.items, + a_match_options.unordered(), + a_inclusion_compare, + a_is_negated + ); + l_result := l_result + compare_data( l_self, l_other, l_diff_cursor_text ); + end if; + return l_result; + end; + + overriding member function is_empty return boolean is + begin + return self.elements_count = 0; + end; + +end; +/