diff --git a/docs/about/authors.md b/docs/about/authors.md index 55f85642d..76bb24e5f 100644 --- a/docs/about/authors.md +++ b/docs/about/authors.md @@ -7,11 +7,11 @@ | ---------------- | -------------- | David Pyke | [Shoelace](https://github.com/Shoelace) | Jacek Gebal | [jgebal](https://github.com/jgebal) +| Lukasz Wasylow | [lwasylow](https://github.com/lwasylow/) | Pavel Kaplya | [Pazus](https://github.com/Pazus) | Robert Love | [rlove](https://github.com/rlove) -| Vinicius Avellar | [viniciusam](https://github.com/viniciusam/) | Samuel Nitsche | [pesse](https://github.com/pesse/) -| Lukasz Wasylow | [lwasylow](https://github.com/lwasylow/) +| Vinicius Avellar | [viniciusam](https://github.com/viniciusam/) diff --git a/docs/images/venn21.gif b/docs/images/venn21.gif new file mode 100644 index 000000000..0efecae07 Binary files /dev/null and b/docs/images/venn21.gif differ diff --git a/docs/images/venn22.gif b/docs/images/venn22.gif new file mode 100644 index 000000000..52768b71f Binary files /dev/null and b/docs/images/venn22.gif differ diff --git a/docs/userguide/advanced_data_comparison.md b/docs/userguide/advanced_data_comparison.md index 3eb45fe50..6929b9845 100644 --- a/docs/userguide/advanced_data_comparison.md +++ b/docs/userguide/advanced_data_comparison.md @@ -6,7 +6,7 @@ utPLSQL expectations incorporates advanced data comparison options when comparin - object type - nested table and varray -Advanced data-comparison options are available for the [`equal`](expectations.md#equal) matcher. +Advanced data-comparison options are available for the [`equal`](expectations.md#equal) and [`include / contain`](expectations.md#include) matcher. ## Syntax @@ -15,6 +15,10 @@ Advanced data-comparison options are available for the [`equal`](expectations.md ut.expect( a_actual {data-type} ).not_to( equal( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]]) ); ut.expect( a_actual {data-type} ).to_equal( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]]); ut.expect( a_actual {data-type} ).not_to_equal( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]] ); + ut.expect( a_actual {data-type} ).to_contain( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]]); + ut.expect( a_actual {data-type} ).to_include( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]]); + ut.expect( a_actual {data-type} ).not_to_include( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]]); + ut.expect( a_actual {data-type} ).not_to_contain( a_expected {data-type})[.extendend_option()[.extendend_option()[...]]]); ``` `extended_option` can be one of: @@ -23,28 +27,25 @@ Advanced data-comparison options are available for the [`equal`](expectations.md - `exclude(a_items varchar2)` - item or comma separated list of items to exclude - `include(a_items ut_varchar2_list)` - table of items to include - `exclude(a_items ut_varchar2_list)` - table of items to exclude - - `unordered` - perform compare on unordered set of data, return only missing or actual - - `join_by(a_columns varchar2)` - columns or comma seperated list of columns to join two cursors by + - `unordered` - perform compare on unordered set of data, return only missing or actual, ***not supported for `include / contain`*** , as alternative `join_by` can be used + - `join_by(a_columns varchar2)` - columns or comma separated list of columns to join two cursors by - `join_by(a_columns ut_varchar2_list)` - table of columns to join two cursors by Each item in the comma separated list can be: - a column name of cursor to be compared - an attribute name of object type to be compared - an attribute name of object type within a table of objects to be compared -- an [XPath](http://zvon.org/xxl/XPathTutorial/Output/example1.html) expression representing column/attribute - Include and exclude option will not support implicit colum names that starts with single quota, or in fact any other special characters e.g. <, >, & Each element in `ut_varchar2_list` nested table can be an item or a comma separated list of items. When specifying column/attribute names, keep in mind that the names are **case sensitive**. -**XPath expressions with comma are not supported.** - ## Excluding elements from data comparison -Consider the following example +Consider the following examples ```sql -procedure test_cursors_skip_columns is +procedure test_cur_skip_columns_eq is l_expected sys_refcursor; l_actual sys_refcursor; begin @@ -52,10 +53,19 @@ begin open l_actual for select sysdate "ADate", d.* from user_tables d; ut.expect( l_actual ).to_equal( l_expected ).exclude( 'IGNORE_ME,ADate' ); end; + +procedure test_cur_skip_columns_cn is + l_expected sys_refcursor; + l_actual sys_refcursor; +begin + open l_expected for select 'text' ignore_me, d.* from user_tables d; + open l_actual for select sysdate "ADate", d.* from user_tables d; + ut.expect( l_actual ).to_include( l_expected ).exclude( 'IGNORE_ME,ADate' ); +end; ``` Columns 'ignore_me' and "ADate" will get excluded from cursor comparison. -The cursor data is equal, when those columns are excluded. +The cursor data is equal or includes expected, when those columns are excluded. This option is useful in scenarios, when you need to exclude incomparable/unpredictable column data like CREATE_DATE of a record that is maintained by default value on a table column. @@ -63,7 +73,7 @@ This option is useful in scenarios, when you need to exclude incomparable/unpred Consider the following example ```sql -procedure include_columns_as_csv is +procedure include_col_as_csv_eq is l_actual sys_refcursor; l_expected sys_refcursor; begin @@ -71,14 +81,23 @@ begin open l_actual for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL, a.* from all_objects a where rownum < 4; ut.expect( l_actual ).to_equal( l_expected ).include( 'RN,A_Column,SOME_COL' ); end; + +procedure include_col_as_csv_cn is + l_actual sys_refcursor; + l_expected sys_refcursor; +begin + open l_expected for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL from dual a connect by level < 4; + open l_actual for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL, a.* from all_objects a where rownum < 6; + ut.expect( l_actual ).to_contain( l_expected ).include( 'RN,A_Column,SOME_COL' ); +end; ``` ## Combining include/exclude options You can chain the advanced options in an expectation and mix the `varchar2` with `ut_varchar2_list` arguments. -When doing so, the fianl list of items to include/exclude will be a concatenation of all items. +When doing so, the final list of items to include/exclude will be a concatenation of all items. ```sql -procedure include_columns_as_csv is +procedure include_col_as_csv_eq is l_actual sys_refcursor; l_expected sys_refcursor; begin @@ -89,6 +108,19 @@ begin .include( ut_varchar2_list( 'A_Column', 'SOME_COL' ) ) .exclude( 'SOME_COL' ); end; + +procedure include_col_as_csv_cn is + l_actual sys_refcursor; + l_expected sys_refcursor; +begin + open l_expected for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL from dual a connect by level < 4; + open l_actual for select rownum as rn, 'a' as "A_Column", 'Y' SOME_COL, a.* from all_objects a where rownum < 6; + ut.expect( l_actual ).to_contain( l_expected ) + .include( 'RN') + .include( ut_varchar2_list( 'A_Column', 'SOME_COL' ) ) + .exclude( 'SOME_COL' ); +end; + ``` Only the columns 'RN', "A_Column" will be compared. Column 'SOME_COL' is excluded. @@ -101,6 +133,8 @@ Unordered option allows for quick comparison of two cursors without need of orde Result of such comparison will be limited to only information about row existing or not existing in given set without actual information about exact differences. +**This option is not supported for `include / contain` matcher** + ```sql @@ -169,6 +203,33 @@ This will show you difference in row 'TEST' regardless of order. Assumption is that join by is made by column name so that what will be displayed as part of results. +Consider this example using `contain / include ` + +```sql +procedure join_by_username_cn is + l_actual sys_refcursor; + l_expected sys_refcursor; +begin + open l_actual for select username, user_id from all_users; + open l_expected for select username, user_id from all_users + union all + select 'TEST' username, -610 user_id from dual; + + ut.expect( l_actual ).to_contain( l_expected ).join_by('USERNAME'); +end; +``` + +This will show you that one value is not included in actual set: + +```sql + Actual: refcursor [ count = 43 ] was expected to include: refcursor [ count = 44 ] + Diff: + Rows: [ 1 differences ] + PK TEST - Missing -610 +``` + + + Join by options currently doesn't support nested table inside cursor comparison, however is still possible to compare a collection as a whole. Example. @@ -220,25 +281,33 @@ Diff: +***Please note that .join_by option will take longer to process due to need of parsing via primary keys.*** +## Defining item lists in option +XPath expressions are deprecated. They are currently still supported but in future versions they can be removed completely. Please use a current standard of defining items filter. -**Please note that .join_by option will take longer to process due to need of parsing via primary keys.** - -## Defining item as XPath -When using XPath expression, keep in mind the following: +When using item list expression, keep in mind the following: -- cursor columns are nested under `` element - object type attributes are nested under `` element - nested table and varray items type attributes are nested under `` elements -Example of a valid XPath parameter to include columns: `RN`, `A_Column`, `SOME_COL` in data comparison. +Example of a valid parameter to include columns: `RN`, `A_Column`, `SOME_COL` in data comparison. ```sql -procedure include_columns_as_xpath is +procedure include_col_list_eq is l_actual sys_refcursor; l_expected sys_refcursor; begin open l_expected for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL from dual a connect by level < 4; open l_actual for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL, a.* from all_objects a where rownum < 4; - ut.expect( l_actual ).to_equal( l_expected ).include( '/ROW/RN|/ROW/A_Column|/ROW/SOME_COL' ); + ut.expect( l_actual ).to_equal( l_expected ).include( 'RN,A_Column,SOME_COL' ); +end; + +procedure include_col_list_eq is + l_actual sys_refcursor; + l_expected sys_refcursor; +begin + open l_expected for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL from dual a connect by level < 4; + open l_actual for select rownum as rn, 'a' as "A_Column", 'x' SOME_COL, a.* from all_objects a where rownum < 6; + ut.expect( l_actual ).to_include( l_expected ).include( 'RN,A_Column,SOME_COL' ); end; ``` diff --git a/docs/userguide/expectations.md b/docs/userguide/expectations.md index b02605605..a1eb1f1dc 100644 --- a/docs/userguide/expectations.md +++ b/docs/userguide/expectations.md @@ -112,7 +112,7 @@ create or replace package body test_divide is procedure divides_numbers is begin - ut3.ut.expect(divide(6,2)).to_equal(3); + ut.expect(divide(6,2)).to_equal(3); end; procedure raises_divisor_exception is @@ -124,7 +124,7 @@ create or replace package body test_divide is end; / -exec ut3.ut.run('test_divide'); +exec ut.run('test_divide'); ``` For details see documentation of the [`--%throws` annotation.](annotations.md#throws-annotation) @@ -145,6 +145,7 @@ utPLSQL provides the following matchers to perform checks on the expected and ac - `be_null` - `be_true` - `equal` +- `include ` / `contain` - `have_count` - `match` @@ -363,68 +364,68 @@ end; create or replace package test_animals_getter is - --%suite(Animals getter tests) - - --%test(get_animal - returns a dog) - procedure test_variant_1_get_animal; - --%test(get_animal - returns a dog) - procedure test_variant_2_get_animal; - --%test(get_animal - returns a dog) - procedure test_variant_3_get_animal; - --%test(get_animal - returns a dog) - procedure test_variant_4_get_animal; - --%test(get_animal - returns a dog) - procedure test_variant_5_get_animal; + --%suite(Animals getter tests) + + --%test(get_animal - returns a dog) + procedure test_variant_1_get_animal; + --%test(get_animal - returns a dog) + procedure test_variant_2_get_animal; + --%test(get_animal - returns a dog) + procedure test_variant_3_get_animal; + --%test(get_animal - returns a dog) + procedure test_variant_4_get_animal; + --%test(get_animal - returns a dog) + procedure test_variant_5_get_animal; end; / create or replace package body test_animals_getter is - --The below tests perform exactly the same check. - --They use different syntax to achieve the goal. - procedure test_variant_1_get_animal is - l_actual varchar2(100) := 'a dog'; - l_expected varchar2(100); - begin - --Arrange - l_actual := 'a dog'; - --Act - l_expected := get_animal(); - --Assert - ut.expect( l_actual ).to_equal( l_expected ); - end; + --The below tests perform exactly the same check. + --They use different syntax to achieve the goal. + procedure test_variant_1_get_animal is + l_actual varchar2(100) := 'a dog'; + l_expected varchar2(100); + begin + --Arrange + l_actual := 'a dog'; + --Act + l_expected := get_animal(); + --Assert + ut.expect( l_actual ).to_equal( l_expected ); + end; - procedure test_variant_2_get_animal is - l_expected varchar2(100); - begin - --Act - l_expected := get_animal(); - --Assert - ut.expect( l_expected ).to_equal( 'a dog' ); - end; + procedure test_variant_2_get_animal is + l_expected varchar2(100); + begin + --Act + l_expected := get_animal(); + --Assert + ut.expect( l_expected ).to_equal( 'a dog' ); + end; - procedure test_variant_3_get_animal is - begin - --Act / Assert - ut.expect( get_animal() ).to_equal( 'a dog' ); - end; + procedure test_variant_3_get_animal is + begin + --Act / Assert + ut.expect( get_animal() ).to_equal( 'a dog' ); + end; - procedure test_variant_4_get_animal is - begin - --Act / Assert - ut.expect( get_animal() ).to_equal( 'a dog', a_nulls_are_equal => true ); - end; + procedure test_variant_4_get_animal is + begin + --Act / Assert + ut.expect( get_animal() ).to_equal( 'a dog', a_nulls_are_equal => true ); + end; - procedure test_variant_5_get_animal is - begin - --Act / Assert - ut.expect( get_animal() ).to_( equal( 'a dog' ) ); - end; + procedure test_variant_5_get_animal is + begin + --Act / Assert + ut.expect( get_animal() ).to_( equal( 'a dog' ) ); + end; - procedure test_variant_6_get_animal is - begin - --Act / Assert - ut.expect( get_animal() ).to_( equal( 'a dog', a_nulls_are_equal => true ) ); - end; + procedure test_variant_6_get_animal is + begin + --Act / Assert + ut.expect( get_animal() ).to_( equal( 'a dog', a_nulls_are_equal => true ) ); + end; end; ``` @@ -432,6 +433,273 @@ end; The `a_nulls_are_equal` parameter controls the behavior of a `null = null` comparison. To change the behavior of `NULL = NULL` comparison pass the `a_nulls_are_equal => false` to the `equal` matcher. +## include / contain + +This matcher supports only cursor comparison. It check if the give set contain all values from given subset. + +when comparing data using `include / contain` matcher, the data-types of columns for compared cursors must be exactly the same. + +The matcher supports all advanced comparison options as `equal` e.g. include , exclude, join_by. + +The matcher is successful when all of the values from expected results are included in actual data set. + +The matcher will cause a test to fail if any of expected values are not included in actual data set. + +![included_set](../images/venn21.gif) + +*Example 1*. + +```sql + procedure ut_refcursors is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select rownum as rn from dual a connect by level < 10; + open l_expected for select rownum as rn from dual a connect by level < 4 + union all select rownum as rn from dual a connect by level < 4; + + --Act + ut.expect(l_actual).to_include(l_expected); + end; +``` + +Will result in failure message + +```sql + 1) ut_refcursors + Actual: refcursor [ count = 9 ] was expected to include: refcursor [ count = 6 ] + Diff: + Rows: [ 3 differences ] + Missing: 3 + Missing: 2 + Missing: 1 +``` + +When duplicate rows are present in expected data set, actual data set must also include the same amount of duplicate. + +*Example 2.* + + + +```sql +create or replace package ut_duplicate_test is + + --%suite(Sample Test Suite) + + --%test(Ref Cursor contain duplicates) + procedure ut_duplicate_include; + +end ut_duplicate_test; +/ + +create or replace package body ut_duplicate_test is + procedure ut_duplicate_include is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + open l_expected for select mod(level,2) as rn from dual connect by level < 5; + open l_actual for select mod(level,8) as rn from dual connect by level < 9; + ut.expect(l_actual).to_include(l_expected); + end; + +end ut_duplicate_test; +``` + +Will result in failure test message + +```sql + 1) ut_duplicate_include + Actual: refcursor [ count = 8 ] was expected to include: refcursor [ count = 4 ] + Diff: + Rows: [ 2 differences ] + Missing: 0 + Missing: 1 +``` + + + +The negated version of `include / contain` ( `not_to_include`/ `not_to_contain` ) is successful only when all values from expected set are not part of actual (they are disjoint and there is no overlap). + + + +![not_overlapping_set](../images/venn22.gif) + +*Example 3.* + +Set 1 is defined as [ A , B , C ] + +*Set 2 is defined as [A , D , E ]* + +*Result : This will fail both of options to`to_include` and `not_to_include`* + + + +*Example 4.* + +Set 1 is defined as [ A , B , C , D ] + +*Set 2 is defined as [A , B , D ]* + +*Result : This will be success on option `to_include` and fail `not_to_include`* + + + +*Example 5. + +Set 1 is defined as [ A , B , C ] + +*Set 2 is defined as [D, E , F ]* + +*Result : This will be success on options `not_to_include` and fail `to_include`* + + + +Example usage + +```sql +create or replace package example_include is + --%suite(Include test) + + --%test( Cursor include data from another cursor) + procedure cursor_to_include; + + --%test( Cursor include data from another cursor) + procedure cursor_not_to_include; + + --%test( Cursor fail include) + procedure cursor_fail_include; + + --%test( Cursor fail not include) + procedure cursor_fail_not_include; +end; +/ + +create or replace package body example_include is + + procedure cursor_to_include is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for + select 'a' as name from dual union all + select 'b' as name from dual union all + select 'c' as name from dual union all + select 'd' as name from dual; + + open l_expected for + select 'a' as name from dual union all + select 'b' as name from dual union all + select 'c' as name from dual; + + --Act + ut.expect(l_actual).to_include(l_expected); + end; + + procedure cursor_not_to_include is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for + select 'a' as name from dual union all + select 'b' as name from dual union all + select 'c' as name from dual; + + open l_expected for + select 'd' as name from dual union all + select 'e' as name from dual union all + select 'f' as name from dual; + + --Act + ut.expect(l_actual).not_to_include(l_expected); + end; + + procedure cursor_fail_include is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for + select 'a' as name from dual union all + select 'b' as name from dual union all + select 'c' as name from dual; + + open l_expected for + select 'a' as name from dual union all + select 'd' as name from dual union all + select 'e' as name from dual; + + --Act + ut.expect(l_actual).to_include(l_expected); + end; + + procedure cursor_fail_not_include is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for + select 'a' as name from dual union all + select 'b' as name from dual union all + select 'c' as name from dual; + + open l_expected for + select 'a' as name from dual union all + select 'd' as name from dual union all + select 'e' as name from dual; + + --Act + ut.expect(l_actual).not_to_include(l_expected); + end; +end; +/ +``` + + + +Above execution will provide results as follow: + +```sql +Include test + Cursor include data from another cursor [.045 sec] + Cursor include data from another cursor [.039 sec] + Cursor fail include [.046 sec] (FAILED - 1) + Cursor fail not include [.043 sec] (FAILED - 2) + +Failures: + + 1) cursor_fail_include + Actual: refcursor [ count = 3 ] was expected to include: refcursor [ count = 3 ] + Diff: + Rows: [ 2 differences ] + Missing: d + Missing: e + at "UT3.EXAMPLE_INCLUDE.CURSOR_FAIL_INCLUDE", line 71 ut.expect(l_actual).to_include(l_expected); + + + 2) cursor_fail_not_include + Actual: (refcursor [ count = 3 ]) + Data-types: + CHAR + + Data: + a + b + c + was expected not to include:(refcursor [ count = 3 ]) + Data-types: + CHAR + + Data: + a + d + e + at "UT3.EXAMPLE_INCLUDE.CURSOR_FAIL_NOT_INCLUDE", line 94 ut.expect(l_actual).not_to_include(l_expected); +``` + + ## Comparing cursors, object types, nested tables and varrays @@ -442,17 +710,41 @@ utPLSQL is capable of comparing compound data-types including: ### Notes on comparison of compound data - 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. -- Cursors, nested table and varray types are compared as **ordered lists of elements**. If order of elements differ, expectation will fail. + +- Attributes in nested table and array types are compared as **ordered lists of elements**. If order of attributes in nested table and array differ, expectation will fail. + +- Columns in cursors are compared as **ordered list of elements** by default. If order of columns in cursor is not of importance the option has to be passed to enforce column order comparison ` unordered_columns` or a short version `uc` e.g. + + ```sql + procedure ut_refcursors1 is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + open l_actual for select 1 user_id,'s' a_col,'test' username from dual; + open l_expected for select 'test' username,'s' a_col,1 user_id from dual; + --Act + ut.expect(l_actual).to_equal(l_expected).join_by('USER_ID').unordered_columns(); + ut.expect(l_actual).to_equal(l_expected).join_by('USER_ID').uc(); + end; + ``` + - 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. + - 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. + - 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. -See [Comparing cursor data containing TIMESTAMP bind variables](#comparing-cursor-data-containing-timestamp-bind-variables) for examples. + See [Comparing cursor data containing TIMESTAMP bind variables](#comparing-cursor-data-containing-timestamp-bind-variables) for examples. + - To compare nested table/varray type you need to convert it to `anydata` by using `anydata.convertCollection()` + - To compare object type you need to convert it to `anydata` by using `anydata.convertObject()` + - 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 - 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)) - 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) - + utPLSQL offers advanced data-comparison options, for comparing compound data-types. The options allow you to: - define columns/attributes to exclude from comparison @@ -761,9 +1053,9 @@ Example below illustrates usage of `cast` operator to assure appropriate precisi ```sql drop table timestamps; create table timestamps ( - ts3 timestamp (3), - ts6 timestamp (6), - ts9 timestamp (9) + ts3 timestamp (3), + ts6 timestamp (6), + ts9 timestamp (9) ); create or replace package timestamps_api is @@ -880,23 +1172,21 @@ Since NULL is neither *true* nor *false*, both expectations will report failure. The matrix below illustrates the data types supported by different matchers. -| Matcher |blob |boolean|clob |date |number|timestamp|timestamp
with
timezone|timestamp
with
local
timezone|varchar2|interval
year
to
month|interval
day
to
second|cursor|nested
table
/ varray|object| -|:----------------------|:---:|:-----:|:---:|:---:|:----:|:-------:|:---------------------------:|:------------------------------------:|:------:|:-----------------------------:|:-----------------------------:|:----:|:-------------------------:|:----:| -|**be_not_null** | X | X | X | X | X | X | X | X | X | X | X | X | X | X | -|**be_null** | X | X | X | X | X | X | X | X | X | X | X | X | X | X | -|**be_false** | | X | | | | | | | | | | | | | -|**be_true** | | X | | | | | | | | | | | | | -|**be_greater_than** | | | | X | X | X | X | X | | X | X | | | | -|**be_greater_or_equal**| | | | X | X | X | X | X | | X | X | | | | -|**be_less_or_equal** | | | | X | X | X | X | X | | X | X | | | | -|**be_less_than** | | | | X | X | X | X | X | | X | X | | | | -|**be_between** | | | | X | X | X | X | X | X | X | X | | | | -|**equal** | X | X | X | X | X | X | X | X | X | X | X | X | X | X | -|**match** | | | X | | | | | | X | | | | | | -|**be_like** | | | X | | | | | | X | | | | | | -|**be_empty** | X | | X | | | | | | | | | X | X | | -|**have_count** | | | | | | | | | | | | X | X | | - - - +| Matcher | blob | boolean | clob | date | number | timestamp | timestamp
with
timezone | timestamp
with
local
timezone | varchar2 | interval
year
to
month | interval
day
to
second | cursor | nested
table
/ varray | object | +| :---------------------- | :--: | :-----: | :--: | :--: | :----: | :-------: | :---------------------------: | :------------------------------------: | :------: | :-----------------------------: | :-----------------------------: | :----: | :-------------------------: | :----: | +| **be_not_null** | X | X | X | X | X | X | X | X | X | X | X | X | X | X | +| **be_null** | X | X | X | X | X | X | X | X | X | X | X | X | X | X | +| **be_false** | | X | | | | | | | | | | | | | +| **be_true** | | X | | | | | | | | | | | | | +| **be_greater_than** | | | | X | X | X | X | X | | X | X | | | | +| **be_greater_or_equal** | | | | X | X | X | X | X | | X | X | | | | +| **be_less_or_equal** | | | | X | X | X | X | X | | X | X | | | | +| **be_less_than** | | | | X | X | X | X | X | | X | X | | | | +| **be_between** | | | | X | X | X | X | X | X | X | X | | | | +| **equal** | X | X | X | X | X | X | X | X | X | X | X | X | X | X | +| **include / contain** | | | | | | | | | | | | X | X | X | +| **match** | | | X | | | | | | X | | | | | | +| **be_like** | | | X | | | | | | X | | | | | | +| **be_empty** | X | | X | | | | | | | | | X | X | | +| **have_count** | | | | | | | | | | | | X | X | | diff --git a/source/api/contain.syn b/source/api/contain.syn new file mode 100644 index 000000000..34609ef8b --- /dev/null +++ b/source/api/contain.syn @@ -0,0 +1 @@ +create synonym contain for ut_include; diff --git a/source/api/include.syn b/source/api/include.syn new file mode 100644 index 000000000..855700e1e --- /dev/null +++ b/source/api/include.syn @@ -0,0 +1 @@ +create synonym include for ut_include; diff --git a/source/api/ut_runner.pkb b/source/api/ut_runner.pkb index 526b365c7..4ee05e083 100644 --- a/source/api/ut_runner.pkb +++ b/source/api/ut_runner.pkb @@ -49,6 +49,7 @@ create or replace package body ut_runner is ut_metadata.reset_source_definition_cache; ut_utils.read_cache_to_dbms_output(); ut_coverage_helper.cleanup_tmp_table(); + ut_compound_data_helper.cleanup_diff(); if not a_force_manual_rollback then rollback; end if; diff --git a/source/core/ut_utils.pks b/source/core/ut_utils.pks index 0b8121e57..26dd1f253 100644 --- a/source/core/ut_utils.pks +++ b/source/core/ut_utils.pks @@ -114,6 +114,10 @@ create or replace package ut_utils authid definer is 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); + gc_max_storage_varchar2_len constant integer := 4000; gc_max_output_string_length constant integer := 4000; gc_max_input_string_length constant integer := gc_max_output_string_length - 2; --we need to remove 2 chars for quotes around string @@ -126,6 +130,9 @@ create or replace package ut_utils authid definer is 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, diff --git a/source/create_synonyms_and_grants_for_public.sql b/source/create_synonyms_and_grants_for_public.sql index b8daca313..b745449bf 100644 --- a/source/create_synonyms_and_grants_for_public.sql +++ b/source/create_synonyms_and_grants_for_public.sql @@ -112,20 +112,24 @@ prompt Creating synonyms for UTPLSQL objects in &&ut3_owner schema to PUBLIC create public synonym ut_expectation for &&ut3_owner..ut_expectation; create public synonym ut_expectation_compound for &&ut3_owner..ut_expectation_compound; -create public synonym be_between for &&ut3_owner..ut_be_between; -create public synonym be_empty for &&ut3_owner..ut_be_empty; -create public synonym be_false for &&ut3_owner..ut_be_false; -create public synonym be_greater_or_equal for &&ut3_owner..ut_be_greater_or_equal; -create public synonym be_greater_than for &&ut3_owner..ut_be_greater_than; -create public synonym be_less_or_equal for &&ut3_owner..ut_be_less_or_equal; -create public synonym be_less_than for &&ut3_owner..ut_be_less_than; -create public synonym be_like for &&ut3_owner..ut_be_like; -create public synonym be_not_null for &&ut3_owner..ut_be_not_null; -create public synonym be_null for &&ut3_owner..ut_be_null; -create public synonym be_true for &&ut3_owner..ut_be_true; -create public synonym equal for &&ut3_owner..ut_equal; + +create public synonym be_between for &&ut3_owner..be_between; +create public synonym be_empty for &&ut3_owner..be_empty; +create public synonym be_false for &&ut3_owner..be_false; +create public synonym be_greater_or_equal for &&ut3_owner..be_greater_or_equal; +create public synonym be_greater_than for &&ut3_owner..be_greater_than; +create public synonym be_less_or_equal for &&ut3_owner..be_less_or_equal; +create public synonym be_less_than for &&ut3_owner..be_less_than; +create public synonym be_like for &&ut3_owner..be_like; +create public synonym be_not_null for &&ut3_owner..be_not_null; +create public synonym be_null for &&ut3_owner..be_null; +create public synonym be_true for &&ut3_owner..be_true; +create public synonym contain for &&ut3_owner..include; +create public synonym equal for &&ut3_owner..equal; create public synonym have_count for &&ut3_owner..have_count; -create public synonym match for &&ut3_owner..ut_match; +create public synonym include for &&ut3_owner..include; +create public synonym match for &&ut3_owner..match; + create public synonym ut for &&ut3_owner..ut; create public synonym ut_runner for &&ut3_owner..ut_runner; create public synonym ut_teamcity_reporter for &&ut3_owner..ut_teamcity_reporter; diff --git a/source/create_user_synonyms.sql b/source/create_user_synonyms.sql index 9a087f61a..3c75c590b 100644 --- a/source/create_user_synonyms.sql +++ b/source/create_user_synonyms.sql @@ -55,6 +55,7 @@ prompt Creating synonyms for UTPLSQL objects in &&ut3_owner schema to user &&ut3 create or replace synonym &ut3_user..ut_expectation for &&ut3_owner..ut_expectation; create or replace synonym &ut3_user..ut_expectation_compound for &&ut3_owner..ut_expectation_compound; + create or replace synonym &ut3_user..be_between for &&ut3_owner..be_between; create or replace synonym &ut3_user..be_empty for &&ut3_owner..be_empty; create or replace synonym &ut3_user..be_false for &&ut3_owner..be_false; @@ -69,6 +70,7 @@ create or replace synonym &ut3_user..be_true for &&ut3_owner..be_true; create or replace synonym &ut3_user..equal for &&ut3_owner..equal; create or replace synonym &ut3_user..have_count for &&ut3_owner..have_count; create or replace synonym &ut3_user..match for &&ut3_owner..match; + create or replace synonym &ut3_user..ut for &&ut3_owner..ut; create or replace synonym &ut3_user..ut_runner for &&ut3_owner..ut_runner; create or replace synonym &ut3_user..ut_teamcity_reporter for &&ut3_owner..ut_teamcity_reporter; diff --git a/source/expectations/data_values/ut_compound_data_diff_tmp.sql b/source/expectations/data_values/ut_compound_data_diff_tmp.sql index a7959d4aa..242d6c2fc 100644 --- a/source/expectations/data_values/ut_compound_data_diff_tmp.sql +++ b/source/expectations/data_values/ut_compound_data_diff_tmp.sql @@ -12,15 +12,15 @@ create global temporary table ut_compound_data_diff_tmp( See the License for the specific language governing permissions and limitations under the License. */ - diff_id raw(128), - item_no integer, - pk_hash raw(128), - item_hash raw(128), + diff_id raw(128), + act_data_id raw(32), + exp_data_id raw(32), + act_item_data xmltype, + exp_item_data xmltype, + item_no integer, duplicate_no integer, - constraint ut_compound_data_diff_tmp_uk1 unique (diff_id,duplicate_no,item_no,item_hash, pk_hash), + constraint ut_compound_data_diff_tmp_uk1 unique (diff_id,duplicate_no,item_no), constraint ut_compound_data_diff_tmp_chk check( - item_no is not null and pk_hash is null and duplicate_no is null - or item_no is null and item_hash is not null and duplicate_no is not null - or item_no is null and pk_hash is not null and duplicate_no is not null + item_no is not null ) ) on commit preserve rows; diff --git a/source/expectations/data_values/ut_compound_data_helper.pkb b/source/expectations/data_values/ut_compound_data_helper.pkb index 0fba5b0e7..59e6f9c63 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pkb +++ b/source/expectations/data_values/ut_compound_data_helper.pkb @@ -16,25 +16,12 @@ create or replace package body ut_compound_data_helper is limitations under the License. */ - g_user_defined_type pls_integer := dbms_sql.user_defined_type; - - function get_column_info_xml(a_column_details ut_key_anyval_pair) return xmltype is - l_result varchar2(4000); - l_res xmltype; - l_data ut_data_value := a_column_details.value; - l_key varchar2(4000) := ut_utils.xmlgen_escaped_string(a_column_details.KEY); - begin - l_result := '<'||l_key||' xml_valid_name="'||l_key||'">'; - if l_data is of(ut_data_value_xmltype) then - l_result := l_result || (treat(l_data as ut_data_value_xmltype).to_string); - else - l_result := l_result || ut_utils.xmlgen_escaped_string((treat(l_data as ut_data_value_varchar2).data_value)); - end if; - - l_result := l_result ||''; - return xmltype(l_result); - end; - + g_diff_count integer; + gc_xpath_extract_reg constant varchar2(50) := '^((/ROW/)|^(//)|^(/\*/))?(.*)'; + type t_type_name_map is table of varchar2(128) index by binary_integer; + g_type_name_map t_type_name_map; + g_anytype_name_map t_type_name_map; + function get_columns_filter( a_exclude_xpath varchar2, a_include_xpath varchar2, a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data' @@ -56,236 +43,476 @@ create or replace package body ut_compound_data_helper is end if; return l_filter; end; - - /** - * Current get column filter shaving off ROW tag during extract, this not working well with include and XMLTABLE option - * so when there is extract we artificially inject removed tag - **/ - function get_columns_row_filter( - a_exclude_xpath varchar2, a_include_xpath varchar2, - a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data' - ) return varchar2 is - l_filter varchar2(32767); - l_source_column varchar2(500) := a_table_alias||'.'||a_column_alias; + + function get_columns_diff_ordered(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab) + return tt_column_diffs is + l_results tt_column_diffs; begin - -- this SQL statement is constructed in a way that we always get the same number and ordering of substitution variables - -- That is, we always get: l_exclude_xpath, l_include_xpath - -- regardless if the variables are NULL (not to be used) or NOT NULL and will be used for filtering - if a_exclude_xpath is null and a_include_xpath is null then - l_filter := ':l_exclude_xpath, :l_include_xpath, '||l_source_column||' as '||a_column_alias; - elsif a_exclude_xpath is not null and a_include_xpath is null then - l_filter := 'deletexml( '||l_source_column||', :l_exclude_xpath ) as '||a_column_alias||', :l_include_xpath'; - elsif a_exclude_xpath is null and a_include_xpath is not null then - l_filter := ':l_exclude_xpath, xmlelement("ROW",extract( '||l_source_column||', :l_include_xpath )) as '||a_column_alias; - elsif a_exclude_xpath is not null and a_include_xpath is not null then - l_filter := 'xmlelement("ROW",extract( deletexml( '||l_source_column||', :l_exclude_xpath ), :l_include_xpath )) as '||a_column_alias; - end if; - return l_filter; + with + expected_cols as + (select access_path exp_column_name,column_position exp_col_pos, + replace(column_type,'VARCHAR2','CHAR') exp_col_type_compare, column_type exp_col_type + from table(a_expected)), + actual_cols as + (select access_path act_column_name,column_position act_col_pos, + replace(column_type,'VARCHAR2','CHAR') act_col_type_compare, column_type act_col_type + from table(a_actual)), + joined_cols as + (select e.*,a.*, + 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 + 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 + bulk collect into l_results + 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 + --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) + order by exp_col_pos, act_col_pos; + return l_results; end; - - function get_columns_diff( - a_expected xmltype, a_actual xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2 - ) return tt_column_diffs is - l_column_filter varchar2(32767); - l_sql varchar2(32767); + + function get_columns_diff_unordered(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab) + return tt_column_diffs is l_results tt_column_diffs; begin - l_column_filter := get_columns_row_filter(a_exclude_xpath, a_include_xpath); - --CARDINALITY hints added to address issue: https://github.com/utPLSQL/utPLSQL/issues/752 - l_sql := q'[ - with - expected_cols as ( select :a_expected as item_data from dual ), - actual_cols as ( select :a_actual as item_data from dual ), - expected_cols_info as ( - select e.*, - replace(expected_type,'VARCHAR2','CHAR') expected_type_compare - from ( - select /*+ CARDINALITY(xt 100) */ - rownum expected_pos, - xt.name expected_name, - xt.type expected_type - from (select ]'||l_column_filter||q'[ from expected_cols ucd) x, - xmltable( - '/ROW/*' - passing x.item_data - columns - name varchar2(4000) PATH '@xml_valid_name', - type varchar2(4000) PATH '/' - ) xt - ) e - ), - actual_cols_info as ( - select a.*, - replace(actual_type,'VARCHAR2','CHAR') actual_type_compare - from (select /*+ CARDINALITY(xt 100) */ - rownum actual_pos, - xt.name actual_name, - xt.type actual_type - from (select ]'||l_column_filter||q'[ from actual_cols ucd) x, - xmltable('/ROW/*' - passing x.item_data - columns - name varchar2(4000) path '@xml_valid_name', - type varchar2(4000) path '/' - ) xt - ) a - ), - joined_cols as ( - select e.*, a.*, - row_number() over(partition by case when actual_pos + expected_pos is not null then 1 end order by actual_pos) a_pos_nn, - row_number() over(partition by case when actual_pos + expected_pos is not null then 1 end order by expected_pos) e_pos_nn - from expected_cols_info e - full outer join actual_cols_info a on e.expected_name = a.actual_name - ) + with + expected_cols as + (select access_path exp_column_name,column_position exp_col_pos, + replace(column_type,'VARCHAR2','CHAR') exp_col_type_compare, column_type exp_col_type + from table(a_expected)), + actual_cols as + (select access_path act_column_name,column_position act_col_pos, + replace(column_type,'VARCHAR2','CHAR') act_col_type_compare, column_type act_col_type + from table(a_actual)), + joined_cols as + (select e.*,a.* + from expected_cols e + full outer join actual_cols a on e.exp_column_name = a.act_column_name) select case - when expected_pos is null and actual_pos is not null then '+' - when expected_pos is not null and actual_pos is null then '-' - when expected_type_compare != actual_type_compare then 't' + 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, - expected_name, expected_type, expected_pos, - actual_name, actual_type, actual_pos + exp_column_name, exp_col_type, exp_col_pos, + act_column_name, act_col_type, act_col_pos + bulk collect into l_results from joined_cols --column is unexpected (extra) or missing - where actual_pos is null or expected_pos is null + where act_col_pos is null or exp_col_pos is null --column type is not matching (except CHAR/VARCHAR2) - or actual_type_compare != expected_type_compare - --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 expected_pos != actual_pos) - order by expected_pos, actual_pos]'; - execute immediate l_sql - bulk collect into l_results - using a_expected, a_actual, a_exclude_xpath, a_include_xpath, a_exclude_xpath, a_include_xpath; - + or act_col_type_compare != exp_col_type_compare + order by exp_col_pos, act_col_pos; return l_results; end; + + 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 + begin + return + case + when a_order_enforced then get_columns_diff_ordered(a_expected,a_actual) + else get_columns_diff_unordered(a_expected,a_actual) + end; + end; function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob is l_pk_value clob; begin select replace((extract(a_item_data,a_join_by_xpath).getclobval()),chr(10)) into l_pk_value from dual; return l_pk_value; + end; + + procedure generate_not_equal_stmt( + a_data_info ut_cursor_column, a_pk_table ut_varchar2_list, + a_not_equal_stmt in out nocopy clob, a_col_name 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 := l_sql_stmt || case when a_not_equal_stmt is null then null else ' or ' end + ||' (decode(a.'||a_col_name||','||' e.'||a_col_name||',1,0) = 0)'; + ut_utils.append_to_clob(a_not_equal_stmt,l_sql_stmt); + end if; end; + + procedure generate_join_by_stmt( + a_data_info ut_cursor_column, a_pk_table ut_varchar2_list, + a_join_by_stmt in out nocopy clob, a_col_name 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 := case when a_join_by_stmt is null then null else ' and ' end; + l_sql_stmt := l_sql_stmt ||' a.'||a_col_name||q'[ = ]'||' e.'||a_col_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; + ut_utils.append_to_clob(a_join_by_stmt,l_sql_stmt); + end if; + end; + + procedure generate_equal_sql(a_equal_stmt in out nocopy clob,a_col_name in varchar2) is + l_sql_stmt varchar2(32767); + begin + l_sql_stmt := case when a_equal_stmt is null then null else ' and ' end ||' a.'||a_col_name||q'[ = ]'||' e.'||a_col_name; + ut_utils.append_to_clob(a_equal_stmt,l_sql_stmt); + end; + + procedure generate_partition_stmt( + a_data_info ut_cursor_column, a_partition_stmt in out nocopy clob, + a_pk_table in ut_varchar2_list,a_col_name in varchar2,a_alias varchar2 := 'ucd.' + ) is + l_alias varchar2(10) := a_alias; + 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 := case when a_partition_stmt is null then null else ',' end; + l_sql_stmt := l_sql_stmt ||l_alias||a_col_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; + else + l_sql_stmt := case when a_partition_stmt is null then null else ',' end ||l_alias||a_col_name; + end if; + ut_utils.append_to_clob(a_partition_stmt,l_sql_stmt); + end; + + procedure generate_select_stmt(a_data_info ut_cursor_column,a_sql_stmt in out nocopy clob, a_col_name varchar2,a_alias varchar2 := 'ucd.') 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_col_name||'.getClobVal()) as '||a_col_name ; + else + l_col_syntax := l_alias||a_col_name||' as '|| a_col_name; + end if; + ut_utils.append_to_clob(a_sql_stmt,','||l_col_syntax); + end; + + procedure generate_xmltab_stmt(a_data_info ut_cursor_column,a_sql_stmt in out nocopy clob, a_col_name varchar2) is + l_sql_stmt varchar2(32767); + 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 = 'DATE' then + l_col_type := 'TIMESTAMP'; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('TIMESTAMP','TIMESTAMP WITH TIME ZONE') then + l_col_type := a_data_info.column_type; + --TODO : Oracle bug : https://community.oracle.com/thread/1957521 + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type = 'TIMESTAMP WITH LOCAL TIME ZONE' then + l_col_type := 'VARCHAR2(50)'; + elsif a_data_info.is_sql_diffable = 1 and a_data_info.column_type in ('INTERVAL DAY TO SECOND','INTERVAL YEAR TO MONTH') then + 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; + l_sql_stmt := case when a_sql_stmt is null then '' else ', ' end ||a_col_name||' '||l_col_type||q'[ PATH ']'||a_data_info.access_path||q'[']'; + ut_utils.append_to_clob(a_sql_stmt, l_sql_stmt); + end; + + 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, + 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, + a_not_equal_stmt out nocopy clob + ) is + l_cursor_info ut_cursor_column_tab := a_data_info; + l_partition_tmp clob; + l_col_name varchar2(100); + begin + if l_cursor_info is not null then + --Parition by piece + ut_utils.append_to_clob(a_partition_stmt,', row_number() over (partition by '); + for i in 1..l_cursor_info.count loop + if l_cursor_info(i).has_nested_col = 0 then + l_col_name := l_cursor_info(i).transformed_name; + --Get XMLTABLE column list + generate_xmltab_stmt(l_cursor_info(i),a_xml_stmt,l_col_name); + --Get Select statment list of columns + generate_select_stmt(l_cursor_info(i),a_select_stmt,l_col_name); + --Get columns by which we partition + generate_partition_stmt(l_cursor_info(i),l_partition_tmp,a_pk_table,l_col_name); + --Get equal statement + generate_equal_sql(a_equal_stmt,l_col_name); + --Generate join by stmt + generate_join_by_stmt(l_cursor_info(i),a_pk_table,a_join_by_stmt,l_col_name); + --Generate not equal stmt + generate_not_equal_stmt(l_cursor_info(i),a_pk_table,a_not_equal_stmt,l_col_name); + end if; + end loop; + --Finish partition by + ut_utils.append_to_clob(a_partition_stmt,l_partition_tmp||' order by '||l_partition_tmp||' ) dup_no '); + else + --Partition by piece when no data + ut_utils.append_to_clob(a_partition_stmt,', 1 dup_no '); + end if; + end; + + procedure get_act_and_exp_set( + a_current_stmt in out nocopy clob, a_partition_stmt clob, a_select_stmt clob, + a_xmltable_stmt clob, a_unordered boolean,a_type varchar2 + ) is + l_temp_string varchar2(32767); + l_ut_owner varchar2(250) := ut_utils.ut_owner; + begin + ut_utils.append_to_clob(a_current_stmt, a_partition_stmt); - function get_rows_diff( + l_temp_string := 'from (select ucd.item_data '; + ut_utils.append_to_clob(a_current_stmt,l_temp_string); + ut_utils.append_to_clob(a_current_stmt, a_select_stmt); + + l_temp_string := ',x.data_id, ' + || case when not a_unordered then 'position + x.item_no ' else 'rownum ' end + ||'item_no from ' || l_ut_owner || '.ut_compound_data_tmp x,' + ||q'[xmltable('/ROWSET/ROW' passing x.item_data columns ]' ; + ut_utils.append_to_clob(a_current_stmt,l_temp_string); + + ut_utils.append_to_clob(a_current_stmt,a_xmltable_stmt); + ut_utils.append_to_clob(a_current_stmt,case when a_xmltable_stmt is null then '' else ',' end||q'[ item_data xmltype PATH '*']'); + if not a_unordered then + ut_utils.append_to_clob(a_current_stmt,', POSITION for ordinality '); + end if; + ut_utils.append_to_clob(a_current_stmt,' ) ucd where data_id = :'||a_type||'_guid ) ucd ) '); + end; + + + function gen_compare_sql( + a_inclusion_type boolean, a_is_negated boolean, a_unordered boolean, + a_other ut_data_value_refcursor := null, a_join_by_list ut_varchar2_list := ut_varchar2_list() + ) return clob is + l_compare_sql clob; + l_temp_string varchar2(32767); + + l_xmltable_stmt clob; + l_select_stmt clob; + l_partition_stmt clob; + l_equal_stmt clob; + l_join_on_stmt clob; + l_not_equal_stmt clob; + + 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; + + begin + dbms_lob.createtemporary(l_compare_sql, true); + gen_sql_pieces_out_of_cursor(a_other.cursor_details.cursor_columns_info, a_join_by_list, + l_xmltable_stmt, l_select_stmt, l_partition_stmt, l_equal_stmt, + l_join_on_stmt, l_not_equal_stmt); + + l_temp_string := 'with exp as ( select ucd.* '; + ut_utils.append_to_clob(l_compare_sql, l_temp_string); + get_act_and_exp_set(l_compare_sql, l_partition_stmt,l_select_stmt, l_xmltable_stmt, a_unordered,'exp'); + + + l_temp_string :=',act as ( select ucd.* '; + ut_utils.append_to_clob(l_compare_sql, l_temp_string); + get_act_and_exp_set(l_compare_sql, l_partition_stmt,l_select_stmt, l_xmltable_stmt, a_unordered,'act'); + + l_temp_string := ' 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, '|| + case when a_unordered then 'rownum item_no' else 'nvl(e.item_no,a.item_no) item_no' end ||', nvl(e.dup_no,a.dup_no) dup_no ' + ||'from act a '||get_join_type(a_inclusion_type,a_is_negated)||' exp e on ( '; + ut_utils.append_to_clob(l_compare_sql,l_temp_string); + + if a_unordered then + ut_utils.append_to_clob(l_compare_sql,' e.dup_no = a.dup_no and '); + end if; + + if (a_join_by_list.count = 0) and a_unordered then + -- If no key defined do the join on all columns + ut_utils.append_to_clob(l_compare_sql,l_equal_stmt); + elsif (a_join_by_list.count > 0) and a_unordered then + -- If key defined do the join or these and where on diffrences + ut_utils.append_to_clob(l_compare_sql,l_join_on_stmt); + elsif not a_unordered then + ut_utils.append_to_clob(l_compare_sql, 'a.item_no = e.item_no ' ); + end if; + + ut_utils.append_to_clob(l_compare_sql,' ) where '); + + if (a_join_by_list.count > 0) and (a_unordered) and (not a_is_negated) then + if l_not_equal_stmt is not null then + ut_utils.append_to_clob(l_compare_sql,' ( '||l_not_equal_stmt||' ) or '); + end if; + elsif not a_unordered and l_not_equal_stmt is not null then + ut_utils.append_to_clob(l_compare_sql,' ( '||l_not_equal_stmt||' ) or '); + end if; + + --If its inlcusion we expect a actual set to fully match and have no extra elements over expected + if a_inclusion_type and not(a_is_negated) then + l_temp_string := ' ( a.data_id is null ) '; + elsif a_inclusion_type and a_is_negated then + l_temp_string := ' 1 = 1 '; + else + l_temp_string := ' (a.data_id is null or e.data_id is null) '; + end if; + ut_utils.append_to_clob(l_compare_sql,l_temp_string); + 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_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2, - a_join_by_xpath varchar2 + a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false ) return tt_row_diffs is - l_column_filter varchar2(32767); - l_results tt_row_diffs; + 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_column_filter := get_columns_row_filter(a_exclude_xpath,a_include_xpath); + 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, s.column_value.getclobval() col_val + from ( + select exp_data_id, extract( ucd.exp_item_data, :column_path ) exp_item_data, item_no, + ut_compound_data_helper.get_pk_value(:join_by, ucd.exp_item_data) 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,'/*') ) ) 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, s.column_value.getclobval() col_val + from ( + select act_data_id, extract( ucd.act_item_data, :column_path ) act_item_data, item_no, + ut_compound_data_helper.get_pk_value(:join_by, ucd.act_item_data) 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,'/*') ) ) 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 ( ]'; + + if a_unordered then + l_sql := l_sql || 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 + l_sql := l_sql || 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 if; - /** - * Since its unordered search we cannot select max rows from diffs as we miss some comparision records - * We will restrict output on higher level of select - * NO_MERGE hint was introduced to prevent optimizer from merging views and rewriting query which in some cases - * lead to second value being null depend on execution plan that been chosen - **/ - execute immediate q'[ - with diff_info as (select item_hash,pk_hash,duplicate_no from ut_compound_data_diff_tmp ucdc where diff_id = :diff_guid) - select rn,diff_type,diffed_row,pk_value from - ( - select - diff_type, diffed_row, - dense_rank() over (order by case when diff_type in ('Extra','Missing') then diff_type end, - case when diff_type in ('Actual','Expected') then pk_hash end, - case when diff_type in ('Extra','Missing') then pk_hash end, - case when diff_type in ('Actual','Expected') then diff_type end) rn, - pk_value, pk_hash - from - ( - select diff_type,diffed_row,pk_hash,pk_value from - (select diff_type,data_item diffed_row,pk_hash,pk_value - from - (select /*+NO_MERGE*/ nvl(exp.pk_hash, act.pk_hash) pk_hash,nvl(exp.pk_value, act.pk_value) pk_value, - xmlserialize(content exp.row_data no indent) exp_item, - xmlserialize(content act.row_data no indent) act_item - from - (select ucd.* - from - (select ucd.column_value row_data, - r.item_hash row_hash, - r.pk_hash , - r.duplicate_no, - ucd.column_value.getclobval() col_val, - ucd.column_value.getRootElement() col_name, - ut_compound_data_helper.get_pk_value(:join_xpath,r.item_data) pk_value - from - (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_hash, i.pk_hash, i.duplicate_no - from ut_compound_data_tmp ucd, - diff_info i - where ucd.data_id = :self_guid - and ucd.item_hash = i.item_hash - ) r, - table( xmlsequence( extract(r.item_data,'/*/*') ) ) ucd - ) ucd - ) exp - join ( - select ucd.* - from - (select ucd.column_value row_data, - r.item_hash row_hash, - r.pk_hash , - r.duplicate_no, - ucd.column_value.getclobval() col_val, - ucd.column_value.getRootElement() col_name, - ut_compound_data_helper.get_pk_value(:join_xpath,r.item_data) pk_value - from - (select ]'||l_column_filter||q'[, ucd.item_no, ucd.item_hash, i.pk_hash, i.duplicate_no - from ut_compound_data_tmp ucd, - diff_info i - where ucd.data_id = :other_guid - and ucd.item_hash = i.item_hash - ) r, - table( xmlsequence( extract(r.item_data,'/*/*') ) ) ucd - ) ucd - ) act - on exp.pk_hash = act.pk_hash and exp.col_name = act.col_name - and exp.duplicate_no = act.duplicate_no - where dbms_lob.compare(exp.col_val, act.col_val) != 0 - ) - unpivot ( data_item for diff_type in (exp_item as 'Expected:', act_item as 'Actual:') ) - ) - union all - select case when exp.pk_hash is null then 'Extra' else 'Missing' end as diff_type, - xmlserialize(content nvl(exp.item_data, act.item_data) no indent) diffed_row, - coalesce(exp.pk_hash,act.pk_hash) pk_hash, - coalesce(exp.pk_value,act.pk_value) pk_value - from (select extract(deletexml(ucd.item_data, :join_by),'/*/*') item_data,i.pk_hash, - ut_compound_data_helper.get_pk_value(:join_by,item_data) pk_value - from ut_compound_data_tmp ucd, - diff_info i - where ucd.data_id = :self_guid - and ucd.item_hash = i.item_hash - ) exp - full outer join ( - select extract(deletexml(ucd.item_data, :join_by),'/*/*') item_data,i.pk_hash, - ut_compound_data_helper.get_pk_value(:join_by,item_data) pk_value - from ut_compound_data_tmp ucd, - diff_info i - where ucd.data_id = :other_guid - and ucd.item_hash = i.item_hash - )act - on exp.pk_hash = act.pk_hash - where exp.pk_hash is null or act.pk_hash is null - ) - ) where rn <= :max_rows - order by rn, pk_hash, diff_type - ]' - bulk collect into l_results - using a_diff_id, - a_join_by_xpath, - a_exclude_xpath, a_include_xpath, a_expected_dataset_guid, - a_join_by_xpath, - a_exclude_xpath, a_include_xpath, a_actual_dataset_guid, - a_join_by_xpath,a_join_by_xpath,a_expected_dataset_guid,a_join_by_xpath,a_join_by_xpath, a_actual_dataset_guid, - a_max_rows; + l_sql := l_sql || 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,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 + ,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,]'; + + if a_enforce_column_order then + l_sql := l_sql ||q'[case when final_order = 1 then rn else rnk end, + case when final_order = 1 then rnk else rn end ]'; + elsif not(a_enforce_column_order) and not(a_unordered) then + l_sql := l_sql ||q'[case when final_order = 1 then rn else rnk end, + case when final_order = 1 then rnk else rn end ]'; + elsif a_unordered then + l_sql := l_sql ||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 + ]'; + end if; + + execute immediate l_sql + bulk collect into l_results + using l_exp_extract_xpath,l_join_xpath,a_diff_id, a_expected_dataset_guid, + l_act_extract_xpath,l_join_xpath,a_diff_id, a_actual_dataset_guid, + l_join_xpath,l_join_xpath,a_diff_id; return l_results; end; - + function get_rows_diff( a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2 @@ -361,238 +588,294 @@ create or replace package body ut_compound_data_helper is return l_results; end; - function get_rows_diff_unordered( - a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, - a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2 - ) return tt_row_diffs is - l_column_filter varchar2(32767); - l_results tt_row_diffs; + function get_hash(a_data raw, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is begin - l_column_filter := get_columns_filter(a_exclude_xpath,a_include_xpath); - - /** - * Since its unordered search we cannot select max rows from diffs as we miss some comparision records - * We will restrict output on higher level of select - */ - execute immediate q'[with - diff_info as (select item_hash,duplicate_no from ut_compound_data_diff_tmp ucdc where diff_id = :diff_guid) - select duplicate_no, - diffed_type, - diffed_row, - null pk_value - from - (select - coalesce(exp.duplicate_no,act.duplicate_no) duplicate_no, - case - when act.row_hash is null then - 'Missing:' - else 'Extra:' - end diffed_type, - case when exp.row_hash is null then - xmlserialize(content act.row_data no indent) - when act.row_hash is null then - xmlserialize(content exp.row_data no indent) - end diffed_row - from (select ucd.* - from (select ucd.column_value row_data, - r.item_hash row_hash, - r.duplicate_no - from (select ]'||l_column_filter||q'[, ucd.item_no, i.item_hash, i.duplicate_no - from ut_compound_data_tmp ucd, - diff_info i - where ucd.data_id = :self_guid - and ucd.item_hash = i.item_hash - and ucd.duplicate_no = i.duplicate_no - ) r, - table( xmlsequence( extract(r.item_data,'/*') ) ) ucd - ) ucd - ) exp - full outer join - (select ucd.* - from (select ucd.column_value row_data, - r.item_hash row_hash, - r.duplicate_no - from (select ]'||l_column_filter||q'[, ucd.item_no, i.item_hash, i.duplicate_no - from ut_compound_data_tmp ucd, - diff_info i - where ucd.data_id = :other_guid - and ucd.item_hash = i.item_hash - and ucd.duplicate_no = i.duplicate_no - ) r, - table( xmlsequence( extract(r.item_data,'/*') ) ) ucd - ) ucd - ) act - on exp.row_hash = act.row_hash - and exp.duplicate_no = act.duplicate_no - where exp.row_hash is null or act.row_hash is null - order by diffed_type, coalesce(exp.row_hash,act.row_hash), duplicate_no - ) - where rownum < :max_rows ]' - bulk collect into l_results - using a_diff_id, - a_exclude_xpath, a_include_xpath, a_expected_dataset_guid, - a_exclude_xpath, a_include_xpath, a_actual_dataset_guid, - a_max_rows; - - return l_results; + 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_failure_for_all,'Failure to insert a diff tmp data.'); end; - function compare_type(a_join_by_xpath in varchar2,a_unordered boolean) return varchar2 is - begin - case - when a_join_by_xpath is not null then - return gc_compare_join_by; - when a_unordered then - return gc_compare_unordered; - else - return gc_compare_normal; - end case; - end; + procedure set_rows_diff(a_rows_diff integer) is + begin + g_diff_count := a_rows_diff; + end; - function get_rows_diff( - a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, - a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2, - a_join_by_xpath varchar2,a_unorderdered boolean - ) return tt_row_diffs is - l_results tt_row_diffs; - l_compare_type varchar2(10):= compare_type(a_join_by_xpath,a_unorderdered); + procedure cleanup_diff is + begin + g_diff_count := 0; + end; + + function get_rows_diff_count return integer is begin - case - when l_compare_type = gc_compare_join_by then - return get_rows_diff(a_expected_dataset_guid, a_actual_dataset_guid, a_diff_id, - a_max_rows, a_exclude_xpath, a_include_xpath ,a_join_by_xpath); - when l_compare_type = gc_compare_unordered then - return get_rows_diff_unordered(a_expected_dataset_guid, a_actual_dataset_guid, a_diff_id, - a_max_rows, a_exclude_xpath, a_include_xpath); - else - return get_rows_diff(a_expected_dataset_guid, a_actual_dataset_guid, a_diff_id, - a_max_rows, a_exclude_xpath, a_include_xpath); - end case; + return g_diff_count; + end; + --Filter out columns from cursor based on include (exists) or exclude (not exists) + function filter_out_cols( + a_cursor_info ut_cursor_column_tab, + a_current_list ut_varchar2_list, + a_include boolean := true + ) return ut_cursor_column_tab is + l_result ut_cursor_column_tab := ut_cursor_column_tab(); + l_filter_sql varchar2(32767); + begin + l_filter_sql := + q'[with + coltab as ( + select i.parent_name,i.access_path,i.has_nested_col,i.transformed_name,i.hierarchy_level,i.column_position , + i.xml_valid_name,i.column_name,i.column_type,i.column_type_name ,i.column_schema,i.column_len,i.is_sql_diffable ,i.is_collection + from table(:cursor_info) i), + filter as (select column_value from table(:current_list)) + select ut_cursor_column(i.parent_name,i.access_path,i.has_nested_col,i.transformed_name,i.hierarchy_level,i.column_position , + i.xml_valid_name,i.column_name,i.column_type,i.column_type_name ,i.column_schema,i.column_len,i.is_sql_diffable ,i.is_collection) + from coltab i where ]'||case when a_include then null else ' not ' end + ||q'[exists (select 1 from filter f where regexp_like(i.access_path, '^'||f.column_value||'($|/.*)'))]'; + + execute immediate l_filter_sql bulk collect into l_result using a_cursor_info,a_current_list; + return l_result; + end; + + function get_missing_filter_columns(a_cursor_info ut_cursor_column_tab, a_column_filter_list ut_varchar2_list) + return ut_varchar2_list is + l_result ut_varchar2_list := ut_varchar2_list(); + begin + select fl.column_value + bulk collect into l_result + from table(a_column_filter_list) fl + where not exists (select 1 from table(a_cursor_info) c where regexp_like(c.access_path, '^'||fl.column_value||'($|/.*)')); + return l_result; + end; + + function get_missing_pk(a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab, a_current_list ut_varchar2_list) + return tt_missing_pk is + l_actual ut_varchar2_list := coalesce(get_missing_filter_columns(a_actual,a_current_list),ut_varchar2_list()); + l_expected ut_varchar2_list := coalesce(get_missing_filter_columns(a_expected,a_current_list),ut_varchar2_list()); + l_missing_pk tt_missing_pk; + begin + select name,type + bulk collect into l_missing_pk + from + (select act.column_value name, 'e' type from table(l_expected) act + union all + select exp.column_value name, 'a' type from table(l_actual) exp) + order by type desc,name; + return l_missing_pk; end; + + function inc_exc_columns_from_cursor (a_cursor_info ut_cursor_column_tab, a_exclude_xpath ut_varchar2_list, a_include_xpath ut_varchar2_list) + return ut_cursor_column_tab is + l_filtered_set ut_varchar2_list := ut_varchar2_list(); + l_result ut_cursor_column_tab := ut_cursor_column_tab(); + l_include boolean; + begin + -- if include and exclude is not null its columns from include minus exclude + -- If include is not null and exclude is null cursor will have only include + -- If exclude is not null and include is null cursor will have all except exclude + if a_include_xpath.count > 0 and a_exclude_xpath.count > 0 then + select col_names bulk collect into l_filtered_set + from( + select regexp_replace(column_value,gc_xpath_extract_reg,'\5') col_names + from table(a_include_xpath) + minus + select regexp_replace(column_value,gc_xpath_extract_reg,'\5') col_names + from table(a_exclude_xpath) + ); + l_include := true; + elsif a_include_xpath.count > 0 and a_exclude_xpath.count = 0 then + select regexp_replace(column_value,gc_xpath_extract_reg,'\5') col_names + bulk collect into l_filtered_set + from table(a_include_xpath); + l_include := true; + elsif a_include_xpath.count = 0 and a_exclude_xpath.count > 0 then + select regexp_replace(column_value,gc_xpath_extract_reg,'\5') col_names + bulk collect into l_filtered_set + from table(a_exclude_xpath); + l_include := false; + elsif a_cursor_info is not null then + l_result:= a_cursor_info; + else + l_result := ut_cursor_column_tab(); + end if; + + if l_filtered_set.count > 0 then + l_result := filter_out_cols(a_cursor_info,l_filtered_set,l_include); + end if; - function get_hash(a_data raw, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is + return l_result; + end; + + function contains_collection (a_cursor_info ut_cursor_column_tab) + return number is + l_collection_elements number; begin - return dbms_crypto.hash(a_data, a_hash_type); + select count(1) into l_collection_elements from + table(a_cursor_info) c where c.is_collection = 1; + return l_collection_elements; end; + + function remove_incomparable_cols( a_cursor_details ut_cursor_column_tab,a_incomparable_cols ut_varchar2_list) + return ut_cursor_column_tab is + l_result ut_cursor_column_tab; + begin + select ut_cursor_column(i.parent_name,i.access_path,i.has_nested_col,i.transformed_name,i.hierarchy_level,i.column_position , + i.xml_valid_name,i.column_name,i.column_type,i.column_type_name ,i.column_schema,i.column_len,i.is_sql_diffable ,i.is_collection) + bulk collect into l_result + from table(a_cursor_details) i + left outer join table(a_incomparable_cols) c + on (i.access_path = c.column_value) + where c.column_value is null; - function get_hash(a_data clob, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash is + return l_result; + end; + + function getxmlchildren(a_parent_name varchar2,a_cursor_table ut_cursor_column_tab) + return xmltype is + l_result xmltype; begin - return dbms_crypto.hash(a_data, a_hash_type); + select xmlagg(xmlelement(evalname t.column_name,t.column_type, + getxmlchildren(t.column_name,a_cursor_table))) + into l_result + from table(a_cursor_table) t + where (a_parent_name is not null and parent_name = a_parent_name and hierarchy_level > 1 and column_name is not null) + or (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; - function columns_hash( - a_data_value_cursor ut_data_value_refcursor, a_exclude_xpath varchar2, a_include_xpath varchar2, - a_hash_type binary_integer := dbms_crypto.hash_sh1 - ) return t_hash is - l_cols_hash t_hash; - begin - if not a_data_value_cursor.is_null then - execute immediate - q'[select dbms_crypto.hash(replace(x.item_data.getclobval(),'>CHAR<','>VARCHAR2<'),]'||a_hash_type||') ' || - ' from ( select '||get_columns_filter(a_exclude_xpath, a_include_xpath)|| - ' from (select :columns_info as item_data from dual ) ucd' || - ' ) x' - into l_cols_hash using a_exclude_xpath,a_include_xpath, a_data_value_cursor.columns_info; + 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.bfile_type), + g_anytype_name_map(dbms_types.typecode_namedcollection)) + then + l_assert := false; + else + l_assert := true; end if; - return l_cols_hash; + return l_assert; end; - function is_pk_exists(a_expected_cursor xmltype,a_actual_cursor xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2,a_join_by_xpath varchar2) - return tt_missing_pk is - l_pk_xpath_tabs ut_varchar2_list := ut_varchar2_list(); - l_column_filter varchar2(32767); - l_no_missing_keys tt_missing_pk := tt_missing_pk(); + 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_anytype_members_info( a_anytype anytype ) + return t_anytype_members_rec is + l_result t_anytype_members_rec; begin - if a_join_by_xpath is not null then - l_pk_xpath_tabs := ut_utils.string_to_table(a_join_by_xpath,'|'); - l_column_filter := get_columns_row_filter(a_exclude_xpath, a_include_xpath); - - execute immediate q'[ - with xpaths_tab as (select column_value xpath from table(:xpath_tabs)), - expected_column_info as ( select :expected as item_data from dual ), - actual_column_info as ( select :actual as item_data from dual ) - select REGEXP_SUBSTR (xpath,'[^(/\*/)](.+)$'),diif_type from - ( - (select xpath,'e' diif_type from xpaths_tab - minus - select xpath,'e' diif_type - from ( select ]'||l_column_filter||q'[ from expected_column_info ucd) x - ,xpaths_tab - where xmlexists (xpaths_tab.xpath passing x.item_data) - ) - union all - (select xpath,'a' diif_type from xpaths_tab - minus - select xpath,'a' diif_type - from ( select ]'||l_column_filter||q'[ from actual_column_info ucd) x - ,xpaths_tab - where xmlexists (xpaths_tab.xpath passing x.item_data) - ) - )]' bulk collect into l_no_missing_keys - using l_pk_xpath_tabs,a_expected_cursor,a_actual_cursor, - a_exclude_xpath, a_include_xpath, - a_exclude_xpath, a_include_xpath; - + if a_anytype is not null then + l_result.type_code := a_anytype.getinfo( + prec => l_result.precision, + scale => l_result.scale, + len => l_result.length, + csid => l_result.char_set_id, + csfrm => l_result.char_set_frm, + schema_name => l_result.schema_name, + type_name => l_result.type_name, + version => l_result.version, + numelems => l_result.elements_count + ); end if; - - return l_no_missing_keys; + return l_result; end; - - procedure update_row_and_pk_hash(a_self_data_id in raw, a_other_data_id in raw, a_exclude_xpath varchar2, - a_include_xpath varchar2, a_join_by_xpath varchar2) is - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_column_filter varchar2(32767); - l_pk_hash_sql varchar2(32767); - - function get_column_pk_hash(a_join_by_xpath varchar2) return varchar2 is - l_column varchar2(32767); - begin - /* due to possibility of key being to columns we cannot use xmlextractvalue - usage of xmlagg is possible however it greatly complicates code and performance is impacted. - xpath to be looked at or regex - */ - if a_join_by_xpath is not null then - l_column := l_ut_owner ||'.ut_compound_data_helper.get_hash(extract(ucd.item_data,:join_by_xpath).GetClobVal()) pk_hash'; - else - l_column := ':join_by_xpath pk_hash'; - end if; - return l_column; - end; - - begin - l_column_filter := ut_compound_data_helper.get_columns_filter(a_exclude_xpath, a_include_xpath); - l_pk_hash_sql := get_column_pk_hash(a_join_by_xpath); - - execute immediate 'merge into ' || l_ut_owner || '.ut_compound_data_tmp tgt - using ( - select ucd_out.item_hash, - ucd_out.pk_hash, - ucd_out.item_no, - ucd_out.data_id, - row_number() over (partition by ucd_out.pk_hash,ucd_out.item_hash,ucd_out.data_id order by 1,2) duplicate_no - from - ( - select '||l_ut_owner ||'.ut_compound_data_helper.get_hash(ucd.item_data.getclobval()) item_hash, - pk_hash, ucd.item_no, ucd.data_id - from - ( - select '||l_column_filter||','||l_pk_hash_sql||', item_no, data_id - from ' || l_ut_owner || q'[.ut_compound_data_tmp ucd - where data_id = :self_guid or data_id = :other_guid - ) ucd - )ucd_out - ) src - on (tgt.item_no = src.item_no and tgt.data_id = src.data_id) - when matched then update - set tgt.item_hash = src.item_hash, - tgt.pk_hash = src.pk_hash, - tgt.duplicate_no = src.duplicate_no]' - using a_exclude_xpath, a_include_xpath,a_join_by_xpath,a_self_data_id, a_other_data_id; + function get_attr_elem_info( a_anytype anytype, a_pos pls_integer := null ) + return t_anytype_elem_info_rec is + l_result t_anytype_elem_info_rec; + begin + if a_anytype is not null then + l_result.type_code := a_anytype.getattreleminfo( + pos => a_pos, + prec => l_result.precision, + scale => l_result.scale, + len => l_result.length, + csid => l_result.char_set_id, + csfrm => l_result.char_set_frm, + attr_elt_type => l_result.attr_elt_type, + aname => l_result.attribute_name + ); + end if; + return l_result; end; - + +begin + g_anytype_name_map(dbms_types.typecode_date) := 'DATE'; + g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; + g_anytype_name_map(dbms_types.typecode_raw) := 'RAW'; + g_anytype_name_map(dbms_types.typecode_char) := 'CHAR'; + g_anytype_name_map(dbms_types.typecode_varchar2) := 'VARCHAR2'; + g_anytype_name_map(dbms_types.typecode_varchar) := 'VARCHAR'; + g_anytype_name_map(dbms_types.typecode_blob) := 'BLOB'; + g_anytype_name_map(dbms_types.typecode_bfile) := 'BFILE'; + g_anytype_name_map(dbms_types.typecode_clob) := 'CLOB'; + g_anytype_name_map(dbms_types.typecode_timestamp) := 'TIMESTAMP'; + g_anytype_name_map(dbms_types.typecode_timestamp_tz) := 'TIMESTAMP WITH TIME ZONE'; + g_anytype_name_map(dbms_types.typecode_timestamp_ltz) := 'TIMESTAMP WITH LOCAL TIME ZONE'; + g_anytype_name_map(dbms_types.typecode_interval_ym) := 'INTERVAL YEAR TO MONTH'; + g_anytype_name_map(dbms_types.typecode_interval_ds) := 'INTERVAL DAY TO SECOND'; + g_anytype_name_map(dbms_types.typecode_bfloat) := 'BINARY_FLOAT'; + g_anytype_name_map(dbms_types.typecode_bdouble) := 'BINARY_DOUBLE'; + g_anytype_name_map(dbms_types.typecode_urowid) := 'UROWID'; + g_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'; + end; / diff --git a/source/expectations/data_values/ut_compound_data_helper.pks b/source/expectations/data_values/ut_compound_data_helper.pks index a18c20952..79c90f4c5 100644 --- a/source/expectations/data_values/ut_compound_data_helper.pks +++ b/source/expectations/data_values/ut_compound_data_helper.pks @@ -16,7 +16,6 @@ create or replace package ut_compound_data_helper authid definer is limitations under the License. */ - gc_compare_join_by constant varchar2(10):='join_by'; gc_compare_unordered constant varchar2(10):='unordered'; gc_compare_normal constant varchar2(10):='normal'; @@ -48,41 +47,112 @@ create or replace package ut_compound_data_helper authid definer is type tt_row_diffs is table of t_row_diffs; - function get_column_info_xml(a_column_details ut_key_anyval_pair) return xmltype; + type t_diff_rec is record ( + act_item_data xmltype, + act_data_id raw(32), + exp_item_data xmltype, + exp_data_id raw(32), + item_no number, + dup_no number + ); + + type t_anytype_members_rec is record ( + type_code pls_integer, + schema_name varchar2(128), + type_name varchar2(128), + length pls_integer, + elements_count pls_integer, + version varchar2(32767), + precision pls_integer, + scale pls_integer, + char_set_id pls_integer, + char_set_frm pls_integer + ); + + type t_anytype_elem_info_rec is record ( + type_code pls_integer, + attribute_name varchar2(260), + length pls_integer, + version varchar2(32767), + precision pls_integer, + scale pls_integer, + char_set_id pls_integer, + char_set_frm pls_integer, + attr_elt_type anytype + ); + type t_diff_tab is table of t_diff_rec; + function get_columns_filter( a_exclude_xpath varchar2, a_include_xpath varchar2, a_table_alias varchar2 := 'ucd', a_column_alias varchar2 := 'item_data' ) return varchar2; function get_columns_diff( - a_expected xmltype, a_actual xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2 + a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab,a_order_enforced boolean := false ) return tt_column_diffs; - function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob; + function get_pk_value (a_join_by_xpath varchar2,a_item_data xmltype) return clob; - function compare_type(a_join_by_xpath in varchar2,a_unordered boolean) return varchar2; + function get_rows_diff( + a_expected_dataset_guid raw, a_actual_dataset_guid raw, a_diff_id raw, + a_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2 + ) return tt_row_diffs; - function get_rows_diff( + 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_max_rows integer, a_exclude_xpath varchar2, a_include_xpath varchar2, - a_join_by_xpath varchar2,a_unorderdered boolean + a_join_by_list ut_varchar2_list, a_unordered boolean, a_enforce_column_order boolean := false ) return tt_row_diffs; subtype t_hash is raw(128); function get_hash(a_data raw, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash; function get_hash(a_data clob, a_hash_type binary_integer := dbms_crypto.hash_sh1) return t_hash; - function columns_hash( - a_data_value_cursor ut_data_value_refcursor, a_exclude_xpath varchar2, a_include_xpath varchar2, - a_hash_type binary_integer := dbms_crypto.hash_sh1 - ) return t_hash; - function is_pk_exists(a_expected_cursor xmltype, a_actual_cursor xmltype, a_exclude_xpath varchar2, a_include_xpath varchar2,a_join_by_xpath varchar2) - return tt_missing_pk; + function get_fixed_size_hash(a_string varchar2, a_base integer :=0,a_size integer :=9999999) return number; + + function gen_compare_sql( + a_inclusion_type boolean, a_is_negated boolean, a_unordered boolean, + a_other ut_data_value_refcursor :=null, a_join_by_list ut_varchar2_list:=ut_varchar2_list() + ) return clob; + + procedure insert_diffs_result(a_diff_tab t_diff_tab, a_diff_id raw); + + procedure set_rows_diff(a_rows_diff integer); + + procedure cleanup_diff; + + function get_rows_diff_count return integer; + + function filter_out_cols( + a_cursor_info ut_cursor_column_tab, a_current_list ut_varchar2_list,a_include boolean := true + ) return ut_cursor_column_tab; + + function get_missing_pk( + a_expected ut_cursor_column_tab, a_actual ut_cursor_column_tab, a_current_list ut_varchar2_list + ) return tt_missing_pk; + + function inc_exc_columns_from_cursor ( + a_cursor_info ut_cursor_column_tab, a_exclude_xpath ut_varchar2_list, a_include_xpath ut_varchar2_list + ) return ut_cursor_column_tab; + + function contains_collection (a_cursor_info ut_cursor_column_tab) return number; + + function remove_incomparable_cols( + a_cursor_details ut_cursor_column_tab,a_incomparable_cols ut_varchar2_list + ) return ut_cursor_column_tab; + + function getxmlchildren(a_parent_name varchar2,a_cursor_table ut_cursor_column_tab) return xmltype; + + function is_sql_compare_allowed(a_type_name varchar2) return boolean; + + function get_column_type_desc(a_type_code in integer, a_dbms_sql_desc in boolean) return varchar2; + + + function get_anytype_members_info( a_anytype anytype ) return t_anytype_members_rec; - procedure update_row_and_pk_hash(a_self_data_id in raw, a_other_data_id in raw, a_exclude_xpath varchar2, - a_include_xpath varchar2, a_join_by_xpath varchar2); + function get_attr_elem_info( a_anytype anytype, a_pos pls_integer := null ) return t_anytype_elem_info_rec; end; / diff --git a/source/expectations/data_values/ut_compound_data_tmp.sql b/source/expectations/data_values/ut_compound_data_tmp.sql index a4c6516af..0fb5f9544 100644 --- a/source/expectations/data_values/ut_compound_data_tmp.sql +++ b/source/expectations/data_values/ut_compound_data_tmp.sql @@ -18,5 +18,6 @@ create global temporary table ut_compound_data_tmp( item_hash raw(128), pk_hash raw(128), duplicate_no integer, - constraint ut_cmp_data_tmp_hash_pk unique (data_id,item_no, item_hash , duplicate_no) + constraint ut_cmp_data_tmp_hash_pk unique (data_id, item_no, duplicate_no) ) on commit preserve rows; +--xmltype column item_data store as binary xml; \ No newline at end of file diff --git a/source/expectations/data_values/ut_compound_data_value.tpb b/source/expectations/data_values/ut_compound_data_value.tpb index e5a766890..6c67060fb 100644 --- a/source/expectations/data_values/ut_compound_data_value.tpb +++ b/source/expectations/data_values/ut_compound_data_value.tpb @@ -43,7 +43,6 @@ create or replace type body ut_compound_data_value as overriding member function to_string return varchar2 is l_results ut_utils.t_clob_tab; - c_max_rows constant integer := 20; l_result clob; l_result_string varchar2(32767); begin @@ -53,10 +52,13 @@ create or replace type body ut_compound_data_value as --return first c_max_rows rows execute immediate ' select xmlserialize( content ucd.item_data no indent) - from '|| ut_utils.ut_owner ||'.ut_compound_data_tmp ucd - where ucd.data_id = :data_id - and ucd.item_no <= :max_rows' - bulk collect into l_results using self.data_id, c_max_rows; + from '|| ut_utils.ut_owner ||q'[.ut_compound_data_tmp tmp + ,xmltable ( '/ROWSET' passing tmp.item_data + columns item_data xmltype PATH '*' + ) ucd + where tmp.data_id = :data_id + and rownum <= :max_rows]' + bulk collect into l_results using self.data_id, ut_utils.gc_diff_max_rows; ut_utils.append_to_clob(l_result,l_results); @@ -66,7 +68,10 @@ create or replace type body ut_compound_data_value as return l_result_string; end; - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2 is + overriding member function diff( + a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, + a_join_by_xpath varchar2, a_unordered boolean := false + ) return varchar2 is l_result clob; l_result_string varchar2(32767); begin @@ -76,62 +81,54 @@ create or replace type body ut_compound_data_value as return l_result_string; end; - member function get_data_diff(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, - a_join_by_xpath varchar2, a_unordered boolean) return clob is - c_max_rows constant integer := 20; + -- TODO : Rework to exclude xpath + member function get_data_diff( + a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, + a_join_by_xpath varchar2, a_unordered boolean + ) return clob is + c_max_rows integer := ut_utils.gc_diff_max_rows; l_result clob; l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); l_message varchar2(32767); - l_ut_owner varchar2(250) := ut_utils.ut_owner; l_diff_row_count integer; l_actual ut_compound_data_value; l_diff_id ut_compound_data_helper.t_hash; l_row_diffs ut_compound_data_helper.tt_row_diffs; - l_compare_type varchar2(10); - - function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs,a_compare_type varchar2) return varchar2 is + + function get_diff_message (a_row_diff ut_compound_data_helper.t_row_diffs,a_is_unordered boolean) return varchar2 is begin - if a_compare_type = ut_compound_data_helper.gc_compare_join_by and a_row_diff.pk_value is not null then - return ' PK '||a_row_diff.pk_value||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - elsif a_compare_type = ut_compound_data_helper.gc_compare_join_by or a_compare_type = ut_compound_data_helper.gc_compare_normal then - return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - elsif a_compare_type = ut_compound_data_helper.gc_compare_unordered then - return rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; - end if; + return ' Row No. '||a_row_diff.rn||' - '||rpad(a_row_diff.diff_type,10)||a_row_diff.diffed_row; end; begin if not a_other is of (ut_compound_data_value) then raise value_error; - end if; + end if; + l_actual := treat(a_other as ut_compound_data_value); dbms_lob.createtemporary(l_result,true); - - --diff rows and row elements - l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_actual.data_id); + l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_actual.data_id); -- First tell how many rows are different - execute immediate 'select count('||case when a_join_by_xpath is not null then 'distinct pk_hash' else '*' end||') from ' - || l_ut_owner || '.ut_compound_data_diff_tmp - where diff_id = :diff_id' into l_diff_row_count using l_diff_id; - + l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; if l_diff_row_count > 0 then - l_compare_type := ut_compound_data_helper.compare_type(a_join_by_xpath,a_unordered); l_row_diffs := ut_compound_data_helper.get_rows_diff( - self.data_id, l_actual.data_id, l_diff_id, c_max_rows, a_exclude_xpath, a_include_xpath, a_join_by_xpath, a_unordered); + self.data_id, l_actual.data_id, l_diff_id, c_max_rows, a_exclude_xpath, + a_include_xpath); 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.' 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 ); for i in 1 .. l_row_diffs.count loop l_results.extend; - l_results(l_results.last) := get_diff_message(l_row_diffs(i),l_compare_type); + l_results(l_results.last) := get_diff_message(l_row_diffs(i),a_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 are not matching.'; + ut_utils.append_to_clob( l_result, l_message ); end if; return l_result; end; @@ -163,7 +160,7 @@ create or replace type body ut_compound_data_value as if not a_other is of (ut_compound_data_value) then raise value_error; end if; - + l_other := treat(a_other as ut_compound_data_value); l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_other.data_id); @@ -189,75 +186,61 @@ create or replace type body ut_compound_data_value as if sql%rowcount = 0 and self.elements_count = l_other.elements_count then l_result := 0; else + ut_compound_data_helper.set_rows_diff(sql%rowcount); l_result := 1; end if; return l_result; end; - member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean ) return integer is - l_other ut_compound_data_value; - l_ut_owner varchar2(250) := ut_utils.ut_owner; - l_diff_id ut_compound_data_helper.t_hash; - l_result integer; - l_row_diffs ut_compound_data_helper.tt_row_diffs; - c_max_rows constant integer := 20; - - begin - if not a_other is of (ut_compound_data_value) then - raise value_error; - end if; - - l_other := treat(a_other as ut_compound_data_value); + member function compare_implementation( + a_other ut_data_value, a_unordered boolean, a_inclusion_compare boolean, + a_is_negated boolean, a_join_by_list ut_varchar2_list := ut_varchar2_list() + ) return integer is - l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_other.data_id); + l_diff_id ut_compound_data_helper.t_hash; + l_other ut_compound_data_value; + l_result integer; + --We will start with number od differences being displayed. + l_max_rows integer := ut_utils.gc_diff_max_rows; - /** - * Due to incompatibility issues in XML between 11 and 12.2 and 12.1 versions we will prepopulate pk_hash upfront to - * avoid optimizer incorrectly rewrite and causing NULL error or ORA-600 - **/ - ut_compound_data_helper.update_row_and_pk_hash(self.data_id, l_other.data_id, a_exclude_xpath,a_include_xpath,a_join_by_xpath); + l_loop_curs sys_refcursor; + l_diff_tab ut_compound_data_helper.t_diff_tab; + l_sql_rowcount integer :=0; - /* Peform minus on two sets two get diffrences that will be used later on to print results */ - execute immediate 'insert into ' || l_ut_owner || '.ut_compound_data_diff_tmp ( diff_id,item_hash,pk_hash,duplicate_no) - with source_data as - ( select t.data_id,t.item_hash,t.duplicate_no, - pk_hash - from ' || l_ut_owner || '.ut_compound_data_tmp t - where data_id = :self_guid or data_id = :other_guid - ) - select distinct :diff_id,tmp.item_hash,tmp.pk_hash,tmp.duplicate_no - from( - ( - select t.item_hash,t.duplicate_no,t.pk_hash - from source_data t - where t.data_id = :self_guid - minus - select t.item_hash,t.duplicate_no,t.pk_hash - from source_data t - where t.data_id = :other_guid - ) - union all - ( - select t.item_hash,t.duplicate_no,t.pk_hash - from source_data t - where t.data_id = :other_guid - minus - select t.item_hash,t.duplicate_no,t.pk_hash - from source_data t - where t.data_id = :self_guid - ))tmp' - using self.data_id, l_other.data_id, - l_diff_id, - self.data_id, l_other.data_id, - l_other.data_id,self.data_id; - --result is OK only if both are same - if sql%rowcount = 0 and self.elements_count = l_other.elements_count then + begin + l_other := treat(a_other as ut_compound_data_value); + l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_other.data_id); +-- dbms_output.put_line(ut_compound_data_helper.gen_compare_sql( +-- a_inclusion_compare, a_is_negated, a_unordered, +-- treat(a_other as ut_data_value_refcursor), a_join_by_list +-- )); + open l_loop_curs for + ut_compound_data_helper.gen_compare_sql( + a_inclusion_compare, a_is_negated, a_unordered, + treat(a_other as ut_data_value_refcursor), a_join_by_list + ) using self.data_id,l_other.data_id; + loop + fetch l_loop_curs bulk collect into l_diff_tab limit l_max_rows; + exit when l_diff_tab.count = 0; + if (ut_utils.gc_diff_max_rows > l_sql_rowcount ) then + ut_compound_data_helper.insert_diffs_result(l_diff_tab,l_diff_id); + end if; + l_sql_rowcount := l_sql_rowcount + l_diff_tab.count; + if (ut_utils.gc_diff_max_rows <= l_sql_rowcount and l_max_rows != ut_utils.gc_bc_fetch_limit ) then + l_max_rows := ut_utils.gc_bc_fetch_limit; + end if; + end loop; + + ut_compound_data_helper.set_rows_diff(l_sql_rowcount); + --result is OK only if both are same + if l_sql_rowcount = 0 and ( self.elements_count = l_other.elements_count or a_inclusion_compare ) then l_result := 0; else l_result := 1; end if; + return l_result; - end; - + end; + end; / diff --git a/source/expectations/data_values/ut_compound_data_value.tps b/source/expectations/data_values/ut_compound_data_value.tps index bf942c92e..771a2f3d2 100644 --- a/source/expectations/data_values/ut_compound_data_value.tps +++ b/source/expectations/data_values/ut_compound_data_value.tps @@ -34,7 +34,7 @@ create or replace type ut_compound_data_value force under ut_data_value( * Holds unique id for retrieving the data from ut_compound_data_tmp temp table */ data_id raw(16), - + overriding member function get_object_info return varchar2, overriding member function is_null return boolean, overriding member function is_diffable return boolean, @@ -44,6 +44,7 @@ create or replace type ut_compound_data_value force under ut_data_value( overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2, member function get_data_diff(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean) return clob, member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2) return integer, - member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean ) return integer + member function compare_implementation(a_other ut_data_value, a_unordered boolean, a_inclusion_compare boolean, + a_is_negated boolean, a_join_by_list ut_varchar2_list:=ut_varchar2_list() ) return integer ) not final not instantiable / diff --git a/source/expectations/data_values/ut_curr_usr_compound_helper.pkb b/source/expectations/data_values/ut_curr_usr_compound_helper.pkb deleted file mode 100644 index b610ee13c..000000000 --- a/source/expectations/data_values/ut_curr_usr_compound_helper.pkb +++ /dev/null @@ -1,244 +0,0 @@ -create or replace package body ut_curr_usr_compound_helper is - - type t_type_name_map is table of varchar2(100) index by binary_integer; - g_type_name_map t_type_name_map; - g_anytype_name_map t_type_name_map; - g_anytype_collection_name t_type_name_map; - g_user_defined_type pls_integer := dbms_sql.user_defined_type; - g_is_collection boolean := false; - - procedure set_collection_state(a_is_collection boolean) is - begin - --Make sure that we set a g_is_collection only once so we dont reset from true to false. - if not g_is_collection then - g_is_collection := a_is_collection; - end if; - end; - - function get_column_type(a_desc_rec dbms_sql.desc_rec3, a_desc_user_types boolean := false) return ut_key_anyval_pair is - l_data ut_data_value; - l_result ut_key_anyval_pair; - l_data_type varchar2(500) := 'unknown datatype'; - - function is_collection (a_owner varchar2,a_type_name varchar2) return boolean is - l_type_view varchar2(200) := ut_metadata.get_dba_view('dba_types'); - l_typecode varchar2(100); - begin - execute immediate 'select typecode from '||l_type_view ||' - where owner = :owner and type_name = :typename' - into l_typecode using a_owner,a_type_name; - - return l_typecode = 'COLLECTION'; - end; - - begin - if g_type_name_map.exists(a_desc_rec.col_type) then - l_data := ut_data_value_varchar2(g_type_name_map(a_desc_rec.col_type)); - /*If its a collection regardless is we want to describe user defined types we will return just a name - and capture that name */ - elsif a_desc_rec.col_type = g_user_defined_type and is_collection(a_desc_rec.col_schema_name,a_desc_rec.col_type_name) then - l_data := ut_data_value_varchar2(a_desc_rec.col_schema_name||'.'||a_desc_rec.col_type_name); - set_collection_state(true); - elsif a_desc_rec.col_type = g_user_defined_type and a_desc_user_types then - l_data :=ut_data_value_xmltype(get_user_defined_type(a_desc_rec.col_schema_name,a_desc_rec.col_type_name)); - elsif a_desc_rec.col_schema_name is not null and a_desc_rec.col_type_name is not null then - l_data := ut_data_value_varchar2(a_desc_rec.col_schema_name||'.'||a_desc_rec.col_type_name); - end if; - return ut_key_anyval_pair(a_desc_rec.col_name,l_data); - end; - - function get_columns_info( - a_columns_tab dbms_sql.desc_tab3, a_columns_count integer, a_desc_user_types boolean := false - ) return ut_key_anyval_pairs is - l_result ut_key_anyval_pairs := ut_key_anyval_pairs(); - begin - for i in 1 .. a_columns_count loop - l_result.extend; - l_result(l_result.last) := get_column_type(a_columns_tab(i),a_desc_user_types); - end loop; - return l_result; - end; - - procedure get_descr_cursor( - a_cursor in out nocopy sys_refcursor, - a_columns_tab in out nocopy ut_key_anyval_pairs, - a_join_by_tab in out nocopy ut_key_anyval_pairs - ) is - l_cursor_number integer; - l_columns_count pls_integer; - l_columns_desc dbms_sql.desc_tab3; - begin - if a_cursor is null or not a_cursor%isopen then - a_columns_tab := null; - a_join_by_tab := null; - end if; - l_cursor_number := dbms_sql.to_cursor_number( a_cursor ); - dbms_sql.describe_columns3( l_cursor_number, l_columns_count, l_columns_desc ); - a_cursor := dbms_sql.to_refcursor( l_cursor_number ); - a_columns_tab := get_columns_info( l_columns_desc, l_columns_count, false); - a_join_by_tab := get_columns_info( l_columns_desc, l_columns_count, true); - end; - - procedure get_columns_info( - a_cursor in out nocopy sys_refcursor, - a_columns_info out nocopy xmltype, - a_join_by_info out nocopy xmltype, - a_contains_collection out nocopy number - ) is - l_columns_info xmltype; - l_join_by_info xmltype; - l_result_tmp xmltype; - l_columns_tab ut_key_anyval_pairs; - l_join_by_tab ut_key_anyval_pairs; - begin - - get_descr_cursor(a_cursor,l_columns_tab,l_join_by_tab); - - for i in 1..l_columns_tab.COUNT - loop - l_result_tmp := ut_compound_data_helper.get_column_info_xml(l_columns_tab(i)); - select xmlconcat(l_columns_info,l_result_tmp) into l_columns_info from dual; - end loop; - - for i in 1..l_join_by_tab.COUNT - loop - l_result_tmp := ut_compound_data_helper.get_column_info_xml(l_join_by_tab(i)); - select xmlconcat(l_join_by_info,l_result_tmp) into l_join_by_info from dual; - end loop; - - select XMLELEMENT("ROW",l_columns_info ),XMLELEMENT("ROW",l_join_by_info ) - into a_columns_info,a_join_by_info from dual; - - a_contains_collection := ut_utils.boolean_to_int(g_is_collection); - end; - - function get_anytype_attribute_count (a_anytype anytype) return pls_integer is - l_attribute_typecode pls_integer; - l_schema_name varchar2(32767); - l_version varchar2(32767); - l_type_name varchar2(32767); - l_attributes pls_integer; - l_prec pls_integer; - l_scale pls_integer; - l_len pls_integer; - l_csid pls_integer; - l_csfrm pls_integer; - begin - l_attribute_typecode := a_anytype.getinfo( - prec => l_prec, - scale => l_scale, - len => l_len, - csid => l_csid, - csfrm => l_csfrm, - schema_name => l_schema_name, - type_name => l_type_name, - version => l_version, - numelems => l_attributes); - return l_attributes; - end; - - function get_anytype_attributes_info (a_anytype anytype) return ut_key_value_pairs is - l_result ut_key_value_pairs := ut_key_value_pairs(); - l_attribute_typecode pls_integer; - l_aname varchar2(32767); - l_prec pls_integer; - l_scale pls_integer; - l_len pls_integer; - l_csid pls_integer; - l_csfrm pls_integer; - l_attr_elt_type anytype; - begin - for i in 1..get_anytype_attribute_count(a_anytype) loop - l_attribute_typecode := a_anytype.getAttrElemInfo( - pos => i, --First attribute - prec => l_prec, - scale => l_scale, - len => l_len, - csid => l_csid, - csfrm => l_csfrm, - attr_elt_type => l_attr_elt_type, - aname => l_aname); - - l_result.extend; - l_result(l_result.last) := ut_key_value_pair(l_aname, g_anytype_name_map(l_attribute_typecode)); - --check for collection - if g_anytype_collection_name.exists(l_attribute_typecode) then - set_collection_state(true); - end if; - end loop; - return l_result; - end; - - function get_user_defined_type(a_owner varchar2,a_type_name varchar2) return xmltype is - l_anydata anydata; - l_anytype anytype; - l_typecode pls_integer; - l_result xmltype; - l_columns_tab ut_key_value_pairs := ut_key_value_pairs(); - - begin - execute immediate 'declare - l_v '||a_owner||'.'||a_type_name||'; - begin - :anydata := anydata.convertobject(l_v); - end;' USING IN OUT l_anydata; - - l_typecode := l_anydata.gettype(l_anytype); - l_columns_tab := get_anytype_attributes_info(l_anytype); - - select xmlagg(xmlelement(evalname key,value)) - into l_result from table(l_columns_tab); - - return l_result; - - end; - - begin - g_anytype_name_map(dbms_types.typecode_date) :=' DATE'; - g_anytype_name_map(dbms_types.typecode_number) := 'NUMBER'; - g_anytype_name_map(dbms_types.typecode_raw) := 'RAW'; - g_anytype_name_map(dbms_types.typecode_char) := 'CHAR'; - g_anytype_name_map(dbms_types.typecode_varchar2) := 'VARCHAR2'; - g_anytype_name_map(dbms_types.typecode_varchar) := 'VARCHAR'; - g_anytype_name_map(dbms_types.typecode_blob) := 'BLOB'; - g_anytype_name_map(dbms_types.typecode_bfile) := 'BFILE'; - g_anytype_name_map(dbms_types.typecode_clob) := 'CLOB'; - g_anytype_name_map(dbms_types.typecode_timestamp) := 'TIMESTAMP'; - g_anytype_name_map(dbms_types.typecode_timestamp_tz) := 'TIMESTAMP WITH TIME ZONE'; - g_anytype_name_map(dbms_types.typecode_timestamp_ltz) := 'TIMESTAMP WITH LOCAL TIME ZONE'; - g_anytype_name_map(dbms_types.typecode_interval_ym) := 'INTERVAL YEAR TO MONTH'; - g_anytype_name_map(dbms_types.typecode_interval_ds) := 'INTERVAL DAY TO SECOND'; - g_anytype_name_map(dbms_types.typecode_bfloat) := 'BINARY_FLOAT'; - g_anytype_name_map(dbms_types.typecode_bdouble) := 'BINARY_DOUBLE'; - g_anytype_name_map(dbms_types.typecode_urowid) := 'UROWID'; - g_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_collection_name(dbms_types.typecode_varray) := 'VARRRAY'; - g_anytype_collection_name(dbms_types.typecode_table) := 'TABLE'; - g_anytype_collection_name(dbms_types.typecode_namedcollection) := 'NAMEDCOLLECTION'; - - - g_type_name_map( dbms_sql.binary_bouble_type ) := 'BINARY_DOUBLE'; - g_type_name_map( dbms_sql.bfile_type ) := 'BFILE'; - g_type_name_map( dbms_sql.binary_float_type ) := 'BINARY_FLOAT'; - g_type_name_map( dbms_sql.blob_type ) := 'BLOB'; - g_type_name_map( dbms_sql.long_raw_type ) := 'LONG RAW'; - g_type_name_map( dbms_sql.char_type ) := 'CHAR'; - g_type_name_map( dbms_sql.clob_type ) := 'CLOB'; - g_type_name_map( dbms_sql.long_type ) := 'LONG'; - g_type_name_map( dbms_sql.date_type ) := 'DATE'; - g_type_name_map( dbms_sql.interval_day_to_second_type ) := 'INTERVAL DAY TO SECOND'; - g_type_name_map( dbms_sql.interval_year_to_month_type ) := 'INTERVAL YEAR TO MONTH'; - g_type_name_map( dbms_sql.raw_type ) := 'RAW'; - g_type_name_map( dbms_sql.timestamp_type ) := 'TIMESTAMP'; - g_type_name_map( dbms_sql.timestamp_with_tz_type ) := 'TIMESTAMP WITH TIME ZONE'; - g_type_name_map( dbms_sql.timestamp_with_local_tz_type ) := 'TIMESTAMP WITH LOCAL TIME ZONE'; - g_type_name_map( dbms_sql.varchar2_type ) := 'VARCHAR2'; - g_type_name_map( dbms_sql.number_type ) := 'NUMBER'; - g_type_name_map( dbms_sql.rowid_type ) := 'ROWID'; - g_type_name_map( dbms_sql.urowid_type ) := 'UROWID'; - -end; -/ diff --git a/source/expectations/data_values/ut_curr_usr_compound_helper.pks b/source/expectations/data_values/ut_curr_usr_compound_helper.pks deleted file mode 100644 index d662f51f9..000000000 --- a/source/expectations/data_values/ut_curr_usr_compound_helper.pks +++ /dev/null @@ -1,13 +0,0 @@ -create or replace package ut_curr_usr_compound_helper authid current_user is - - procedure get_columns_info( - a_cursor in out nocopy sys_refcursor, - a_columns_info out nocopy xmltype, - a_join_by_info out nocopy xmltype, - a_contains_collection out nocopy number - ); - - function get_user_defined_type(a_owner varchar2, a_type_name varchar2) return xmltype; - -end; -/ diff --git a/source/expectations/data_values/ut_cursor_column.tpb b/source/expectations/data_values/ut_cursor_column.tpb new file mode 100644 index 000000000..a956164f5 --- /dev/null +++ b/source/expectations/data_values/ut_cursor_column.tpb @@ -0,0 +1,53 @@ +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 + ) 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_name := TRIM( BOTH '''' FROM a_col_name); --name of the column + self.column_type_name := a_col_type_name; --type name e.g. test_dummy_object or varchar2 + self.access_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.xml_valid_name := '"'||self.column_name||'"'; --User friendly column name + self.transformed_name := case when self.parent_name is null then + self.xml_valid_name + else + '"'||ut_compound_data_helper.get_fixed_size_hash(self.parent_name||self.column_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 + 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 + ) 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); + 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 new file mode 100644 index 000000000..4b436051a --- /dev/null +++ b/source/expectations/data_values/ut_cursor_column.tps @@ -0,0 +1,46 @@ +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), + has_nested_col number(1,0), + transformed_name varchar2(32), + hierarchy_level number, + column_position number, + xml_valid_name varchar2(128), + column_name varchar2(128), + column_type varchar2(128), + column_type_name varchar2(128), + column_schema varchar2(128), + column_len 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), + + 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) + 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_column_tab.tps b/source/expectations/data_values/ut_cursor_column_tab.tps new file mode 100644 index 000000000..1b7a1698f --- /dev/null +++ b/source/expectations/data_values/ut_cursor_column_tab.tps @@ -0,0 +1,19 @@ +create or replace type ut_cursor_column_tab 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. + */ +table of ut_cursor_column +/ \ No newline at end of file diff --git a/source/expectations/data_values/ut_cursor_details.tpb b/source/expectations/data_values/ut_cursor_details.tpb new file mode 100644 index 000000000..36bc9d6a1 --- /dev/null +++ b/source/expectations/data_values/ut_cursor_details.tpb @@ -0,0 +1,202 @@ +create or replace type body ut_cursor_details as + + order member function compare(a_other ut_cursor_details) return integer is + l_diffs integer; + begin + if self.is_column_order_enforced = 1 then + 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 + where a.column_name is null or e.column_name is null; + else + 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') + where a.column_name is null or e.column_name is null; + end if; + return l_diffs; + end; + + member function get_user_defined_type(a_owner varchar2, a_type_name varchar2) return anytype is + l_anytype anytype; + not_found exception; + pragma exception_init(not_found,-22303); + begin + begin + $if dbms_db_version.version <= 12 $then + l_anytype := anytype.getpersistent( a_owner, a_type_name ); + $else + l_anytype := getanytypefrompersistent( a_owner, a_type_name ); + $end + exception + when not_found then + null; + end; + return l_anytype; + 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_compound_data_helper.t_anytype_members_rec; + l_element_info ut_compound_data_helper.t_anytype_elem_info_rec; + l_is_collection boolean; + begin + + l_elements_info := ut_compound_data_helper.get_anytype_members_info( a_compound_data ); + + l_is_collection := is_collection(l_elements_info.type_code); + + if l_elements_info.elements_count is null then + + l_element_info := ut_compound_data_helper.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 + ); + 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_compound_data_helper.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 + ); + 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 := 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 + ); + if l_columns_desc(pos).col_type = dbms_sql.user_defined_type or l_is_collection then + desc_compound_data( + 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 get_anytype_from_name( + a_owner in varchar2, a_type_name in varchar2 + ) return anytype is + l_result anytype; + begin + begin + $if dbms_db_version.version <= 12 $then + l_result := anytype.getpersistent( a_owner, a_type_name ); + $else + l_result := getanytypefrompersistent( a_owner, a_type_name ); + $end + exception + when others then + null; + end; + return l_result; + end; + + member function is_collection (a_anytype_code in integer) return boolean is + begin + return coalesce(a_anytype_code in (dbms_types.typecode_varray,dbms_types.typecode_table,dbms_types.typecode_namedcollection),false); + end; + + member function is_collection (a_owner varchar2, a_type_name varchar2) return boolean is + begin + return is_collection( + ut_compound_data_helper.get_anytype_members_info( + get_anytype_from_name(a_owner, a_type_name) + ).type_code + ); + end; + + member procedure ordered_columns(self in out nocopy ut_cursor_details,a_ordered_columns boolean := false) is + begin + self.is_column_order_enforced := ut_utils.boolean_to_int(a_ordered_columns); + end; + +end; +/ diff --git a/source/expectations/data_values/ut_cursor_details.tps b/source/expectations/data_values/ut_cursor_details.tps new file mode 100644 index 000000000..545106d35 --- /dev/null +++ b/source/expectations/data_values/ut_cursor_details.tps @@ -0,0 +1,38 @@ +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, + is_column_order_enforced 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, + order member function compare(a_other ut_cursor_details) return integer, + 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 get_user_defined_type(a_owner varchar2, a_type_name varchar2) return anytype, + member function get_anytype_from_name( + a_owner in varchar2, a_type_name in varchar2 + ) return anytype, + member function is_collection(a_anytype_code in integer) return boolean, + member function is_collection(a_owner varchar2, a_type_name varchar2) return boolean, + member procedure ordered_columns(self in out nocopy ut_cursor_details, a_ordered_columns boolean := false) +) +/ diff --git a/source/expectations/data_values/ut_data_value_anydata.tpb b/source/expectations/data_values/ut_data_value_anydata.tpb index 7d5322cca..0be830afb 100644 --- a/source/expectations/data_values/ut_data_value_anydata.tpb +++ b/source/expectations/data_values/ut_data_value_anydata.tpb @@ -36,6 +36,8 @@ create or replace type body ut_data_value_anydata as else self.is_data_null := 1; end if; + + ut_compound_data_helper.cleanup_diff; if not self.is_null() then ut_expectation_processor.set_xml_nls_params(); open l_query for select a_value val from dual; diff --git a/source/expectations/data_values/ut_data_value_refcursor.tpb b/source/expectations/data_values/ut_data_value_refcursor.tpb index 1c7619be0..2ac8b99ec 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tpb +++ b/source/expectations/data_values/ut_data_value_refcursor.tpb @@ -16,95 +16,106 @@ create or replace type body ut_data_value_refcursor as 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 + 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 init(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) is - c_bulk_rows constant integer := 1000; + 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_current_date_format varchar2(4000); - cursor_not_open exception; - l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_ctx number; + l_xml xmltype; + l_ut_owner varchar2(250) := ut_utils.ut_owner; + l_set_id integer := 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; + + self.elements_count := self.elements_count + dbms_xmlgen.getNumRowsProcessed(l_ctx); + execute immediate + 'insert into ' || l_ut_owner || '.ut_compound_data_tmp(data_id, item_no, item_data) ' || + 'values (:self_guid, :self_row_count, :l_xml)' + using in self.data_id, l_set_id, l_xml; + l_set_id := l_set_id + c_bulk_rows; + end loop; + ut_expectation_processor.reset_nls_params(); + dbms_xmlgen.closeContext(l_ctx); + exception + when others then + ut_expectation_processor.reset_nls_params(); + dbms_xmlgen.closeContext(l_ctx); + raise; + end; + + member procedure init(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) is + l_cursor sys_refcursor := a_value; + cursor_not_open exception; + l_cursor_number number; begin self.is_data_null := ut_utils.boolean_to_int(a_value is null); self.self_type := $$plsql_unit; self.data_id := sys_guid(); self.data_type := 'refcursor'; + ut_compound_data_helper.cleanup_diff; + if l_cursor is not null then if l_cursor%isopen then - --Get some more info regarding cursor, including if it containts collection columns and what is their name - - ut_curr_usr_compound_helper.get_columns_info(l_cursor,self.columns_info,self.key_info, - self.contain_collection); - - self.elements_count := 0; - -- 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); - - execute immediate - 'insert into ' || l_ut_owner || '.ut_compound_data_tmp(data_id, item_no, item_data) ' || - 'select :self_guid, :self_row_count + rownum, value(a) ' || - ' from table( xmlsequence( extract(:l_xml,''ROWSET/*'') ) ) a' - using in self.data_id, self.elements_count, l_xml; - - exit when sql%rowcount = 0; - - self.elements_count := self.elements_count + sql%rowcount; - end loop; - - ut_expectation_processor.reset_nls_params(); - if l_cursor%isopen then - close l_cursor; - end if; - dbms_xmlgen.closeContext(l_ctx); - + --Get some more info regarding cursor, including if it containts collection columns and what is their name + self.elements_count := 0; + extract_cursor(l_cursor); + l_cursor_number := dbms_sql.to_cursor_number(l_cursor); + self.cursor_details := ut_cursor_details(l_cursor_number); + dbms_sql.close_cursor(l_cursor_number); elsif not l_cursor%isopen then - raise cursor_not_open; + 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 - ut_expectation_processor.reset_nls_params(); if l_cursor%isopen then close l_cursor; end if; - dbms_xmlgen.closeContext(l_ctx); raise; end; - + overriding member function to_string return varchar2 is l_result clob; l_result_string varchar2(32767); + l_cursor_details ut_cursor_column_tab := self.cursor_details.cursor_columns_info; + + l_query varchar2(32767); + l_column_info xmltype; + begin if not self.is_null() then dbms_lob.createtemporary(l_result, true); ut_utils.append_to_clob(l_result, 'Data-types:'||chr(10)); - ut_utils.append_to_clob(l_result, self.columns_info.getclobval()); + + l_column_info := ut_compound_data_helper.getxmlchildren(null,l_cursor_details); + ut_utils.append_to_clob(l_result, l_column_info.getclobval()); 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); @@ -112,14 +123,28 @@ create or replace type body ut_data_value_refcursor as return l_result_string; end; - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2 is + member function diff( + a_other ut_data_value, a_unordered boolean := false, a_join_by_list ut_varchar2_list:=ut_varchar2_list() + ) return varchar2 is l_result clob; l_results ut_utils.t_clob_tab := ut_utils.t_clob_tab(); l_result_string varchar2(32767); l_actual ut_data_value_refcursor; l_column_diffs ut_compound_data_helper.tt_column_diffs := ut_compound_data_helper.tt_column_diffs(); - l_exclude_xpath varchar2(32767) := a_exclude_xpath; + + l_act_cols ut_cursor_column_tab; + l_exp_cols ut_cursor_column_tab; + l_missing_pk ut_compound_data_helper.tt_missing_pk := ut_compound_data_helper.tt_missing_pk(); + l_col_diffs ut_compound_data_helper.tt_column_diffs := ut_compound_data_helper.tt_column_diffs(); + + 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); + + l_column_order_enforce boolean := ut_utils.int_to_boolean(self.cursor_details.is_column_order_enforced); function get_col_diff_text(a_col ut_compound_data_helper.t_column_diffs) return varchar2 is begin @@ -139,7 +164,6 @@ create or replace type body ut_data_value_refcursor as function get_missing_key_message(a_missing_keys ut_compound_data_helper.t_missing_pk) return varchar2 is l_message varchar2(200); begin - if a_missing_keys.diff_type = 'a' then l_message := ' Join key '||a_missing_keys.missingxpath||' does not exists in actual'; elsif a_missing_keys.diff_type = 'e' then @@ -149,59 +173,90 @@ create or replace type body ut_data_value_refcursor as return l_message; end; - function add_incomparable_cols_to_xpath( - a_column_diffs ut_compound_data_helper.tt_column_diffs, a_exclude_xpath varchar2 - ) return varchar2 is + 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_incomparable_cols ut_varchar2_list := ut_varchar2_list(); - l_result varchar2(32767); + l_filter_out ut_cursor_column_tab; begin for i in 1 .. a_column_diffs.count loop if a_column_diffs(i).diff_type in ('-','+') then l_incomparable_cols.extend; - l_incomparable_cols(l_incomparable_cols.last) := ut_utils.xmlgen_escaped_string(coalesce(a_column_diffs(i).expected_name,a_column_diffs(i).actual_name)); - end if; + l_incomparable_cols(l_incomparable_cols.last) := coalesce(a_column_diffs(i).expected_name,a_column_diffs(i).actual_name); + end if; end loop; - l_result := ut_utils.to_xpath(l_incomparable_cols); - if a_exclude_xpath is not null and l_result is not null then - l_result := l_result ||'|'||a_exclude_xpath; - else - l_result := coalesce(a_exclude_xpath, l_result); - end if; - return l_result; + + return ut_compound_data_helper.remove_incomparable_cols(a_cursor_details,l_incomparable_cols); 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_actual := treat(a_other as ut_data_value_refcursor); - dbms_lob.createtemporary(l_result,true); + l_act_cols := l_actual.cursor_details.cursor_columns_info; + l_exp_cols := self.cursor_details.cursor_columns_info; + dbms_lob.createtemporary(l_result,true); --diff columns if not self.is_null and not l_actual.is_null then - l_column_diffs := ut_compound_data_helper.get_columns_diff(self.columns_info, l_actual.columns_info, a_exclude_xpath, a_include_xpath); - + l_column_diffs := ut_compound_data_helper.get_columns_diff(self.cursor_details.cursor_columns_info,l_actual.cursor_details.cursor_columns_info,l_column_order_enforce); + if l_column_diffs.count > 0 then ut_utils.append_to_clob(l_result,chr(10) || 'Columns:' || chr(10)); end if; - 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); - l_exclude_xpath := add_incomparable_cols_to_xpath(l_column_diffs, a_exclude_xpath); + l_act_cols := remove_incomparable_cols(l_actual.cursor_details.cursor_columns_info,l_column_diffs); + l_exp_cols := remove_incomparable_cols(self.cursor_details.cursor_columns_info,l_column_diffs); end if; --check for missing pk - if (a_join_by_xpath is not null) then - l_missing_pk := ut_compound_data_helper.is_pk_exists(self.key_info, l_actual.key_info, a_exclude_xpath, a_include_xpath,a_join_by_xpath); + if a_join_by_list.count > 0 then + l_missing_pk := ut_compound_data_helper.get_missing_pk(l_exp_cols,l_act_cols,a_join_by_list); end if; --diff rows and row elements if the pk is not missing if l_missing_pk.count = 0 then - ut_utils.append_to_clob(l_result, self.get_data_diff(a_other, a_exclude_xpath, a_include_xpath, a_join_by_xpath, a_unordered)); + l_diff_id := ut_compound_data_helper.get_hash(self.data_id||l_actual.data_id); + + -- First tell how many rows are different + l_diff_row_count := ut_compound_data_helper.get_rows_diff_count; + l_results := ut_utils.t_clob_tab(); + if l_diff_row_count > 0 then + l_row_diffs := ut_compound_data_helper.get_rows_diff_by_sql( + 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); + 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 ); + 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_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_missing_pk.count loop @@ -209,7 +264,8 @@ create or replace type body ut_data_value_refcursor as ut_utils.append_to_clob(l_result, get_missing_key_message(l_missing_pk(i))|| chr(10)); end loop; - if ut_utils.int_to_boolean(self.contain_collection) or ut_utils.int_to_boolean(l_actual.contain_collection) then + if ut_compound_data_helper.contains_collection(self.cursor_details.cursor_columns_info) > 0 + or ut_compound_data_helper.contains_collection(l_actual.cursor_details.cursor_columns_info) > 0 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; @@ -220,38 +276,30 @@ create or replace type body ut_data_value_refcursor as return l_result_string; end; - overriding member function compare_implementation (a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean) return integer is + overriding member function compare_implementation(a_other ut_data_value, a_unordered boolean, a_inclusion_compare boolean := false, a_is_negated boolean := false, + a_join_by_list ut_varchar2_list:=ut_varchar2_list()) return integer is l_result integer := 0; - l_other ut_data_value_refcursor; - function is_pk_missing (a_pk_missing_tab ut_compound_data_helper.tt_missing_pk) return integer is - begin - return case when a_pk_missing_tab.count > 0 then 1 else 0 end; - end; + l_actual ut_data_value_refcursor; + l_pk_missing_tab ut_compound_data_helper.tt_missing_pk; + begin if not a_other is of (ut_data_value_refcursor) then raise value_error; end if; - - l_other := treat(a_other as ut_data_value_refcursor); - - --if we join by key and key is missing fail and report error - if a_join_by_xpath is not null then - l_result := is_pk_missing(ut_compound_data_helper.is_pk_exists(self.key_info, l_other.key_info, a_exclude_xpath, a_include_xpath,a_join_by_xpath)); + + l_actual := treat(a_other as ut_data_value_refcursor); + + if a_join_by_list.count > 0 then + l_pk_missing_tab := ut_compound_data_helper.get_missing_pk(self.cursor_details.cursor_columns_info,l_actual.cursor_details.cursor_columns_info,a_join_by_list); + l_result := case when (l_pk_missing_tab.count > 0) then 1 else 0 end; end if; - - if l_result = 0 then - --if column names/types are not equal - build a diff of column names and types - if ut_compound_data_helper.columns_hash( self, a_exclude_xpath, a_include_xpath ) - != ut_compound_data_helper.columns_hash( l_other, a_exclude_xpath, a_include_xpath ) - then - l_result := 1; - end if; - - if a_unordered then - l_result := l_result + (self as ut_compound_data_value).compare_implementation(a_other, a_exclude_xpath, a_include_xpath, a_join_by_xpath, a_unordered); - else - l_result := l_result + (self as ut_compound_data_value).compare_implementation(a_other, a_exclude_xpath, a_include_xpath); + + if l_result = 0 then + if (self.cursor_details is not null and l_actual.cursor_details is not null) and (self.cursor_details != l_actual.cursor_details) then + l_result := 1; end if; + l_result := l_result + (self as ut_compound_data_value).compare_implementation(a_other,a_unordered, a_inclusion_compare, + a_is_negated, a_join_by_list); end if; return l_result; @@ -262,6 +310,15 @@ create or replace type body ut_data_value_refcursor as return self.elements_count = 0; end; + 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 + l_result ut_data_value_refcursor := self; + begin + if l_result.cursor_details.cursor_columns_info is not null then + l_result.cursor_details.cursor_columns_info := ut_compound_data_helper.inc_exc_columns_from_cursor(l_result.cursor_details.cursor_columns_info,a_exclude_xpath,a_include_xpath); + l_result.cursor_details.ordered_columns(a_ordered_columns); + end if; + return l_result; + end; end; / diff --git a/source/expectations/data_values/ut_data_value_refcursor.tps b/source/expectations/data_values/ut_data_value_refcursor.tps index 02ded0b39..d8fb2c891 100644 --- a/source/expectations/data_values/ut_data_value_refcursor.tps +++ b/source/expectations/data_values/ut_data_value_refcursor.tps @@ -24,28 +24,20 @@ create or replace type ut_data_value_refcursor under ut_compound_data_value( * Determines if the cursor is null */ is_cursor_null integer, - - /** - * hold information if the cursor contains collection object - */ - contain_collection number(1,0), - - /** - * Holds information about column names and column data-types - */ - columns_info xmltype, - - /** - * Holds more detailed information regarding the pk joins - */ - key_info xmltype, + + /* + *columns info + */ + cursor_details ut_cursor_details, constructor function ut_data_value_refcursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor) return self as result, + member procedure extract_cursor(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor), member procedure init(self in out nocopy ut_data_value_refcursor, a_value sys_refcursor), overriding member function to_string return varchar2, - overriding member function diff( a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean := false ) return varchar2, - overriding member function compare_implementation(a_other ut_data_value, a_exclude_xpath varchar2, a_include_xpath varchar2, a_join_by_xpath varchar2, a_unordered boolean) return integer, - overriding member function is_empty return boolean - + member function diff( a_other ut_data_value, a_unordered boolean := false, a_join_by_list ut_varchar2_list:=ut_varchar2_list() ) return varchar2, + overriding member function compare_implementation(a_other ut_data_value, a_unordered boolean, a_inclusion_compare boolean := false, a_is_negated boolean := false, + a_join_by_list ut_varchar2_list:=ut_varchar2_list()) return integer, + overriding member function is_empty return boolean, + 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 ) / diff --git a/source/expectations/matchers/ut_equal.tpb b/source/expectations/matchers/ut_equal.tpb index 611b1cd1c..000d3a10c 100644 --- a/source/expectations/matchers/ut_equal.tpb +++ b/source/expectations/matchers/ut_equal.tpb @@ -25,7 +25,7 @@ create or replace type body ut_equal as self.exclude_list := ut_varchar2_list(); self.join_columns := ut_varchar2_list(); end; - + member function equal_with_nulls(a_assert_result boolean, a_actual ut_data_value) return boolean is begin ut_utils.debug_log('ut_equal.equal_with_nulls :' || ut_utils.to_test_result(a_assert_result) || ':'); @@ -158,28 +158,37 @@ create or replace type body ut_equal as member function include(a_items varchar2) return ut_equal is l_result ut_equal := self; begin - ut_utils.append_to_list(l_result.include_list, a_items); + l_result.include_list := l_result.include_list multiset union coalesce(ut_utils.string_to_table(REPLACE(a_items,'|',','),','),ut_varchar2_list()); return l_result; end; member function include(a_items ut_varchar2_list) return ut_equal is l_result ut_equal := self; + l_items ut_varchar2_list := ut_varchar2_list(); begin - l_result.include_list := l_result.include_list multiset union all coalesce(a_items,ut_varchar2_list()); + for i in 1..a_items.count loop + l_items := l_items multiset union all coalesce(ut_utils.string_to_table(REPLACE(a_items(i),'|',','),','),ut_varchar2_list()); + end loop; + l_result.include_list := l_result.include_list multiset union all coalesce(l_items,ut_varchar2_list()); return l_result; end; member function exclude(a_items varchar2) return ut_equal is l_result ut_equal := self; begin - ut_utils.append_to_list(l_result.exclude_list, a_items); + l_result.exclude_list := l_result.exclude_list multiset union all coalesce(ut_utils.string_to_table(REPLACE(a_items,'|',','),','),ut_varchar2_list()); return l_result; end; member function exclude(a_items ut_varchar2_list) return ut_equal is l_result ut_equal := self; + l_items ut_varchar2_list := ut_varchar2_list(); begin - l_result.exclude_list := l_result.exclude_list multiset union all coalesce(a_items,ut_varchar2_list()); + for i in 1..a_items.count loop + l_items := l_items multiset union all coalesce(ut_utils.string_to_table(REPLACE(a_items(i),'|',','),','),ut_varchar2_list()); + end loop; + + l_result.exclude_list := l_result.exclude_list multiset union all coalesce(l_items,ut_varchar2_list()); return l_result; end; @@ -194,15 +203,29 @@ create or replace type body ut_equal as l_result ut_equal := self; begin l_result.is_unordered := ut_utils.boolean_to_int(true); - ut_utils.append_to_list(l_result.join_columns, a_columns); + l_result.join_columns := l_result.join_columns multiset union all coalesce(ut_utils.string_to_table(REPLACE(a_columns,'|',','),','),ut_varchar2_list()); + + select regexp_replace(column_value,'^((/ROW/)|^(//)|^(/\*/))?(.*)','\5') col_names + bulk collect into l_result.join_on_list + from table(l_result.join_columns); return l_result; end; member function join_by(a_columns ut_varchar2_list) return ut_equal is l_result ut_equal := self; + l_items ut_varchar2_list := ut_varchar2_list(); begin l_result.is_unordered := ut_utils.boolean_to_int(true); - l_result.join_columns := l_result.join_columns multiset union all coalesce(a_columns,ut_varchar2_list()); + for i in 1..a_columns.count loop + --TODO : idoiot proof solution for both include and exclude + l_items := l_items multiset union all coalesce(ut_utils.string_to_table(REPLACE(a_columns(i),'|',','),','),ut_varchar2_list()); + end loop; + l_result.join_columns := l_result.join_columns multiset union all coalesce(l_items,ut_varchar2_list()); + + select regexp_replace(column_value,'^((/ROW/)|^(//)|^(/\*/))?(.*)','\5') col_names + bulk collect into l_result.join_on_list + from table(l_result.join_columns); + return l_result; end; @@ -226,14 +249,38 @@ create or replace type body ut_equal as return ut_utils.to_xpath( coalesce(join_columns, ut_varchar2_list()) ); end; + member function get_join_by_list return ut_varchar2_list is + begin + return ( coalesce(join_columns, ut_varchar2_list()) ); + end; + + member function uc return ut_equal is + begin + return unordered_columns; + end; + + member function unordered_columns return ut_equal is + l_result ut_equal := self; + begin + l_result.is_column_order_enforced := ut_utils.boolean_to_int(false); + return l_result; + end; + + member function get_ordered_columns return boolean is + begin + return ut_utils.int_to_boolean(nvl(is_column_order_enforced,1)); + end; + overriding member function run_matcher(self in out nocopy ut_equal, a_actual ut_data_value) return boolean is l_result boolean; + l_actual ut_data_value; begin if self.expected.data_type = a_actual.data_type then if self.expected is of (ut_data_value_anydata) then l_result := 0 = treat(self.expected as ut_data_value_anydata).compare_implementation(a_actual, get_exclude_xpath(), get_include_xpath()); elsif self.expected is of (ut_data_value_refcursor) then - l_result := 0 = treat(self.expected as ut_data_value_refcursor).compare_implementation(a_actual, get_exclude_xpath(), get_include_xpath(), get_join_by_xpath(), get_unordered()); + l_actual := treat(a_actual as ut_data_value_refcursor).update_cursor_details(exclude_list, include_list,get_ordered_columns()); + l_result := 0 = treat(self.expected as ut_data_value_refcursor).update_cursor_details(exclude_list, include_list,get_ordered_columns()).compare_implementation(l_actual, get_unordered(), false, false, get_join_by_list()); else l_result := equal_with_nulls((self.expected = a_actual), a_actual); end if; @@ -246,11 +293,19 @@ create or replace type body ut_equal as overriding member function failure_message(a_actual ut_data_value) return varchar2 is l_result varchar2(32767); + l_actual ut_data_value; begin if self.expected.data_type = a_actual.data_type and self.expected.is_diffable then - l_result := - 'Actual: '||a_actual.get_object_info()||' '||self.description()||': '||self.expected.get_object_info() - || chr(10) || 'Diff:' || expected.diff(a_actual, get_exclude_xpath(), get_include_xpath(), get_join_by_xpath(), get_unordered()); + if self.expected is of (ut_data_value_refcursor) then + l_actual := treat(a_actual as ut_data_value_refcursor).update_cursor_details(exclude_list, include_list,get_ordered_columns()); + l_result := + 'Actual: '||a_actual.get_object_info()||' '||self.description()||': '||self.expected.get_object_info() + || chr(10) || 'Diff:' || treat(expected as ut_data_value_refcursor).update_cursor_details(exclude_list, include_list,get_ordered_columns()).diff(l_actual, get_unordered(),get_join_by_list()); + else + l_result := + 'Actual: '||a_actual.get_object_info()||' '||self.description()||': '||self.expected.get_object_info() + || chr(10) || 'Diff:' || expected.diff(a_actual, get_exclude_xpath(), get_include_xpath(), get_join_by_xpath(), get_unordered()); + end if; else l_result := (self as ut_matcher).failure_message(a_actual) || ': '|| self.expected.to_string_report(); end if; diff --git a/source/expectations/matchers/ut_equal.tps b/source/expectations/matchers/ut_equal.tps index 9968dba7f..3a065d7c0 100644 --- a/source/expectations/matchers/ut_equal.tps +++ b/source/expectations/matchers/ut_equal.tps @@ -1,4 +1,4 @@ -create or replace type ut_equal under ut_comparison_matcher( +create or replace type ut_equal force under ut_comparison_matcher( /* utPLSQL - Version 3 Copyright 2016 - 2018 utPLSQL Project @@ -36,6 +36,17 @@ create or replace type ut_equal under ut_comparison_matcher( */ join_columns ut_varchar2_list, + /** + * Holds list of columns to be used as a join PK on sys_refcursor comparision, prefiltered to remvoe + * xpath tag from from + */ + join_on_list ut_varchar2_list, + + /** + * Flag to force cursor comparision into ordered mode + */ + is_column_order_enforced number(1,0), + member procedure init(self in out nocopy ut_equal, a_expected ut_data_value, a_nulls_are_equal boolean), member function equal_with_nulls( self in ut_equal, a_assert_result boolean, a_actual ut_data_value) return boolean, constructor function ut_equal(self in out nocopy ut_equal, a_expected anydata, a_nulls_are_equal boolean := null) return self as result, @@ -66,8 +77,13 @@ create or replace type ut_equal under ut_comparison_matcher( member function get_exclude_xpath return varchar2, member function get_unordered return boolean, member function get_join_by_xpath return varchar2, + member function get_join_by_list return ut_varchar2_list, overriding member function run_matcher(self in out nocopy ut_equal, a_actual ut_data_value) return boolean, overriding member function failure_message(a_actual ut_data_value) return varchar2, - overriding member function failure_message_when_negated(a_actual ut_data_value) return varchar2 + overriding member function failure_message_when_negated(a_actual ut_data_value) return varchar2, + member function unordered_columns return ut_equal, + member function uc return ut_equal, + member function get_ordered_columns return boolean ) +not final / diff --git a/source/expectations/matchers/ut_include.tpb b/source/expectations/matchers/ut_include.tpb new file mode 100644 index 000000000..dbf014eb8 --- /dev/null +++ b/source/expectations/matchers/ut_include.tpb @@ -0,0 +1,102 @@ +create or replace type body ut_include 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. + */ + + member procedure init(self in out nocopy ut_include, a_expected ut_data_value) is + begin + self.self_type := $$plsql_unit; + self.expected := a_expected; + self.include_list := ut_varchar2_list(); + self.exclude_list := ut_varchar2_list(); + self.join_columns := ut_varchar2_list(); + end; + + constructor function ut_include(self in out nocopy ut_include, a_expected sys_refcursor) return self as result is + begin + init(ut_data_value_refcursor(a_expected)); + return; + end; + + member function get_inclusion_compare return boolean is + begin + return true; + end; + + member function negated return ut_include is + l_result ut_include := self; + begin + l_result.is_negated := ut_utils.boolean_to_int(true); + return l_result; + end; + + member function get_negated return boolean is + begin + return ut_utils.int_to_boolean(nvl(is_negated,0)); + end; + + overriding member function run_matcher(self in out nocopy ut_include, a_actual ut_data_value) return boolean is + l_result boolean; + l_actual ut_data_value; + l_result1 integer; + begin + if self.expected.data_type = a_actual.data_type then + l_actual := treat(a_actual as ut_data_value_refcursor).update_cursor_details(exclude_list, include_list,self.get_ordered_columns()); + l_result := + ( 0 + = treat( self.expected as ut_data_value_refcursor ).update_cursor_details( + exclude_list, include_list, self.get_ordered_columns( ) + ).compare_implementation( + l_actual, true, self.get_inclusion_compare( ), self.get_negated( ), self.get_join_by_list( ) + ) + ); + else + l_result := (self as ut_matcher).run_matcher(a_actual); + end if; + return l_result; + end; + + overriding member function run_matcher_negated(self in out nocopy ut_include, a_actual ut_data_value) return boolean is + begin + return run_matcher(a_actual); + end; + + overriding member function failure_message(a_actual ut_data_value) return varchar2 is + l_result varchar2(32767); + l_actual ut_data_value; + begin + if self.expected.data_type = a_actual.data_type and self.expected.is_diffable then + l_actual := treat(a_actual as ut_data_value_refcursor).update_cursor_details(exclude_list, include_list,self.get_ordered_columns()); + l_result := + 'Actual: '||a_actual.get_object_info()||' '||self.description()||': '||self.expected.get_object_info() + || chr(10) || 'Diff:' + || treat(expected as ut_data_value_refcursor).update_cursor_details( + exclude_list, include_list, self.get_ordered_columns() + ).diff(l_actual, true, self.get_join_by_list()); + else + l_result := (self as ut_matcher).failure_message(a_actual) || ': '|| self.expected.to_string_report(); + end if; + return l_result; + end; + + overriding member function failure_message_when_negated(a_actual ut_data_value) return varchar2 is + l_result varchar2(32767); + begin + return (self as ut_matcher).failure_message_when_negated(a_actual) || ':'|| expected.to_string_report(); + end; + +end; +/ diff --git a/source/expectations/matchers/ut_include.tps b/source/expectations/matchers/ut_include.tps new file mode 100644 index 000000000..b0e8a4bd3 --- /dev/null +++ b/source/expectations/matchers/ut_include.tps @@ -0,0 +1,37 @@ +create or replace type ut_include under ut_equal( + /* + 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. + */ + + /** + * Due to nature of inclusion compare the not is bit diffrente than standard. + * Result is false when even one element belongs which can cause overlap. + * e.g. set can fail at same time not_include and include. By that we mean + * that false include not necessary mean true not include. + */ + is_negated number(1,0), + + member procedure init(self in out nocopy ut_include, a_expected ut_data_value), + constructor function ut_include(self in out nocopy ut_include, a_expected sys_refcursor) return self as result, + member function get_inclusion_compare return boolean, + member function negated return ut_include, + member function get_negated return boolean, + overriding member function run_matcher(self in out nocopy ut_include, a_actual ut_data_value) return boolean, + overriding member function run_matcher_negated(self in out nocopy ut_include, a_actual ut_data_value) return boolean, + overriding member function failure_message(a_actual ut_data_value) return varchar2, + overriding member function failure_message_when_negated(a_actual ut_data_value) return varchar2 +) +/ diff --git a/source/expectations/ut_expectation.tpb b/source/expectations/ut_expectation.tpb index 26e0c1092..0372e0ddf 100644 --- a/source/expectations/ut_expectation.tpb +++ b/source/expectations/ut_expectation.tpb @@ -32,9 +32,10 @@ create or replace type body ut_expectation as l_matcher ut_matcher := a_matcher; l_message varchar2(32767); begin - + --Negated matcher for include option. l_expectation_result := l_matcher.run_matcher_negated( self.actual_data ); l_expectation_result := coalesce(l_expectation_result,false); + l_message := coalesce( l_matcher.error_message( self.actual_data ), l_matcher.failure_message_when_negated( self.actual_data ) ); ut_expectation_processor.add_expectation_result( ut_expectation_result( ut_utils.to_test_result( l_expectation_result ), self.description, l_message ) ); end; @@ -679,5 +680,25 @@ create or replace type body ut_expectation as self.not_to( ut_be_less_than (a_expected) ); end; + member procedure to_include(self in ut_expectation, a_expected sys_refcursor) is + begin + self.to_( ut_include(a_expected) ); + end; + + member procedure to_contain(self in ut_expectation, a_expected sys_refcursor) is + begin + self.to_( ut_include(a_expected) ); + end; + + member procedure not_to_include(self in ut_expectation, a_expected sys_refcursor) is + begin + self.not_to( ut_include(a_expected).negated ); + end; + + member procedure not_to_contain(self in ut_expectation, a_expected sys_refcursor) is + begin + self.not_to( ut_include(a_expected).negated ); + end; + end; / diff --git a/source/expectations/ut_expectation.tps b/source/expectations/ut_expectation.tps index d27a18377..215a0e00a 100644 --- a/source/expectations/ut_expectation.tps +++ b/source/expectations/ut_expectation.tps @@ -21,7 +21,7 @@ create or replace type ut_expectation authid current_user as object( --base matcher executors member procedure to_(self in ut_expectation, a_matcher ut_matcher), member procedure not_to(self in ut_expectation, a_matcher ut_matcher), - + --shortcuts member procedure to_be_null(self in ut_expectation), member procedure to_be_not_null(self in ut_expectation), @@ -158,7 +158,14 @@ create or replace type ut_expectation authid current_user as object( member procedure not_to_be_less_than(self in ut_expectation, a_expected timestamp_unconstrained), member procedure not_to_be_less_than(self in ut_expectation, a_expected timestamp_ltz_unconstrained), member procedure not_to_be_less_than(self in ut_expectation, a_expected timestamp_tz_unconstrained), - member procedure not_to_be_less_than(self in ut_expectation, a_expected yminterval_unconstrained) + member procedure not_to_be_less_than(self in ut_expectation, a_expected yminterval_unconstrained), + + member procedure to_include(self in ut_expectation, a_expected sys_refcursor), + member procedure to_contain(self in ut_expectation, a_expected sys_refcursor), + + member procedure not_to_include(self in ut_expectation, a_expected sys_refcursor), + member procedure not_to_contain(self in ut_expectation, a_expected sys_refcursor) + ) not final / diff --git a/source/expectations/ut_expectation_compound.tpb b/source/expectations/ut_expectation_compound.tpb index c620ec39c..f15911aab 100644 --- a/source/expectations/ut_expectation_compound.tpb +++ b/source/expectations/ut_expectation_compound.tpb @@ -34,7 +34,6 @@ create or replace type body ut_expectation_compound as self.not_to( ut_be_empty() ); end; - member procedure to_have_count(self in ut_expectation_compound, a_expected integer) is begin self.to_( ut_have_count(a_expected) ); @@ -76,6 +75,36 @@ create or replace type body ut_expectation_compound as return l_result; end; + member function to_include(a_expected sys_refcursor) return ut_expectation_compound is + l_result ut_expectation_compound := self; + begin + l_result.matcher := ut_include(a_expected); + return l_result; + end; + + member function to_contain(a_expected sys_refcursor) return ut_expectation_compound is + l_result ut_expectation_compound := self; + begin + l_result.matcher := ut_include(a_expected); + return l_result; + end; + + member function not_to_include(a_expected sys_refcursor) return ut_expectation_compound is + l_result ut_expectation_compound := self; + begin + l_result.matcher := ut_include(a_expected).negated; + l_result.negated := ut_utils.boolean_to_int(true); + return l_result; + end; + + member function not_to_contain(a_expected sys_refcursor) return ut_expectation_compound is + l_result ut_expectation_compound := self; + begin + l_result.matcher := ut_include(a_expected).negated; + l_result.negated := ut_utils.boolean_to_int(true); + return l_result; + end; + member function include(a_items varchar2) return ut_expectation_compound is l_result ut_expectation_compound; begin @@ -94,22 +123,22 @@ create or replace type body ut_expectation_compound as member procedure include(self in ut_expectation_compound, a_items varchar2) is begin - if ut_utils.int_to_boolean(negated) then - self.not_to( treat(matcher as ut_equal).include(a_items) ); - else - self.to_( treat(matcher as ut_equal).include(a_items) ); - end if; - end; + if ut_utils.int_to_boolean(negated) then + self.not_to( treat(matcher as ut_equal).include(a_items) ); + else + self.to_( treat(matcher as ut_equal).include(a_items) ); + end if; + end; member procedure include(self in ut_expectation_compound, a_items ut_varchar2_list) is begin + if ut_utils.int_to_boolean(negated) then + self.not_to( treat(matcher as ut_equal).include(a_items) ); + else + self.to_( treat(matcher as ut_equal).include(a_items) ); + end if; + end; - if ut_utils.int_to_boolean(negated) then - self.not_to( treat(matcher as ut_equal).include(a_items) ); - else - self.to_( treat(matcher as ut_equal).include(a_items) ); - end if; - end; member function exclude(a_items varchar2) return ut_expectation_compound is @@ -139,7 +168,6 @@ create or replace type body ut_expectation_compound as member procedure exclude(self in ut_expectation_compound, a_items ut_varchar2_list) is begin - if ut_utils.int_to_boolean(negated) then self.not_to( treat(matcher as ut_equal).exclude(a_items) ); else @@ -157,7 +185,6 @@ create or replace type body ut_expectation_compound as member procedure unordered(self in ut_expectation_compound) is begin - if ut_utils.int_to_boolean(negated) then self.not_to( treat(matcher as ut_equal).unordered ); else @@ -165,7 +192,7 @@ create or replace type body ut_expectation_compound as end if; end; - member function join_by(a_columns varchar2) return ut_expectation_compound is + member function join_by(a_columns varchar2) return ut_expectation_compound is l_result ut_expectation_compound; begin l_result := self; @@ -192,7 +219,6 @@ create or replace type body ut_expectation_compound as member procedure join_by(self in ut_expectation_compound, a_columns ut_varchar2_list) is begin - if ut_utils.int_to_boolean(negated) then self.not_to( treat(matcher as ut_equal).join_by(a_columns) ); else @@ -200,5 +226,34 @@ create or replace type body ut_expectation_compound as end if; end; + member function unordered_columns return ut_expectation_compound is + l_result ut_expectation_compound; + begin + l_result := self; + l_result.matcher := treat(l_result.matcher as ut_equal).unordered_columns; + return l_result; + end; + + member procedure unordered_columns(self in ut_expectation_compound) is + begin + + if ut_utils.int_to_boolean(negated) then + self.not_to( treat(matcher as ut_equal).unordered_columns ); + else + self.to_( treat(matcher as ut_equal).unordered_columns ); + end if; + end; + + member function uc return ut_expectation_compound is + l_result ut_expectation_compound; + begin + return unordered_columns; + end; + + member procedure uc(self in ut_expectation_compound) is + begin + unordered_columns; + end; + end; / diff --git a/source/expectations/ut_expectation_compound.tps b/source/expectations/ut_expectation_compound.tps index 529b8875d..3cf141047 100644 --- a/source/expectations/ut_expectation_compound.tps +++ b/source/expectations/ut_expectation_compound.tps @@ -29,6 +29,12 @@ create or replace type ut_expectation_compound under ut_expectation( member function to_equal(a_expected sys_refcursor, a_nulls_are_equal boolean := null) return ut_expectation_compound, member function not_to_equal(a_expected anydata, a_nulls_are_equal boolean := null) return ut_expectation_compound, member function not_to_equal(a_expected sys_refcursor, a_nulls_are_equal boolean := null) return ut_expectation_compound, + + member function to_include(a_expected sys_refcursor) return ut_expectation_compound, + member function to_contain(a_expected sys_refcursor) return ut_expectation_compound, + member function not_to_include(a_expected sys_refcursor) return ut_expectation_compound, + member function not_to_contain(a_expected sys_refcursor) return ut_expectation_compound, + member function include(a_items varchar2) return ut_expectation_compound, member function include(a_items ut_varchar2_list) return ut_expectation_compound, member procedure include(self in ut_expectation_compound, a_items varchar2), @@ -42,7 +48,11 @@ create or replace type ut_expectation_compound under ut_expectation( member function join_by(a_columns varchar2) return ut_expectation_compound, member function join_by(a_columns ut_varchar2_list) return ut_expectation_compound, member procedure join_by(self in ut_expectation_compound, a_columns varchar2), - member procedure join_by(self in ut_expectation_compound, a_columns ut_varchar2_list) + member procedure join_by(self in ut_expectation_compound, a_columns ut_varchar2_list), + member function unordered_columns return ut_expectation_compound, + member procedure unordered_columns(self in ut_expectation_compound), + member function uc return ut_expectation_compound, + member procedure uc(self in ut_expectation_compound) ) final -/ \ No newline at end of file +/ diff --git a/source/install.sql b/source/install.sql index 50d70c944..c029baaef 100644 --- a/source/install.sql +++ b/source/install.sql @@ -175,6 +175,9 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema --expectations and matchers @@install_component.sql 'expectations/data_values/ut_compound_data_tmp.sql' @@install_component.sql 'expectations/data_values/ut_compound_data_diff_tmp.sql' +@@install_component.sql 'expectations/data_values/ut_cursor_column.tps' +@@install_component.sql 'expectations/data_values/ut_cursor_column_tab.tps' +@@install_component.sql 'expectations/data_values/ut_cursor_details.tps' @@install_component.sql 'expectations/data_values/ut_data_value.tps' @@install_component.sql 'expectations/data_values/ut_compound_data_value.tps' @@install_component.sql 'expectations/data_values/ut_data_value_anydata.tps' @@ -196,7 +199,6 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'expectations/data_values/ut_key_anyval_pair.tps' @@install_component.sql 'expectations/data_values/ut_key_anyval_pairs.tps' @@install_component.sql 'expectations/data_values/ut_compound_data_helper.pks' -@@install_component.sql 'expectations/data_values/ut_curr_usr_compound_helper.pks' @@install_component.sql 'expectations/matchers/ut_matcher.tps' @@install_component.sql 'expectations/matchers/ut_comparison_matcher.tps' @@install_component.sql 'expectations/matchers/ut_be_false.tps' @@ -209,16 +211,18 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'expectations/matchers/ut_be_null.tps' @@install_component.sql 'expectations/matchers/ut_be_true.tps' @@install_component.sql 'expectations/matchers/ut_equal.tps' +@@install_component.sql 'expectations/matchers/ut_include.tps' @@install_component.sql 'expectations/matchers/ut_have_count.tps' @@install_component.sql 'expectations/matchers/ut_be_between.tps' @@install_component.sql 'expectations/matchers/ut_be_empty.tps' @@install_component.sql 'expectations/matchers/ut_match.tps' @@install_component.sql 'expectations/ut_expectation.tps' +@@install_component.sql 'expectations/data_values/ut_cursor_column.tpb' +@@install_component.sql 'expectations/data_values/ut_cursor_details.tpb' @@install_component.sql 'expectations/ut_expectation_compound.tps' @@install_component.sql 'expectations/data_values/ut_data_value.tpb' @@install_component.sql 'expectations/data_values/ut_compound_data_value.tpb' @@install_component.sql 'expectations/data_values/ut_compound_data_helper.pkb' -@@install_component.sql 'expectations/data_values/ut_curr_usr_compound_helper.pkb' @@install_component.sql 'expectations/data_values/ut_data_value_anydata.tpb' @@install_component.sql 'expectations/data_values/ut_data_value_object.tpb' @@install_component.sql 'expectations/data_values/ut_data_value_collection.tpb' @@ -247,6 +251,7 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'expectations/matchers/ut_be_null.tpb' @@install_component.sql 'expectations/matchers/ut_be_true.tpb' @@install_component.sql 'expectations/matchers/ut_equal.tpb' +@@install_component.sql 'expectations/matchers/ut_include.tpb' @@install_component.sql 'expectations/matchers/ut_have_count.tpb' @@install_component.sql 'expectations/matchers/ut_be_between.tpb' @@install_component.sql 'expectations/matchers/ut_be_empty.tpb' @@ -307,6 +312,8 @@ prompt Installing DBMSPLSQL Tables objects into &&ut3_owner schema @@install_component.sql 'api/equal.syn' @@install_component.sql 'api/have_count.syn' @@install_component.sql 'api/match.syn' +@@install_component.sql 'api/contain.syn' +@@install_component.sql 'api/include.syn' set linesize 200 set define on diff --git a/source/uninstall_objects.sql b/source/uninstall_objects.sql index 97f2c5a5d..a268acc8a 100644 --- a/source/uninstall_objects.sql +++ b/source/uninstall_objects.sql @@ -5,6 +5,10 @@ drop synonym have_count; drop synonym match; +drop synonym include; + +drop synonym contain; + drop synonym be_false; drop synonym be_empty; @@ -81,6 +85,8 @@ drop type ut_match force; drop type ut_be_between force; +drop type ut_include force; + drop type ut_equal force; drop type ut_be_true force; @@ -141,6 +147,12 @@ drop type ut_data_value_xmltype force; drop type ut_data_value force; +drop type ut_cursor_details force; + +drop type ut_cursor_column_tab force; + +drop type ut_cursor_column force; + drop table ut_compound_data_tmp; drop table ut_compound_data_diff_tmp; @@ -271,8 +283,6 @@ drop package ut_coverage_profiler; drop package ut_compound_data_helper; -drop package ut_curr_usr_compound_helper; - drop package ut_coverage_helper_profiler; drop type ut_have_count; diff --git a/test/core/expectations/test_expectations_cursor.pkb b/test/core/expectations/test_expectations_cursor.pkb index ac4fc9acf..ea3d50363 100644 --- a/test/core/expectations/test_expectations_cursor.pkb +++ b/test/core/expectations/test_expectations_cursor.pkb @@ -62,6 +62,7 @@ create or replace package body test_expectations_cursor is l_actual sys_refcursor; begin -- Arrange + ut.set_nls; open l_expected for select 1 as my_num, 'This is my test string' as my_string, @@ -74,6 +75,7 @@ create or replace package body test_expectations_cursor is to_clob('This is an even longer test clob') as my_clob, to_date('1984-09-05', 'YYYY-MM-DD') as my_date from dual; + ut.reset_nls; --Act ut3.ut.expect( l_actual ).to_equal( l_expected ); --Assert @@ -303,6 +305,80 @@ create or replace package body test_expectations_cursor is ut.expect(expectations.failed_expectations_data()).not_to_be_empty(); end; + procedure pass_on_different_column_order + as + l_expected sys_refcursor; + l_actual sys_refcursor; + begin + --Arrange + open l_expected for select 1 as col_1, 2 as col_2 from dual; + open l_actual for select 2 as col_2, 1 as col_1 from dual; + --Act + ut3.ut.expect( l_actual ).to_equal( l_expected ).unordered_columns; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure pass_on_diff_column_ord_uc + as + l_expected sys_refcursor; + l_actual sys_refcursor; + begin + --Arrange + open l_expected for select 1 as col_1, 2 as col_2 from dual; + open l_actual for select 2 as col_2, 1 as col_1 from dual; + --Act + ut3.ut.expect( l_actual ).to_equal( l_expected ).uc; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure fail_on_multi_diff_col_order + as + l_expected sys_refcursor; + l_actual sys_refcursor; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for select 1 as col_1, 2 as col_2,3 as col_3, 4 as col_4,5 col_5 from dual; + open l_actual for select 2 as col_2, 1 as col_1,40 as col_4, 5 as col_5, 30 col_3 from dual; + --Act + ut3.ut.expect( l_actual ).to_equal( l_expected ).unordered_columns; + --Assert + l_expected_message := q'[Actual: refcursor [ count = 1 ] was expected to equal: refcursor [ count = 1 ] +%Diff: +%Rows: [ 1 differences ] +%Row No. 1 - Actual: 4030 +%Row No. 1 - Expected: 34]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure fail_on_multi_diff_col_ord_uc + as + l_expected sys_refcursor; + l_actual sys_refcursor; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for select 1 as col_1, 2 as col_2,3 as col_3, 4 as col_4,5 col_5 from dual; + open l_actual for select 2 as col_2, 1 as col_1,40 as col_4, 5 as col_5, 30 col_3 from dual; + --Act + ut3.ut.expect( l_actual ).to_equal( l_expected ).uc; + --Assert + l_expected_message := q'[Actual: refcursor [ count = 1 ] was expected to equal: refcursor [ count = 1 ] +%Diff: +%Rows: [ 1 differences ] +%Row No. 1 - Actual: 4030 +%Row No. 1 - Expected: 34]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + procedure fail_on_different_row_order as l_expected sys_refcursor; @@ -407,20 +483,27 @@ create or replace package body test_expectations_cursor is as l_actual SYS_REFCURSOR; l_expected SYS_REFCURSOR; - l_error_code integer := -31011; --xpath_error + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); begin --Arrange open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; - begin --Act - ut3.ut.expect(l_actual).to_equal(l_expected, a_exclude=>'/ROW/A_COLUMN,\\//Some_Col'); + ut3.ut.expect(l_actual).to_equal(l_expected, a_exclude=>'/ROW/A_COLUMN,\\//Some_Col'); --Assert - ut.fail('Expected '||l_error_code||' but nothing was raised'); - exception - when others then - ut.expect(sqlcode).to_equal(l_error_code); - end; + l_expected_message := q'[Actual: refcursor [ count = 3 ] was expected to equal: refcursor [ count = 3 ] +%Diff: +%Rows: [ 3 differences ] +%Row No. 1 - Actual: d +%Row No. 1 - Expected: c +%Row No. 2 - Actual: d +%Row No. 2 - Expected: c +%Row No. 3 - Actual: d +%Row No. 3 - Expected: c]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); end; procedure exclude_columns_xpath @@ -479,25 +562,6 @@ create or replace package body test_expectations_cursor is ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; - procedure include_columns_xpath_invalid - as - l_actual SYS_REFCURSOR; - l_expected SYS_REFCURSOR; - begin - --Arrange - open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; - open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; - begin - --Act - ut3.ut.expect(l_actual).to_equal(l_expected).include('/ROW/RN,\\//A_Column,//SOME_COL'); - --Assert - ut.fail('Expected exception but nothing was raised'); - exception - when others then - ut.expect(sqlcode).to_be_between(-31013,-31011); - end; - end; - procedure include_columns_xpath as l_actual sys_refcursor; @@ -615,7 +679,9 @@ Rows: [ 1 differences ] l_expected_message := q'[Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ] Diff: Columns: - Column data-type is invalid. Expected: NUMBER, actual: VARCHAR2.]'; + Column data-type is invalid. Expected: NUMBER, actual: VARCHAR2. +Rows: [ all different ] + All rows are different as the columns position is not matching.]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); @@ -643,7 +709,6 @@ Columns:% ut.expect(l_actual_message).to_be_like(l_expected_message); end; - --%test(Reports column diff on cusror with different column positions) procedure column_diff_on_col_position is l_actual sys_refcursor; l_expected sys_refcursor; @@ -662,13 +727,27 @@ Columns: Column is misplaced. Expected position: 2, actual position: 4. Column is misplaced. Expected position: 3, actual position: 2. Column is misplaced. Expected position: 4, actual position: 3. -Rows: [ 2 differences ] - All rows are different as the columns are not matching.]'; +Rows: [ all different ] + All rows are different as the columns position is not matching.]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); end; + procedure column_diff_on_col_pos_unord is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum+1 col_1, rownum+2 col_2, rownum+3 col_3, rownum+4 col_4 from dual connect by level <=2; + open l_expected for select rownum+1 col_1, rownum+4 col_4, rownum+2 col_2, rownum+3 col_3 from dual connect by level <=2; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered_columns; + + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; --%test(Reports only mismatched columns on column data mismatch) procedure data_diff_on_col_data_mismatch is @@ -769,6 +848,42 @@ Rows: [ 4 differences ] ut.expect(l_actual_message).to_be_like(l_expected_message); end; + procedure col_and_data_diff_not_ordered is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for + select 1 as ID, 'JACK' as FIRST_NAME, 'SPARROW' AS LAST_NAME, 10000 AS SALARY from dual union all + select 2 as ID, 'LUKE' as FIRST_NAME, 'SKYWALKER' AS LAST_NAME, 1000 AS SALARY from dual union all + select 3 as ID, 'TONY' as FIRST_NAME, 'STARK' AS LAST_NAME, 100000 AS SALARY from dual; + open l_actual for + select 'M' AS GENDER, 'JACK' as FIRST_NAME, 'SPARROW' AS LAST_NAME, 1 as ID, '25000' AS SALARY from dual union all + select 'M' AS GENDER, 'TONY' as FIRST_NAME, 'STARK' AS LAST_NAME, 3 as ID, '100000' AS SALARY from dual union all + select 'F' AS GENDER, 'JESSICA' as FIRST_NAME, 'JONES' AS LAST_NAME, 4 as ID, '2345' AS SALARY from dual union all + select 'M' AS GENDER, 'LUKE' as FIRST_NAME, 'SKYWALKER' AS LAST_NAME, 2 as ID, '1000' AS SALARY from dual; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered_columns; + l_expected_message := q'[Actual: refcursor [ count = 4 ] was expected to equal: refcursor [ count = 3 ] +Diff: +Columns: + Column data-type is invalid. Expected: NUMBER, actual: VARCHAR2. + Column [position: 1, data-type: CHAR] is not expected in results. +Rows: [ 4 differences ] + Row No. 1 - Actual: 25000 + Row No. 1 - Expected: 10000 + Row No. 2 - Actual: TONYSTARK3100000 + Row No. 2 - Expected: 2LUKESKYWALKER1000 + Row No. 3 - Actual: JESSICAJONES42345 + Row No. 3 - Expected: 3TONYSTARK100000 + Row No. 4 - Extra: MLUKESKYWALKER21000]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + procedure prepare_table as pragma autonomous_transaction; @@ -893,15 +1008,15 @@ Rows: [ 4 differences ] l_expected sys_refcursor; begin --Arrange - open l_actual for select object_name from all_objects where rownum <=1100; - open l_expected for select object_name from all_objects where rownum <=1100; + open l_actual for select object_name from all_objects where rownum <=1100 order by object_id; + open l_expected for select object_name from all_objects where rownum <=1100 order by object_id; --Act ut3.ut.expect(l_actual).to_equal(l_expected); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; - + function get_cursor return sys_refcursor is l_cursor sys_refcursor; begin @@ -975,15 +1090,15 @@ Rows: [ 4 differences ] --Act ut3.ut.expect(l_actual).to_equal(l_expected); - l_expected_message := q'[Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% -Diff:% -Columns:% - Column [data-type: NUMBER] is missing. Expected column position: 1.% - Column [data-type: NUMBER] is missing. Expected column position: 2.% - Column <%1%> [position: 1, data-type: CHAR] is not expected in results.% - Column <%2%> [position: 2, data-type: CHAR] is not expected in results.% -Rows: [ 2 differences ]% - All rows are different as the columns are not matching.%]'; + l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ] +%Diff: +%Columns: +%Column [data-type: NUMBER] is missing. Expected column position: 1. +%Column [data-type: NUMBER] is missing. Expected column position: 2. +%Column <1> [position: 1, data-type: CHAR] is not expected in results. +%Column <2> [position: 2, data-type: CHAR] is not expected in results. +%Rows: [ all different ] +%All rows are different as the columns position is not matching.]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); @@ -1003,56 +1118,32 @@ Rows: [ 2 differences ]% --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; - - - procedure include_col_name_implicit is - l_actual SYS_REFCURSOR; - l_expected SYS_REFCURSOR; - begin - --Arrange - open l_actual for select rownum as rn, 'a', 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; - open l_expected for select rownum as rn, 'a', 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; - begin - --Act - ut3.ut.expect(l_actual).to_equal(l_expected).include(q'!/ROW/RN,'a',//SOME_COL!'); - --Assert - ut.fail('Expected exception but nothing was raised'); - exception - when others then - ut.expect(sqlcode).to_be_between(-31013,-31011); - end; - end; - - procedure exclude_col_name_implicit is + + procedure cursor_unorderd_compr_success is l_actual SYS_REFCURSOR; l_expected SYS_REFCURSOR; begin --Arrange - open l_actual for select rownum as rn, 'a', 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; - open l_expected for select rownum as rn, 'a', 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; - begin - --Act - ut3.ut.expect(l_actual).to_equal(l_expected).exclude(q'!/ROW/RN,'a',//SOME_COL!'); - --Assert - ut.fail('Expected exception but nothing was raised'); - exception - when others then - ut.expect(sqlcode).to_be_between(-31013,-31011); - end; + open l_actual for select username , user_id from all_users order by username asc; + open l_expected for select username , user_id from all_users order by username desc; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; - procedure cursor_unorderd_compr_success is + procedure cursor_unord_compr_success_uc is l_actual SYS_REFCURSOR; l_expected SYS_REFCURSOR; begin --Arrange - open l_actual for select username , user_id from all_users order by username asc; + open l_actual for select user_id, username from all_users order by username asc; open l_expected for select username , user_id from all_users order by username desc; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + ut3.ut.expect(l_actual).to_equal(l_expected).unordered().uc(); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); - end; + end; procedure cursor_unordered_compare_fail is l_actual SYS_REFCURSOR; @@ -1075,41 +1166,76 @@ Rows: [ 2 differences ]% l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% %Diff:% %Rows: [ 2 differences ]% -%Extra: test-666% -%Missing: test-667%]'; +%Extra: test-666% +%Missing: test-667%]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); end; + + procedure cursor_joinby_compare_uc is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select owner, object_id, object_name,object_type from all_objects where owner = user; + open l_expected for select object_id, owner, object_name,object_type from all_objects where owner = user; + + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OBJECT_ID').uc(); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; procedure cursor_joinby_compare is l_actual SYS_REFCURSOR; l_expected SYS_REFCURSOR; begin --Arrange - open l_actual for select owner, object_name,object_type from all_objects where owner = user - order by 1,2,3 asc; - open l_expected for select owner, object_name,object_type from all_objects where owner = user - order by 1,2,3 desc; + open l_actual for select object_id, owner, object_name,object_type from all_objects where owner = user; + open l_expected for select object_id, owner, object_name,object_type from all_objects where owner = user; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OWNER'); + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OBJECT_ID'); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; + procedure cursor_joinby_col_not_ord + as + l_expected sys_refcursor; + l_actual sys_refcursor; + l_actual_message varchar2(32767); + l_expected_message varchar2(32767); + begin + --Arrange + open l_expected for select 1 as col_1, 2 as col_2,3 as col_3, 4 as col_4,5 col_5 from dual; + open l_actual for select 2 as col_2, 1 as col_1,40 as col_4, 5 as col_5, 30 col_3 from dual; + --Act + ut3.ut.expect( l_actual ).to_equal( l_expected ).join_by('COL_1').unordered_columns; + --Assert + l_expected_message := q'[Actual: refcursor [ count = 1 ] was expected to equal: refcursor [ count = 1 ] +%Diff: +%Rows: [ 1 differences ] +%PK 1 - Actual: 30 +%PK 1 - Expected: 3 +%PK 1 - Actual: 40 +%PK 1 - Expected: 4]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + procedure cursor_joinby_compare_twocols is l_actual SYS_REFCURSOR; l_expected SYS_REFCURSOR; begin --Arrange - open l_actual for select owner, object_name,object_type from all_objects where owner = user - order by 1,2,3 asc; - open l_expected for select owner, object_name,object_type from all_objects where owner = user - order by 1,2,3 desc; + open l_actual for select object_id, owner, object_name,object_type from all_objects where owner = user; + open l_expected for select object_id, owner, object_name,object_type from all_objects where owner = user; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('OWNER,OBJECT_NAME')); + ut3.ut.expect(l_actual).to_equal(l_expected).join_by(ut3.ut_varchar2_list('OBJECT_ID,OBJECT_NAME')); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; @@ -1257,13 +1383,26 @@ Diff:% l_expected SYS_REFCURSOR; begin --Arrange - open l_actual for select object_name from all_objects where rownum <=1100; - open l_expected for select object_name from all_objects where rownum <=1100; + open l_actual for select level object_id, level || '_TEST' object_name from dual connect by level <=1100; + open l_expected for select level object_id, level || '_TEST' object_name from dual connect by level <=1100; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OBJECT_NAME'); + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('OBJECT_ID'); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; + + procedure cursor_unorder_compare_1000 is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select level object_id, level || '_TEST' object_name from dual connect by level <=1100; + open l_expected for select level object_id, level || '_TEST' object_name from dual connect by level <=1100; + --Act + ut3.ut.expect(l_actual).to_equal(l_expected).unordered; + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; procedure cursor_joinby_compare_fail is l_actual SYS_REFCURSOR; @@ -1310,8 +1449,8 @@ Diff:% l_expected_message := q'[%Actual: refcursor [ count = % ] was expected to equal: refcursor [ count = % ] %Diff:% %Rows: [ 2 differences ]% -%PK TEST-610 - Extra% -%PK TEST-600 - Missing%]'; +%PK TEST-610 - Extra: TEST-610% +%PK TEST-600 - Missing: TEST-600%]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); @@ -1366,7 +1505,7 @@ Diff:% open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('SOME_COL'); + ut3.ut.expect(l_actual).to_equal(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('RN'); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; @@ -1380,7 +1519,7 @@ Diff:% open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 4; open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('SOME_COL'); + ut3.ut.expect(l_actual).to_equal(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('RN'); --Assert ut.expect(expectations.failed_expectations_data()).to_be_empty(); end; @@ -1492,12 +1631,12 @@ Diff:% ut3.ut.expect(l_actual).to_equal(l_expected).unordered; l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 3 ]% Diff:% -Rows: [ 5 differences ] -%Extra: 2Something 22% -%Extra: 1Something 11% -%Missing: 1Somethings 11% -%Missing: 2Somethings 22% -%Missing: 3Somethings 33%]'; +Rows: [ 5 differences% +%Extra: 2Something 22% +%Extra: 1Something 11% +%Missing: 1Somethings 11% +%Missing: 2Somethings 22% +%Missing: 3Somethings 33%]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); @@ -1726,7 +1865,7 @@ Diff:% --Assert l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ]% %Diff:% -%Rows: [ 2 differences ]% +%Rows: [ 4 differences ]% %PK %%%%%%%%%%%%%Extra%%% %PK %%%%%%%%%%%%%Extra%%% %PK %%%%%%%%%%%%%Missing%%% @@ -1791,15 +1930,16 @@ Diff:% from dual connect by level <=2; --Act - ut3.ut.expect(l_actual).to_equal(l_expected).join_by('NESTED_TABLE/UT_KEY_VALUE_PAIR'); + ut3.ut.expect(l_actual).to_equal(l_expected).join_by('NESTED_TABLE/UT_KEY_VALUE_PAIRS'); --Assert l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 2 ] %Diff: -%Unable to join sets: -%Join key NESTED_TABLE/UT_KEY_VALUE_PAIR does not exists in expected% -%Join key NESTED_TABLE/UT_KEY_VALUE_PAIR does not exists in actual% -%Please make sure that your join clause is not refferring to collection element%]'; +%Rows: [ 4 differences ] +%Extra: 21Something 12Something 2 +%Extra: 11Something 12Something 2 +%Missing: 11Somethings 12Somethings 2 +%Missing: 21Somethings 12Somethings 2%]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); @@ -2013,12 +2153,529 @@ Diff:% l_expected_message := q'[%Actual: refcursor [ count = 2 ] was expected to equal: refcursor [ count = 3 ] %Diff: %Rows: [ 1 differences ] -%Missing: Table%]'; +%Missing: Table%]'; l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; --Assert ut.expect(l_actual_message).to_be_like(l_expected_message); end; + procedure cursor_to_include is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select owner, object_name,object_type from all_objects where owner = user + order by 1,2,3 asc; + open l_expected for select owner, object_name,object_type from all_objects where owner = user + and rownum < 20; + + --Act + ut3.ut.expect(l_actual).to_( ut3.include(l_expected) ); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_to_include_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select owner, object_name,object_type from all_objects where owner = user + and rownum < 5; + open l_expected for select owner, object_name,object_type from all_objects where owner = user + and rownum < 10; + + --Act + ut3.ut.expect(l_actual).to_include(l_expected); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 4 ] was expected to include: refcursor [ count = 9 ] +%Diff: +%Rows: [ 5 differences ] +%Missing: %%%% +%Missing: %%%% +%Missing: %%%% +%Missing: %%%% +%Missing: %%%%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_to_contain is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select owner, object_name,object_type from all_objects where owner = user + order by 1,2,3 asc; + open l_expected for select owner, object_name,object_type from all_objects where owner = user + and rownum < 20; + + --Act + ut3.ut.expect(l_actual).to_( ut3.contain(l_expected) ); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_to_contain_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select owner, object_name,object_type from all_objects where owner = user + and rownum < 5; + open l_expected for select owner, object_name,object_type from all_objects where owner = user + and rownum < 10; + + --Act + ut3.ut.expect(l_actual).to_contain(l_expected); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 4 ] was expected to include: refcursor [ count = 9 ] +%Diff: +%Rows: [ 5 differences ] +%Missing: %%%% +%Missing: %%%% +%Missing: %%%% +%Missing: %%%% +%Missing: %%%%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_to_include_joinby is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select username,user_id from all_users; + open l_expected for select username ,user_id from all_users where rownum < 5; + + --Act + ut3.ut.expect(l_actual).to_include(l_expected).join_by('USERNAME'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_to_include_joinby_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select username, user_id from all_users + union all + select 'TEST' username, -600 user_id from dual + order by 1 desc; + open l_expected for select username, user_id from all_users + union all + select 'TEST' username, -601 user_id from dual + order by 1 asc; + + --Act + ut3.ut.expect(l_actual).to_include(l_expected).join_by('USERNAME'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = % ] was expected to include: refcursor [ count = % ] +%Diff: +%Rows: [ 1 differences ] +%PK TEST - Actual: -600 +%PK TEST - Expected: -601%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + + end; + + procedure cursor_contain_joinby is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select username,user_id from all_users; + open l_expected for select username ,user_id from all_users where rownum < 5; + + --Act + ut3.ut.expect(l_actual).to_contain(l_expected).join_by('USERNAME'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_contain_joinby_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select username, user_id from all_users + union all + select 'TEST' username, -600 user_id from dual + order by 1 desc; + open l_expected for select username, user_id from all_users + union all + select 'TEST' username, -601 user_id from dual + order by 1 asc; + + --Act + ut3.ut.expect(l_actual).to_contain(l_expected).join_by('USERNAME'); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = % ] was expected to include: refcursor [ count = % ] +%Diff: +%Rows: [ 1 differences ] +%PK TEST - Actual: -600 +%PK TEST - Expected: -601%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + + end; + + procedure to_include_incl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 6; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_include(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure to_contain_cont_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 6; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_contain(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure to_inc_join_incl_cols_as_lst + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_include(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure to_cont_join_incl_cols_as_lst + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_contain(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure include_join_excl_cols_as_lst + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_include(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure contain_join_excl_cols_as_lst + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_contain(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure include_excl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_include(l_expected).exclude(ut3.ut_varchar2_list('A_COLUMN|//Some_Col')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure contain_excl_cols_as_list + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).to_contain(l_expected).exclude(ut3.ut_varchar2_list('A_COLUMN|//Some_Col')); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_not_to_contain + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + open l_expected for select 'TEST' username, -600 user_id from dual; + + open l_actual for select username, user_id from all_users + union all + select 'TEST1' username, -601 user_id from dual; + + --Act + ut3.ut.expect(l_actual).not_to_contain(l_expected); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_not_to_include + as + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + open l_expected for select 'TEST' username, -600 user_id from dual; + + open l_actual for select username, user_id from all_users + union all + select 'TEST1' username, -601 user_id from dual; + + --Act + ut3.ut.expect(l_actual).not_to_include(l_expected); + --Asserty + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_not_to_contain_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_expected for select 'TEST' username, -600 user_id from dual; + + open l_actual for select username, user_id from all_users + union all + select 'TEST' username, -600 user_id from dual; + + --Act + ut3.ut.expect(l_actual).not_to_contain(l_expected); + --Assert + l_expected_message := q'[%Actual: (refcursor [ count = % ])% +%Data-types:% +%VARCHAR2NUMBER% +%Data:% +%was expected not to include:(refcursor [ count = 1 ])% +%Data-types:% +%CHARNUMBER% +%Data:% +%TEST-600%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_not_to_include_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_expected for select 'TEST' username, -600 user_id from dual; + + open l_actual for select username, user_id from all_users + union all + select 'TEST' username, -600 user_id from dual; + + --Act + ut3.ut.expect(l_actual).not_to_include(l_expected); + --Assert + l_expected_message := q'[%Actual: (refcursor [ count = % ])% +%Data-types:% +%VARCHAR2NUMBER% +%Data:% +%was expected not to include:(refcursor [ count = 1 ])% +%Data-types:% +%CHARNUMBER% +%Data:% +%TEST-600%]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure cursor_not_to_contain_joinby is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select username,rownum * 10 user_id from all_users where rownum < 5; + open l_expected for select username||to_char(rownum) username ,rownum user_id from all_users where rownum < 5; + + --Act + ut3.ut.expect(l_actual).not_to_contain(l_expected).join_by('USER_ID'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure cursor_not_to_include_joinby is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select username,rownum * 10 user_id from all_users where rownum < 5; + open l_expected for select username||to_char(rownum) username ,rownum user_id from all_users where rownum < 5; + + --Act + ut3.ut.expect(l_actual).not_to_include(l_expected).join_by('USER_ID'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure not_inc_join_incl_cols_as_lst is + l_actual sys_refcursor; + l_expected sys_refcursor; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn, 'b' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).not_to_include(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('RN'); + --Assert + l_expected_message := q'[%Actual: (refcursor [ count = 9 ]) +%Data-types: +%NUMBERCHARCHARCHARCHAR +%Data: +%% +%was expected not to include:(refcursor [ count = 3 ]) +%Data-types: +%NUMBERCHARCHARCHARCHAR +%Data: +%1adxc2adxc3adxc]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + + procedure not_cont_join_incl_cols_as_lst is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'b' as "A_Column", 'c' as A_COLUMN, 'x' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum * 20 rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).not_to_contain(l_expected).include(ut3.ut_varchar2_list('RN','//A_Column','SOME_COL')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure not_inc_join_excl_cols_as_lst is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'y' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum * 20 as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).not_to_include(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure not_cont_join_excl_cols_as_lst is + l_actual sys_refcursor; + l_expected sys_refcursor; + begin + --Arrange + open l_actual for select rownum as rn, 'a' as "A_Column", 'c' as A_COLUMN, 'y' SOME_COL, 'd' "Some_Col" from dual a connect by level < 10; + open l_expected for select rownum * 20 as rn, 'a' as "A_Column", 'd' as A_COLUMN, 'x' SOME_COL, 'c' "Some_Col" from dual a connect by level < 4; + --Act + ut3.ut.expect(l_actual).not_to_contain(l_expected).exclude(ut3.ut_varchar2_list('//Some_Col','A_COLUMN')).join_by('RN'); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure to_include_duplicates is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + begin + --Arrange + open l_actual for select rownum as rn from dual a connect by level < 10 + union all + select rownum as rn from dual a connect by level < 4; + open l_expected for select rownum as rn from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_include(l_expected); + --Assert + ut.expect(expectations.failed_expectations_data()).to_be_empty(); + end; + + procedure to_include_duplicates_fail is + l_actual SYS_REFCURSOR; + l_expected SYS_REFCURSOR; + l_expected_message varchar2(32767); + l_actual_message varchar2(32767); + begin + --Arrange + open l_actual for select rownum as rn from dual a connect by level < 10; + open l_expected for select rownum as rn from dual a connect by level < 4 + union all select rownum as rn from dual a connect by level < 4; + + --Act + ut3.ut.expect(l_actual).to_include(l_expected); + --Assert + l_expected_message := q'[%Actual: refcursor [ count = 9 ] was expected to include: refcursor [ count = 6 ] +%Diff: +%Rows: [ 3 differences ] +%Missing: % +%Missing: % +%Missing: %]'; + l_actual_message := ut3.ut_expectation_processor.get_failed_expectations()(1).message; + --Assert + ut.expect(l_actual_message).to_be_like(l_expected_message); + end; + end; / diff --git a/test/core/expectations/test_expectations_cursor.pks b/test/core/expectations/test_expectations_cursor.pks index 88d59e1a3..ea942ddff 100644 --- a/test/core/expectations/test_expectations_cursor.pks +++ b/test/core/expectations/test_expectations_cursor.pks @@ -68,9 +68,21 @@ create or replace package test_expectations_cursor is --%test(Gives failure when different column name is used in cursors) procedure fail_on_different_column_name; - --%test(Gives failure when different column ordering is used in cursors) + --%test(Gives failure when different column ordering is used in cursors when enforced column order) procedure fail_on_different_column_order; + --%test(Pass when different column ordering is used in cursors) + procedure pass_on_different_column_order; + + --%test(Pass when different column ordering is used in cursors - shortname) + procedure pass_on_diff_column_ord_uc; + + --%test(Fail and highlight diffrence between columns when columns are unordered and different value) + procedure fail_on_multi_diff_col_order; + + --%test(Fail and highlight diffrence between columns when columns are unordered and different value - shortname) + procedure fail_on_multi_diff_col_ord_uc; + --%test(Gives failure when different row ordering is used in cursors) procedure fail_on_different_row_order; @@ -92,7 +104,7 @@ create or replace package test_expectations_cursor is --%test(Excludes comma separated list of mixed columns and XPath) procedure exclude_columns_as_mix_csv_lst; - --%test(Exclude columns fails on invalid XPath) + --%test(Exclude column with invalid filter will result in column being included ) procedure exclude_columns_xpath_invalid; --%test(Exclude columns by XPath is case sensitive) @@ -107,9 +119,6 @@ create or replace package test_expectations_cursor is --%test(Comma separated list of columns to include is case sensitive) procedure include_columns_as_csv; - --%test(Include columns fails on invalid XPath) - procedure include_columns_xpath_invalid; - --%test(Include columns by XPath is case sensitive) procedure include_columns_xpath; @@ -134,18 +143,24 @@ create or replace package test_expectations_cursor is --%test(Reports column diff on cursor with different column name) procedure column_diff_on_col_name_diff; - --%test(Reports column diff on cursor with different column positions) + --%test(Reports column diff on cursor with different column positions when column order is enforced) procedure column_diff_on_col_position; + --%test(Reports column diff on cursor with different column positions) + procedure column_diff_on_col_pos_unord; + --%test(Reports only mismatched columns on row data mismatch) procedure data_diff_on_col_data_mismatch; --%test(Reports only first 20 rows of diff and gives a full diff count) procedure data_diff_on_20_rows_only; - --%test(Reports data diff and column diff when both are different) + --%test(Reports data diff and column diff when both are different with enforced ordered columns) procedure column_and_data_diff; + --%test(Reports data diff and column diff when both are different when columns are not ordered) + procedure col_and_data_diff_not_ordered; + procedure prepare_table; procedure cleanup_table; @@ -193,21 +208,24 @@ create or replace package test_expectations_cursor is --%test(Reports column match on cursor with column name implicit ) procedure col_mtch_on_col_name_implicit; - --%test( Fail on passing implicit column name as include filter ) - procedure include_col_name_implicit; - - --%test( Fail on passing implicit column name as exclude filter ) - procedure exclude_col_name_implicit; - --%test( Compare cursors using unordered method success) procedure cursor_unorderd_compr_success; + --%test( Compare cursors using unordered method success and unordered columns position) + procedure cursor_unord_compr_success_uc; + --%test( Compare cursors using unordered method failure) procedure cursor_unordered_compare_fail; --%test( Compare cursors join by single key ) procedure cursor_joinby_compare; + --%test( Compare cursors join by single key with unordered columns position using shortname) + procedure cursor_joinby_compare_uc; + + --%test(Compare cursors by single key with unordered columns position) + procedure cursor_joinby_col_not_ord; + --%test( Compare cursors join by composite key) procedure cursor_joinby_compare_twocols; @@ -231,6 +249,9 @@ create or replace package test_expectations_cursor is --%test( Compare cursors join by single key more than 1000 rows) procedure cursor_joinby_compare_1000; + + --%test( Compare cursors unorder more than 1000 rows) + procedure cursor_unorder_compare_1000; --%test( Compare two column cursors join by and fail to match ) procedure cursor_joinby_compare_fail; @@ -319,5 +340,86 @@ create or replace package test_expectations_cursor is --%test( Unordered fix for issues with duplicate no : #764 ) procedure unordered_fix_764; + --%test( Fail cursor to include data from another cursor ) + procedure cursor_to_include_fail; + + --%test( Cursor include data from another cursor using second keyword) + procedure cursor_to_include; + + --%test( Fail cursor contains data from another cursor using second keyword) + procedure cursor_to_contain_fail; + + --%test( Cursor include data from another cursor with joinby) + procedure cursor_to_include_joinby; + + --%test( Fail cursor include data from another cursor with joinby) + procedure cursor_to_include_joinby_fail; + + --%test( Cursor contains data from another cursor with joinby) + procedure cursor_contain_joinby; + + --%test( Fail cursor contains data from another cursor with joinby) + procedure cursor_contain_joinby_fail; + + --%test(Cursor include data with of columns to include) + procedure to_include_incl_cols_as_list; + + --%test(Cursor contains data with of columns to include) + procedure to_contain_cont_cols_as_list; + + --%test(Cursor includes data with of columns to include and join by value) + procedure to_inc_join_incl_cols_as_lst; + + --%test(Cursor contains data with of columns to include and join by value) + procedure to_cont_join_incl_cols_as_lst; + + --%test(Cursor include data with of columns to exclude and join by value) + procedure include_join_excl_cols_as_lst; + + --%test(Cursor contains data with of columns to exclude and join by value) + procedure contain_join_excl_cols_as_lst; + + --%test(Cursor include data with of columns to exclude) + procedure include_excl_cols_as_list; + + --%test(Cursor contains data with of columns to exclude) + procedure contain_excl_cols_as_list; + + --%test( Cursor not to contains data from another cursor) + procedure cursor_not_to_contain; + + --%test( Cursor not to include data from another cursor) + procedure cursor_not_to_include; + + --%test( Cursor fail not to contains data from another cursor) + procedure cursor_not_to_contain_fail; + + --%test( Cursor fail not to include data from another cursor) + procedure cursor_not_to_include_fail; + + --%test( Cursor not contains data from another cursor with joinby clause) + procedure cursor_not_to_contain_joinby; + + --%test( Cursor not include data from another cursor with joinby clause) + procedure cursor_not_to_include_joinby; + + --%test(Cursor not include data with of columns to include and join by value - Fail) + procedure not_inc_join_incl_cols_as_lst; + + --%test(Cursor not contains data with of columns to include and join by value) + procedure not_cont_join_incl_cols_as_lst; + + --%test(Cursor not include data with of columns to exclude and join by value) + procedure not_inc_join_excl_cols_as_lst; + + --%test(Cursor not contains data with of columns to exclude and join by value) + procedure not_cont_join_excl_cols_as_lst; + + --%test(Cursor to include duplicates) + procedure to_include_duplicates; + + --%test(Cursor to include duplicates fail) + procedure to_include_duplicates_fail; + end; / diff --git a/test/core/min_grant_user/test_min_grant_user.pkb b/test/core/min_grant_user/test_min_grant_user.pkb index 906906fe1..0955e6313 100644 --- a/test/core/min_grant_user/test_min_grant_user.pkb +++ b/test/core/min_grant_user/test_min_grant_user.pkb @@ -53,7 +53,7 @@ create or replace package body test_min_grant_user is execute immediate 'begin ut3$user#.test_cursor_grants.run(); end;'; l_results := core.get_dbms_output_as_clob(); --Assert - ut.expect( l_results ).to_be_like( '%execute join by test [.% sec]' || + ut.expect( l_results ).to_be_like( '%execute join by test [% sec]' || '%1 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)%' ); end; diff --git a/test/install_and_run_tests.sh b/test/install_and_run_tests.sh index bd04495b5..1c0931708 100755 --- a/test/install_and_run_tests.sh +++ b/test/install_and_run_tests.sh @@ -10,6 +10,7 @@ time "$SQLCLI" ${UT3_TESTER}/${UT3_TESTER_PASSWORD}@//${CONNECTION_STR} @install cd .. + time utPLSQL-cli/bin/utplsql run ${UT3_TESTER}/${UT3_TESTER_PASSWORD}@${CONNECTION_STR} \ -source_path=source -owner=ut3 \ -test_path=test -c \