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

Skip to content

Commit 0a6a43e

Browse files
committed
Improved row and column diff.
1 parent ddd8247 commit 0a6a43e

6 files changed

Lines changed: 313 additions & 124 deletions

File tree

source/expectations/data_values/ut_data_value_refcursor.tpb

Lines changed: 43 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -137,104 +137,74 @@ create or replace type body ut_data_value_refcursor as
137137

138138
overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2 ) return varchar2 is
139139
c_max_rows constant integer := 20;
140-
c_pad_depth constant integer := 5;
141140
l_results ut_utils.t_clob_tab;
142141
l_result clob;
143142
l_result_string varchar2(32767);
144143
l_ut_owner varchar2(250) := ut_utils.ut_owner;
145144
l_diff_row_count integer;
146-
l_other ut_data_value_refcursor;
145+
l_actual ut_data_value_refcursor;
147146
l_diff_id raw(16);
148-
l_column_filter varchar2(32767);
149-
l_sql varchar2(32767);
147+
l_column_diffs ut_refcursor_helper.tt_column_diffs;
148+
l_row_diffs ut_refcursor_helper.tt_row_diffs;
149+
l_exclude_list ut_varchar2_list := ut_varchar2_list();
150+
l_exclude_xpath varchar2(32767);
150151
begin
151152
if not a_other is of (ut_data_value_refcursor) then
152153
raise value_error;
153154
end if;
154-
l_other := treat(a_other as ut_data_value_refcursor);
155+
l_actual := treat(a_other as ut_data_value_refcursor);
155156

156157
dbms_lob.createtemporary(l_result,true);
157158

158-
l_column_filter := ut_refcursor_helper.get_columns_filter(a_exclude_xpath, a_include_xpath);
159-
if not self.is_null and not l_other.is_null then
160-
l_sql :=
161-
'with ' ||
162-
' self_cols as (' ||
163-
' select r.column_value.getstringval() col, rownum rn ' ||
164-
' from ( select '||l_column_filter||
165-
' from ( select :columns_info as item_data from dual ) ucd' ||
166-
' ) s, ' ||
167-
' table( xmlsequence(extract(s.item_data,''/ROW/*'')) ) r' ||
168-
' ),'||
169-
' other_cols as (' ||
170-
' select r.column_value.getstringval() col, rownum rn ' ||
171-
' from ( select '||l_column_filter||
172-
' from (select :columns_info as item_data from dual ) ucd' ||
173-
' ) s, ' ||
174-
' table( xmlsequence(extract(s.item_data,''/ROW/*'')) ) r' ||
175-
' ) ' ||
176-
q'[select case when e.rn is null then '+' else '-' end
177-
||'Col No. '||rpad( nvl(a.rn,e.rn), :c_pad_depth)
178-
||' '||nvl(e.col, a.col)]' ||
179-
' from self_cols e' ||
180-
' full outer join other_cols a ' ||
181-
' on a.rn = e.rn and a.col = e.col' ||
182-
' where a.rn is null or e.rn is null' ||
183-
' order by NVL(a.rn,e.rn), a.rn';
184-
execute immediate l_sql bulk collect into l_results
185-
using a_exclude_xpath, a_include_xpath, self.columns_info,
186-
a_exclude_xpath, a_include_xpath, l_other.columns_info, c_pad_depth;
159+
if not self.is_null and not l_actual.is_null then
160+
161+
l_column_diffs := ut_refcursor_helper.get_columns_diff(self.columns_info, l_actual.columns_info, a_exclude_xpath, a_include_xpath);
162+
163+
select case diff_type
164+
when '-' then ' Column <'||expected_name||'> [data-type: '||expected_type||'] is missing. Expected column position: '||expected_pos||'.'
165+
when '+' then ' Column <'||actual_name||'> [position: '||actual_pos||', data-type: '||actual_type||'] is not expected in results.'
166+
when 't' then ' Column <'||actual_name||'> data-type is invalid. Expected: '||expected_type||', actual: '||actual_type||'.'
167+
when 'p' then ' Column <'||actual_name||'> is misplaced. Expected position: '||expected_pos||', actual position: '||actual_pos||'.'
168+
end diff_msg
169+
bulk collect into l_results
170+
from table(l_column_diffs)
171+
order by expected_pos, actual_pos;
187172

