Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d8525d6

Browse files
committed
Change behaviour to compare columns regardless of column position in ref cursor.
1 parent c156852 commit d8525d6

15 files changed

Lines changed: 328 additions & 60 deletions

docs/userguide/expectations.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -680,13 +680,36 @@ utPLSQL is capable of comparing compound data-types including:
680680

681681
### Notes on comparison of compound data
682682
- Compound data can contain elements of any data-type. This includes blob, clob, object type, nested table, varray or even a nested-cursor within a cursor.
683-
- Cursors, nested table and varray types are compared as **ordered lists of elements**. If order of elements differ, expectation will fail.
683+
684+
- Nested table and varray types are compared as **ordered lists of elements**. If order of elements differ, expectation will fail.
685+
686+
- Cursors are compared as **unordered list of elements** by default. If order of elements is of importance the option has to be passed to enforce column order comparison `ordered_columns` e.g.
687+
688+
```sql
689+
procedure ut_refcursors1 is
690+
l_actual sys_refcursor;
691+
l_expected sys_refcursor;
692+
l_expected_message varchar2(32767);
693+
l_actual_message varchar2(32767);
694+
begin
695+
open l_actual for select 1 user_id,'s' a_col,'test' username from dual;
696+
open l_expected for select 'test' username,'s' a_col,1 user_id from dual;
697+
--Act
698+
ut3.ut.expect(l_actual).to_equal(l_expected).join_by('USER_ID').ordered_columns;
699+
end;
700+
```
701+
684702
- Comparison of compound data is data-type aware. So a column `ID NUMBER` in a cursor is not the same as `ID VARCHAR2(100)`, even if they both hold the same numeric values.
703+
685704
- Comparison of cursor columns containing `DATE` will only compare date part **and ignore time** by default. See [Comparing cursor data containing DATE fields](#comparing-cursor-data-containing-date-fields) to check how to enable date-time comparison in cursors.
705+
686706
- Comparison of cursor returning `TIMESTAMP` **columns** against cursor returning `TIMESTAMP` **bind variables** requires variables to be casted to proper precision. This is an Oracle SQL - PLSQL compatibility issue and usage of CAST is the only known workaround for now.
687-
See [Comparing cursor data containing TIMESTAMP bind variables](#comparing-cursor-data-containing-timestamp-bind-variables) for examples.
707+
See [Comparing cursor data containing TIMESTAMP bind variables](#comparing-cursor-data-containing-timestamp-bind-variables) for examples.
708+
688709
- To compare nested table/varray type you need to convert it to `anydata` by using `anydata.convertCollection()`
710+
689711
- To compare object type you need to convert it to `anydata` by using `anydata.convertObject()`
712+
690713
- It is possible to compare PL/SQL records, collections, varrays and associative arrays. To compare this types of data, use cursor comparison feature of utPLSQL and TABLE operator in SQL query
691714
- On Oracle 11g Release 2 - pipelined table functions are needed (see section [Implicit (Shadow) Types in this artcile](https://oracle-base.com/articles/misc/pipelined-table-functions))
692715
- On Oracle 12c and above - use [TABLE function on nested tables/varrays/associative arrays of PL/SQL records](https://oracle-base.com/articles/12c/using-the-table-operator-with-locally-defined-types-in-plsql-12cr1)
@@ -768,7 +791,7 @@ create or replace package body test_cursor_compare as
768791
from dual union all
769792
select 'M' AS GENDER, 'LUKE' as FIRST_NAME, 'SKYWALKER' AS LAST_NAME, 2 as ID, '1000' AS SALARY
770793
from dual;
771-
ut.expect(l_actual).to_equal(l_expected);
794+
ut.expect(l_actual).to_equal(l_expected).ordered_columns;
772795
end;
773796
end;
774797
/

source/expectations/data_values/ut_compound_data_helper.pkb

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ create or replace package body ut_compound_data_helper is
4646
end if;
4747
return l_filter;
4848
end;
49-
50-
function get_columns_diff(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab)
49+
50+
function get_columns_diff_ordered(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab)
5151
return tt_column_diffs is
5252
l_column_filter varchar2(32767);
5353
l_sql varchar2(32767);
@@ -88,6 +88,53 @@ create or replace package body ut_compound_data_helper is
8888
return l_results;
8989
end;
9090

91+
function get_columns_diff_unordered(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab)
92+
return tt_column_diffs is
93+
l_column_filter varchar2(32767);
94+
l_sql varchar2(32767);
95+
l_results tt_column_diffs;
96+
begin
97+
with
98+
expected_cols as
99+
(select access_path exp_column_name,column_position exp_col_pos,
100+
replace(column_type,'VARCHAR2','CHAR') exp_col_type_compare, column_type exp_col_type
101+
from table(a_expected)),
102+
actual_cols as
103+
(select access_path act_column_name,column_position act_col_pos,
104+
replace(column_type,'VARCHAR2','CHAR') act_col_type_compare, column_type act_col_type
105+
from table(a_actual)),
106+
joined_cols as
107+
(select e.*,a.*
108+
from expected_cols e
109+
full outer join actual_cols a on e.exp_column_name = a.act_column_name)
110+
select case
111+
when exp_col_pos is null and act_col_pos is not null then '+'
112+
when exp_col_pos is not null and act_col_pos is null then '-'
113+
when exp_col_type_compare != act_col_type_compare then 't'
114+
else 'p'
115+
end as diff_type,
116+
exp_column_name, exp_col_type, exp_col_pos,
117+
act_column_name, act_col_type, act_col_pos
118+
bulk collect into l_results
119+
from joined_cols
120+
--column is unexpected (extra) or missing
121+
where act_col_pos is null or exp_col_pos is null
122+
--column type is not matching (except CHAR/VARCHAR2)
123+
or act_col_type_compare != exp_col_type_compare
124+
order by exp_col_pos, act_col_pos;
125+
return l_results;
126+
end;
127+
128+
function get_columns_diff(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab,a_order_enforced boolean := false)
129+
return tt_column_diffs is
130+
begin
131+
if a_order_enforced then
132+
return get_columns_diff_ordered(a_expected,a_actual);
133+
else
134+
return get_columns_diff_unordered(a_expected,a_actual);
135+
end if;
136+
end;
137+
91138
function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob is
92139
l_pk_value clob;
93140
begin
@@ -218,10 +265,10 @@ create or replace package body ut_compound_data_helper is
218265
ut_utils.append_to_clob(a_sql_stmt, l_sql_stmt);
219266
end;
220267

221-
procedure gen_sql_pieces_out_of_cursor(a_data_info ut_data_value_refcursor,a_pk_table ut_varchar2_list, a_xml_stmt out nocopy clob,
268+
procedure gen_sql_pieces_out_of_cursor(a_data_info ut_cursor_column_tab,a_pk_table ut_varchar2_list, a_xml_stmt out nocopy clob,
222269
a_select_stmt out nocopy clob ,a_partition_stmt out nocopy clob, a_equal_stmt out nocopy clob, a_join_by_stmt out nocopy clob,
223270
a_not_equal_stmt out nocopy clob) is
224-
l_cursor_info ut_cursor_column_tab := a_data_info.cursor_details.cursor_info;
271+
l_cursor_info ut_cursor_column_tab := a_data_info;
225272
l_partition_tmp clob;
226273
l_col_name varchar2(100);
227274
begin
@@ -305,7 +352,7 @@ create or replace package body ut_compound_data_helper is
305352

306353
begin
307354
dbms_lob.createtemporary(l_compare_sql, true);
308-
gen_sql_pieces_out_of_cursor(a_other, a_join_by_list,
355+
gen_sql_pieces_out_of_cursor(a_other.cursor_details.cursor_info, a_join_by_list,
309356
l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_equal_stmt,
310357
l_join_on_stmt, l_not_equal_stmt);
311358

@@ -374,9 +421,8 @@ create or replace package body ut_compound_data_helper is
374421

375422
function get_rows_diff_by_sql(a_act_cursor_info ut_cursor_column_tab,a_exp_cursor_info ut_cursor_column_tab,
376423
a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw,
377-
a_join_by_list ut_varchar2_list, a_unordered boolean
424+
a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false
378425
) return tt_row_diffs is
379-
380426
l_act_col_filter varchar2(32767);
381427
l_exp_col_filter varchar2(32767);
382428
l_act_extract_xpath varchar2(32767):= ut_utils.to_xpath(get_column_extract_path(a_act_cursor_info));
@@ -412,25 +458,28 @@ create or replace package body ut_compound_data_helper is
412458
select rn, diff_type, diffed_row, pk_value
413459
,case when diff_type = 'Actual:' then 1 else 2 end rnk
414460
,1 final_order
461+
,col_name
415462
from ( ]';
416463

417464
if a_unordered then
418-
l_sql := l_sql || q'[select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, pk_value pk_value
465+
l_sql := l_sql || q'[select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, pk_value,col_name
419466
from
420-
(select nvl(exp.rn, act.rn) rn, nvl(exp.pk_value, act.pk_value) pk_value, exp.col exp_item, act.col act_item
467+
(select nvl(exp.rn, act.rn) rn, nvl(exp.pk_value, act.pk_value) pk_value, exp.col exp_item, act.col act_item ,
468+
nvl(exp.col_name,act.col_name) col_name
421469
from exp join act on exp.rn = act.rn and exp.col_name = act.col_name
422470
where dbms_lob.compare(exp.col_val, act.col_val) != 0)
423471
unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:')
424472
))]';
425473
else
426-
l_sql := l_sql || q'[ select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value
474+
l_sql := l_sql || q'[ select rn, diff_type, xmlserialize(content data_item no indent) diffed_row, null pk_value,col_name
427475
from
428476
(select nvl(exp.rn, act.rn) rn,
429477
xmlagg(exp.col order by exp.col_no) exp_item,
430-
xmlagg(act.col order by act.col_no) act_item
478+
xmlagg(act.col order by act.col_no) act_item,
479+
max(nvl(exp.col_name,act.col_name)) col_name
431480
from exp exp join act act on exp.rn = act.rn and exp.col_name = act.col_name
432481
where dbms_lob.compare(exp.col_val, act.col_val) != 0
433-
group by exp.rn, act.rn
482+
group by (exp.rn, act.rn)
434483
)
435484
unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:'))
436485
)]';
@@ -443,14 +492,26 @@ create or replace package body ut_compound_data_helper is
443492
nvl2(:join_by,ut_compound_data_helper.get_pk_value(:join_by,case when exp_data_id is null then act_item_data else exp_item_data end),null) pk_value
444493
,case when exp_data_id is null then 1 else 2 end rnk
445494
,2 final_order
495+
,null col_name
446496
from ut_compound_data_diff_tmp i
447497
where diff_id = :diff_id
448498
and act_data_id is null or exp_data_id is null
449499
)
450-
order by final_order,
451-
case when final_order = 1 then rn else rnk end,
452-
case when final_order = 1 then rnk else rn end ]';
500+
order by final_order,]';
453501

502+
if a_enforce_column_order then
503+
l_sql := l_sql ||q'[case when final_order = 1 then rn else rnk end,
504+
case when final_order = 1 then rnk else rn end ]';
505+
elsif not(a_enforce_column_order) and not(a_unordered) then
506+
l_sql := l_sql ||q'[case when final_order = 1 then rn else rnk end,
507+
case when final_order = 1 then rnk else rn end ]';
508+
elsif a_unordered then
509+
l_sql := l_sql ||q'[case when final_order = 1 then col_name else to_char(rnk) end,
510+
case when final_order = 1 then to_char(rn) else col_name end,
511+
case when final_order = 1 then to_char(rnk) else col_name end
512+
]';
513+
end if;
514+
454515
execute immediate l_sql
455516
bulk collect into l_results
456517
using l_exp_extract_xpath,l_join_xpath,a_diff_id, a_expected_dataset_guid,

source/expectations/data_values/ut_compound_data_helper.pks

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ create or replace package ut_compound_data_helper authid definer is
5757
);
5858

5959
type t_diff_tab is table of t_diff_rec;
60-
60+
6161
function get_columns_filter(
6262
a_exclude_xpath varchar2, a_include_xpath varchar2,
6363
a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data'
6464
) return varchar2;
6565

66-
function get_columns_diff(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab)
66+
function get_columns_diff(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab,a_order_enforced boolean := false)
6767
return tt_column_diffs;
6868

6969
function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob;
@@ -75,7 +75,7 @@ create or replace package ut_compound_data_helper authid definer is
7575

7676
function get_rows_diff_by_sql(a_act_cursor_info ut_cursor_column_tab,a_exp_cursor_info ut_cursor_column_tab,
7777
a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw,
78-
a_join_by_list ut_varchar2_list, a_unordered boolean
78+
a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false
7979
) return tt_row_diffs;
8080

8181
subtype t_hash is raw(128);

source/expectations/data_values/ut_compound_data_value.tpb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ create or replace type body ut_compound_data_value as
223223
end loop;
224224

225225
ut_compound_data_helper.set_rows_diff(l_sql_rowcount);
226-
--result is OK only if both are same
226+
--result is OK only if both are same
227227
if l_sql_rowcount = 0 and ( self.elements_count = l_other.elements_count or a_inclusion_compare )then
228228
l_result := 0;
229229
else