188173
if l_results.count > 0 then
189174
ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10));
190175
ut_utils.append_to_clob(l_result,l_results);
191176
end if;
177+
178+
select coalesce(expected_name,actual_name)
179+
bulk collect into l_exclude_list
180+
from table(l_column_diffs)
181+
where diff_type in ('-','+');
182+
end if;
183+
l_exclude_xpath := ut_utils.to_xpath(l_exclude_list);
184+
if a_exclude_xpath is not null then
185+
l_exclude_xpath := l_exclude_xpath || a_exclude_xpath;
192186
end if;
193187

194-
l_diff_id := dbms_crypto.hash(self.data_set_guid||l_other.data_set_guid,2);
188+
l_diff_id := dbms_crypto.hash(self.data_set_guid||l_actual.data_set_guid,2);
195189
-- First tell how many rows are different
196190
execute immediate 'select count(*) from ' || l_ut_owner || '.ut_data_set_diff_tmp where diff_id = :diff_id' into l_diff_row_count using l_diff_id;
197191

198192
if l_diff_row_count > 0 then
199-
--return rows which were previously marked as different
200-
l_sql :=
201-
q'[with diff_info as (select item_no from ]' || l_ut_owner || q'[.ut_data_set_diff_tmp ucdc where diff_id = :diff_guid and rownum <= :max_rows)
202-
select ind||'Row No. '||rpad( rn, :c_pad_depth)||xmlserialize(content data_item no indent) diff
203-
from (select nvl(exp.rn, act.rn) rn,
204-
xmlagg(exp.col order by exp.col_no) exp_item,
205-
xmlagg(act.col order by act.col_no) act_item
206-
from (select r.item_no as rn, rownum col_no, s.column_value col,
207-
extract(s.column_value,'/*/text()|/*/*').getclobval() col_val
208-
from (select ]'||l_column_filter||q'[, ucd.item_no
209-
from ]' || l_ut_owner || q'[.ut_data_set_tmp ucd
210-
where ucd.data_set_guid = :self_guid
211-
and ucd.item_no in (select i.item_no from diff_info i)
212-
) r,
213-
table( xmlsequence( extract(r.item_data,'ROW/*') ) ) s
214-
) exp
215-
full outer join (
216-
select item_no as rn, rownum col_no, s.column_value col,
217-
extract(s.column_value,'/*/text()|/*/*').getclobval() col_val
218-
from (select ]'||l_column_filter||q'[, ucd.item_no
219-
from ]' || l_ut_owner || q'[.ut_data_set_tmp ucd
220-
where ucd.data_set_guid = :other_guid
221-
and ucd.item_no in (select i.item_no from diff_info i)
222-
) r,
223-
table( xmlsequence( extract(r.item_data,'ROW/*') ) ) s
224-
) act
225-
on (exp.rn = act.rn and exp.col_no = act.col_no)
226-
where (exp.rn is null and act.rn is not null or exp.rn is not null and act.rn is null)
227-
or nvl(dbms_lob.compare(exp.col_val, act.col_val),1) != 0 and (exp.col_val is not null or act.col_val is not null)
228-
group by nvl(exp.rn, act.rn)
229-
)
230-
unpivot (data_item for ind in (exp_item as '-', act_item as '+'))
231-
order by rn, ind]';
232-
execute immediate l_sql
193+
l_row_diffs := ut_refcursor_helper.get_rows_diff(
194+
self.data_set_guid, l_actual.data_set_guid, l_diff_id,
195+
c_max_rows, l_exclude_xpath, a_include_xpath
196+
);
197+
198+
select ' Row No. '||rn||' - '||rpad(diff_type,10)||diffed_row diff
233199
bulk collect into l_results
234-
using l_diff_id, c_max_rows, c_pad_depth,
235-
a_exclude_xpath, a_include_xpath, self.data_set_guid,
236-
a_exclude_xpath, a_include_xpath, l_other.data_set_guid;
237-
ut_utils.append_to_clob(l_result,chr(10) || 'Rows: [ diff count = ' || to_char(l_diff_row_count) ||' ]' || chr(10));
200+
from table(l_row_diffs)
201+
order by rn, diff_type;
202+
203+
if l_results.count = 0 then
204+
ut_utils.append_to_clob(l_result,chr(10) || 'Rows:'||chr(10)||' All rows are different as the columns are not matching.');
205+
else
206+
ut_utils.append_to_clob(l_result,chr(10) || 'Rows: [ diff count = ' || to_char(l_diff_row_count) ||' ]' || chr(10));
207+
end if;
238208
ut_utils.append_to_clob(l_result,l_results);
239209
end if;
240210