source/expectations/data_values/ut_cursor_details.tpb

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ create or replace type body ut_cursor_details as
22

33
order member function compare(a_other ut_cursor_details) return integer is
44
l_diffs integer;
5-
begin
6-
select count(1) into l_diffs
7-
from table(self.cursor_info) a full outer join table(a_other.cursor_info) e
8-
on ( decode(a.parent_name,e.parent_name,1,0)= 1 and a.column_name = e.column_name and
9-
REPLACE(a.column_type,'VARCHAR2','CHAR') = REPLACE(e.column_type,'VARCHAR2','CHAR')
10-
and a.column_position = e.column_position )
11-
where a.column_name is null or e.column_name is null;
5+
begin
6+
if self.is_column_order_enforced = 1 then
7+
select count(1) into l_diffs
8+
from table(self.cursor_info) a full outer join table(a_other.cursor_info) e
9+
on ( decode(a.parent_name,e.parent_name,1,0)= 1 and a.column_name = e.column_name and
10+
REPLACE(a.column_type,'VARCHAR2','CHAR') = REPLACE(e.column_type,'VARCHAR2','CHAR')
11+
and a.column_position = e.column_position )
12+
where a.column_name is null or e.column_name is null;
13+
else
14+
select count(1) into l_diffs
15+
from table(self.cursor_info) a full outer join table(a_other.cursor_info) e
16+
on ( decode(a.parent_name,e.parent_name,1,0)= 1 and a.column_name = e.column_name and
17+
REPLACE(a.column_type,'VARCHAR2','CHAR') = REPLACE(e.column_type,'VARCHAR2','CHAR'))
18+
where a.column_name is null or e.column_name is null;
19+
end if;
1220
return l_diffs;
1321
end;
1422

@@ -221,5 +229,10 @@ create or replace type body ut_cursor_details as
221229
return;
222230
end;
223231

232+
member procedure ordered_columns(self in out nocopy ut_cursor_details,a_ordered_columns boolean := false) is
233+
begin
234+
self.is_column_order_enforced := ut_utils.boolean_to_int(a_ordered_columns);
235+
end;
236+
224237
end;
225238
/

source/expectations/data_values/ut_cursor_details.tps

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
create or replace type ut_cursor_details force authid current_user as object
22
(
33
cursor_info ut_cursor_column_tab,
4+
is_column_order_enforced number(1,0),
45
order member function compare(a_other ut_cursor_details) return integer,
56
member procedure get_anytype_members_info(a_anytype anytype, a_attribute_typecode out pls_integer,
67
a_schema_name out varchar2, a_type_name out varchar2, a_len out pls_integer,a_elements_count out pls_integer),
@@ -15,6 +16,7 @@ create or replace type ut_cursor_details force authid current_user as object
1516
member function get_user_defined_type(a_data anydata) return anytype,
1617
constructor function ut_cursor_details(self in out nocopy ut_cursor_details) return self as result,
1718
constructor function ut_cursor_details(self in out nocopy ut_cursor_details,a_cursor_number in number)
18-
return self as result
19+
return self as result,
20+
member procedure ordered_columns(self in out nocopy ut_cursor_details,a_ordered_columns boolean := false)
1921
)
2022
/