source/expectations/data_values/ut_refcursor_helper.pkb

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,126 @@ create or replace package body ut_refcursor_helper is
8282
return l_filter;
8383
end;
8484

85+
function get_columns_diff(
86+
a_expected xmltype, a_actual xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2
87+
) return tt_column_diffs is
88+
l_column_filter varchar2(32767);
89+
l_sql varchar2(32767);
90+
l_results tt_column_diffs;
91+
begin
92+
l_column_filter := ut_refcursor_helper.get_columns_filter(a_exclude_xpath, a_include_xpath);
93+
l_sql := q'[
94+
with
95+
expected_cols as ( select :a_expected as item_data from dual ),
96+
actual_cols as ( select :a_actual as item_data from dual ),
97+
expected_cols_info as (
98+
select rownum expected_pos,
99+
r.column_value.getrootelement() expected_name,
100+
extractvalue(r.column_value,'/*') expected_type
101+
from ( select ]'||l_column_filter||q'[ from expected_cols ucd ) s,
102+
table( xmlsequence(extract(s.item_data,'/*/*')) ) r
103+
),
104+
actual_cols_info as (
105+
select rownum actual_pos,
106+
r.column_value.getrootelement() actual_name,
107+
extractvalue(r.column_value,'/*') actual_type
108+
from ( select ]'||l_column_filter||q'[ from actual_cols ucd ) s,
109+
table( xmlsequence(extract(s.item_data,'/*/*')) ) r
110+
),
111+
joined_cols as (
112+
select e.*, a.*,
113+
row_number() over(partition by case when actual_pos + expected_pos is not null then 1 end order by actual_pos) a_pos_nn,
114+
row_number() over(partition by case when actual_pos + expected_pos is not null then 1 end order by expected_pos) e_pos_nn
115+
from expected_cols_info e
116+
full outer join actual_cols_info a on e.expected_name = a.actual_name
117+
)
118+
select case
119+
when expected_pos is null and actual_pos is not null then '+'
120+
when expected_pos is not null and actual_pos is null then '-'
121+
when actual_type != expected_type then 't'
122+
else 'p'
123+
end as diff_type,
124+
expected_name, expected_type, expected_pos,
125+
actual_name, actual_type, actual_pos
126+
from joined_cols
127+
where actual_pos is null or expected_pos is null or actual_type != expected_type or a_pos_nn != e_pos_nn
128+
order by expected_pos, actual_pos]';
129+
execute immediate l_sql
130+
bulk collect into l_results
131+
using a_expected, a_actual, a_exclude_xpath, a_include_xpath, a_exclude_xpath, a_include_xpath;
132+
return l_results;
133+
end;
134+
135+
function get_rows_diff(
136+
a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw,
137+
a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2
138+
) return tt_row_diffs is
139+
l_column_filter varchar2(32767);
140+
l_results tt_row_diffs;
141+
begin
142+
l_column_filter := get_columns_filter(a_exclude_xpath, a_include_xpath);
143+
execute immediate q'[
144+
with
145+
diff_info as (select item_no from ut_data_set_diff_tmp ucdc where diff_id = :diff_guid and rownum <= :max_rows)
146+
select *
147+
from (select rn, diff_type, xmlserialize(content data_item no indent) diffed_row
148+
from (select nvl(exp.rn, act.rn) rn,
149+
xmlagg(exp.col order by exp.col_no) exp_item,
150+
xmlagg(act.col order by act.col_no) act_item
151+
from (select r.item_no as rn, rownum col_no, s.column_value col,
152+
s.column_value.getRootElement() col_name,
153+
s.column_value.getclobval() col_val
154+
from (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_data item_data_no_filter
155+
from ut_data_set_tmp ucd
156+
where ucd.data_set_guid = :self_guid
157+
and ucd.item_no in (select i.item_no from diff_info i)
158+
) r,
159+
table( xmlsequence( extract(r.item_data,'/*/*') ) ) s
160+
) exp
161+
join (
162+
select item_no as rn, rownum col_no, s.column_value col,
163+
s.column_value.getRootElement() col_name,
164+
s.column_value.getclobval() col_val
165+
from (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_data item_data_no_filter
166+
from ut_data_set_tmp ucd
167+
where ucd.data_set_guid = :other_guid
168+
and ucd.item_no in (select i.item_no from diff_info i)
169+
) r,
170+
table( xmlsequence( extract(r.item_data,'/*/*') ) ) s
171+
) act
172+
on exp.rn = act.rn and exp.col_name = act.col_name
173+
where dbms_lob.compare(exp.col_val, act.col_val) != 0
174+
group by exp.rn, act.rn
175+
)
176+
unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') )
177+
)
178+
union all
179+
select nvl(exp.item_no, act.item_no) rn,
180+
case when exp.item_no is null then 'Extra:' else 'Missing:' end as diff_type,
181+
xmlserialize(content nvl(exp.item_data, act.item_data) no indent) diffed_row
182+
from (select ucd.item_no, extract(ucd.item_data,'/*/*') item_data
183+
from ut_data_set_tmp ucd
184+
where ucd.data_set_guid = :self_guid
185+
and ucd.item_no in (select i.item_no from diff_info i)
186+
) exp
187+
full outer join (
188+
select ucd.item_no, extract(ucd.item_data,'/*/*') item_data
189+
from ut_data_set_tmp ucd
190+
where ucd.data_set_guid = :other_guid
191+
and ucd.item_no in (select i.item_no from diff_info i)
192+
193+
)act
194+
on exp.item_no = act.item_no
195+
where exp.item_no is null or act.item_no is null
196+
]'
197+
bulk collect into l_results
198+
using a_diff_id, a_max_rows,
199+
a_exclude_xpath, a_include_xpath, a_expected_dataset_guid,
200+
a_exclude_xpath, a_include_xpath, a_actual_dataset_guid,
201+
a_expected_dataset_guid, a_actual_dataset_guid;
202+
return l_results;
203+
end;
204+
85205
begin
86206
g_type_name_map( dbms_sql.binary_bouble_type ) := 'BINARY_DOUBLE';
87207
g_type_name_map( dbms_sql.bfile_type ) := 'BFILE';