source/expectations/data_values/ut_data_value_refcursor.tpb

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ create or replace type body ut_data_value_refcursor as
141141
l_row_diffs ut_compound_data_helper.tt_row_diffs;
142142
l_message varchar2(32767);
143143

144+
l_column_order_enforce boolean := ut_utils.int_to_boolean(self.cursor_details.is_column_order_enforced);
145+
144146
function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is
145147
begin
146148
return
@@ -208,8 +210,8 @@ create or replace type body ut_data_value_refcursor as
208210
dbms_lob.createtemporary(l_result,true);
209211
--diff columns
210212
if not self.is_null and not l_actual.is_null then
211-
l_column_diffs := ut_compound_data_helper.get_columns_diff(self.cursor_details.cursor_info,l_actual.cursor_details.cursor_info);
212-
213+
l_column_diffs := ut_compound_data_helper.get_columns_diff(self.cursor_details.cursor_info,l_actual.cursor_details.cursor_info,l_column_order_enforce);
214+
213215
if l_column_diffs.count > 0 then
214216
ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10));
215217
end if;
@@ -234,9 +236,9 @@ create or replace type body ut_data_value_refcursor as
234236
-- First tell how many rows are different
235237
l_diff_row_count := ut_compound_data_helper.get_rows_diff_count;
236238
l_results := ut_utils.t_clob_tab();
237-
if l_diff_row_count > 0 then
239+
if l_diff_row_count > 0 then
238240
l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql(
239-
l_exp_cols,l_act_cols, self.data_id, l_actual.data_id, l_diff_id,a_join_by_list , a_unordered);
241+
l_exp_cols,l_act_cols, self.data_id, l_actual.data_id, l_diff_id,a_join_by_list , a_unordered, l_column_order_enforce);
240242
l_message := chr(10)
241243
||'Rows: [ ' || l_diff_row_count ||' differences'
242244
|| case when l_diff_row_count > c_max_rows and l_row_diffs.count > 0 then ', showing first '||c_max_rows end
@@ -248,7 +250,7 @@ create or replace type body ut_data_value_refcursor as
248250
end loop;
249251
ut_utils.append_to_clob(l_result,l_results);
250252
else
251-
l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns are not matching.';
253+
l_message:= chr(10)||'Rows: [ all different ]'||chr(10)||' All rows are different as the columns position is not matching.';
252254
ut_utils.append_to_clob( l_result, l_message );
253255
end if;
254256
else
@@ -304,11 +306,12 @@ create or replace type body ut_data_value_refcursor as
304306
return self.elements_count = 0;
305307
end;
306308

307-
member function filter_cursor (a_exclude_xpath ut_varchar2_list, a_include_xpath ut_varchar2_list) return ut_data_value_refcursor is
309+
member function update_cursor_details (a_exclude_xpath ut_varchar2_list, a_include_xpath ut_varchar2_list,a_ordered_columns boolean := false) return ut_data_value_refcursor is
308310
l_result ut_data_value_refcursor := self;
309311
begin
310312
if l_result.cursor_details.cursor_info is not null then
311313
l_result.cursor_details.cursor_info := ut_compound_data_helper.inc_exc_columns_from_cursor(l_result.cursor_details.cursor_info,a_exclude_xpath,a_include_xpath);
314+
l_result.cursor_details.ordered_columns(a_ordered_columns);
312315
end if;
313316
return l_result;
314317
end;

source/expectations/data_values/ut_data_value_refcursor.tps

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@ create or replace type ut_data_value_refcursor under ut_compound_data_value(
3838
overriding member function compare_implementation(a_other ut_data_value, a_unordered boolean, a_inclusion_compare boolean := false, a_is_negated boolean := false,
3939
a_join_by_list ut_varchar2_list:=ut_varchar2_list()) return integer,
4040
overriding member function is_empty return boolean,
41-
member function filter_cursor (a_exclude_xpath ut_varchar2_list, a_include_xpath ut_varchar2_list) return ut_data_value_refcursor)
41+
member function update_cursor_details (a_exclude_xpath ut_varchar2_list, a_include_xpath ut_varchar2_list,a_ordered_columns boolean := false) return ut_data_value_refcursor
42+
)
4243
/

0 commit comments

Comments
 (0)