source/expectations/data_values/ut_refcursor_helper.pks

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
create or replace package ut_refcursor_helper is
1+
create or replace package ut_refcursor_helper authid definer is
22
/*
33
utPLSQL - Version 3
44
Copyright 2016 - 2017 utPLSQL Project
@@ -16,12 +16,41 @@ create or replace package ut_refcursor_helper is
1616
limitations under the License.
1717
*/
1818

19+
type t_column_diffs is record(
20+
diff_type varchar2(1),
21+
expected_name varchar2(250),
22+
expected_type varchar2(250),
23+
expected_pos integer,
24+
actual_name varchar2(250),
25+
actual_type varchar2(250),
26+
actual_pos integer
27+
);
28+
29+
type tt_column_diffs is table of t_column_diffs;
30+
31+
type t_row_diffs is record(
32+
rn integer,
33+
diff_type varchar2(250),
34+
diffed_row clob
35+
);
36+
37+
type tt_row_diffs is table of t_row_diffs;
38+
1939
function get_columns_info(a_cursor in out nocopy sys_refcursor) return xmltype;
2040

2141
function get_columns_filter(
2242
a_exclude_xpath varchar2, a_include_xpath varchar2,
2343
a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data'
2444
) return varchar2;
2545

46+
function get_columns_diff(
47+
a_expected xmltype, a_actual xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2
48+
) return tt_column_diffs;
49+
50+
function get_rows_diff(
51+
a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw,
52+
a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2
53+
) return tt_row_diffs;
54+
2655
end;
2756
/

source/expectations/matchers/ut_equal.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_equal as
223223
begin
224224
l_result := (self as ut_matcher).failure_message(a_actual) || ': '|| self.expected.to_string_report();
225225
if self.expected.data_type = a_actual.data_type and self.expected.is_diffable then
226-
l_result := l_result || chr(10) || 'diff: ' || expected.diff(a_actual, get_exclude_xpath(), get_include_xpath());
226+
l_result := l_result || chr(10) || 'Diff:' || expected.diff(a_actual, get_exclude_xpath(), get_include_xpath());
227227
end if;
228228
return l_result;
229229
end;

0 commit comments

Comments
 (0)