Here’s a note about a data error generated by using (possibly mis-using) the dbms_redefinition package (jump to conclusion). The original code to demonstrate the problem comes from a note on the Oracle Developer forum, and was brought to my attention by a tweet from Daniel Stein.
The critical feature of the demo is that we can end up with a column containing nulls despite being declared NOT NULL (and I don’t mean by that a simple “is not null” check constraint – which is not quite the same as a NOT NULL declaration).
Here’s the first part of a script, mostly copied from the forum post, that I’ve I’ve been running on 19.11.0.0:
rem
rem Script: redef_bug.sql
rem Author: Jonathan Lewis / Sebastian (User_6AT2M)
rem Dated: May 2022
rem
rem Last tested
rem 19.11.0.0
rem
create table test_nulls (
id number (8) constraint tn_pk primary key,
text varchar2 (25) constraint tn_nn_tx not null
)
/
create unique index i_test_nulls on test_nulls(text);
insert into test_nulls (id, text)
select rownum, 'some text '||rownum from dual connect by level <= 50;
-- create an empty copy, without the constraints or indexes
create table test_nulls_interim (
id number (8),
text varchar2 (25)
);
begin
dbms_redefinition.start_redef_table(
uname => user,
orig_table => 'test_nulls',
int_table => 'test_nulls_interim',
col_mapping => 'id id, substr(text, id, 2) text'
);
end;
/
The script creates a table with a primary key declared on an id column, a not null declaration on a text column and a unique index on the text column, then populates the table with 50 rows that have the form (N, “some text N”) where N is a number between 1 and 50.
After creating an empty copy of the table with no constraints or indexes I start an online redefinition – modifying the content of the text column as part of the redefinition. If you check the col_mapping carefully you will realise that when id reaches 13 the result from the substr() function becomes null.
This is where the trouble starts. If I now call dbms_redefition.copy_table_dependents() to add the original constraints and indexes to the interim table what’s Oracle going to do about the not null declaration on the text column?
declare
error_ct pls_integer;
begin
dbms_redefinition.copy_table_dependents(
uname => user,
orig_table => 'test_nulls',
int_table => 'test_nulls_interim',
num_errors => error_ct,
copy_indexes => 1,
copy_constraints=> true,
ignore_errors => false,
--
copy_triggers => false,
copy_privileges => false,
copy_statistics => false,
copy_mvlog => false
);
dbms_output.put_line('error count: '||error_ct);
end;
/
begin
dbms_redefinition.finish_redef_table(user, 'test_nulls', 'test_nulls_interim');
end;
/
drop table TEST_NULLS_INTERIM purge;
I’ve exposed all the parameters to the copy_table_dependents() procedure call in my code, and you can see that I’ve chosen to copy only the constraints and indexes, and I don’t want to ignore errors.
The PL/SQL anonymous block terminates successfully, doesn’t report any errors, and outputs an error count of zero. So let’s see what we’ve got as the final result of the redefinition.
column search_condition_vc format a20
break on table_name skip 1 on index_name
set echo on
select
table_name, index_name, column_name
from
user_ind_columns
order by
1,2,column_position
/
desc test_nulls
select count(*) from test_nulls;
select /*+ full(test_nulls) */ count(*) from test_nulls;
select
constraint_name,
constraint_type,
search_condition_vc,
status,
deferrable,
deferred,
validated
from
user_constraints
where
table_name = 'TEST_NULLS'
/
set echo off
Here are the results – my login.sql has a lot of column format commands so your results may look a lot messier if you run this bit of code. Here’s the echoed output:
SQL> select
2 table_name, index_name, column_name
3 from
4 user_ind_columns
5 order by
6 1,2,column_position
7 /
TABLE_NAME INDEX_NAME COLUMN_NAME
------------------------- -------------------- --------------------
TEST_NULLS I_TEST_NULLS TEXT
TN_PK ID
2 rows selected.
SQL>
SQL> desc test_nulls
Name Null? Type
------------------------------------------------------------------------ -------- -------------------------------------------------
ID NOT NULL NUMBER(8)
TEXT NOT NULL VARCHAR2(25)
SQL>
SQL> select count(*) from test_nulls;
COUNT(*)
----------
12
1 row selected.
SQL> select /*+ full(test_nulls) */ count(*) from test_nulls;
COUNT(*)
----------
50
1 row selected.
SQL>
SQL> select
2 constraint_name,
3 constraint_type,
4 search_condition_vc,
5 status,
6 deferrable,
7 deferred,
8 validated
9 from
10 user_constraints
11 where
12 table_name = 'TEST_NULLS'
13 /
CONSTRAINT_NAME C SEARCH_CONDITION_VC STATUS DEFERRABLE DEFERRED VALIDATED
-------------------- - -------------------- -------- -------------- --------- -------------
TN_NN_TX C "TEXT" IS NOT NULL ENABLED NOT DEFERRABLE IMMEDIATE VALIDATED
TN_PK P ENABLED NOT DEFERRABLE IMMEDIATE VALIDATED
Both indexes have arrived; both columns have NOT NULL declarations.
When you count the number of rows in the table it’s only 12 – unless you force a full tablescan in which case it’s 50. This happens because the NOT NULL declaration of column text allows the optimizer to use an index-only plan on the index i_test_nulls and there are 38 nulls in the table that don’t appear in the index.
The check on user_constraints shows that both the primary key constraint and the “is not null” check constraint are enabled and validated.
Conclusion
There is code in the copy_table_dependents() procedure that is defective, deficient and dangerous. Clearly I’ve done something that I probably shouldn’t have done (and, historically, I wouldn’t have done) but the code should still have protected me from an error that leaves the database in a state that is not internally consistent and can produce incorrect results.
In my example the copy_table_dependents() procedure has allowed me to add a NOT NULL declaration to a column after the col_mapping parameter of start_table_redef() has resulted in rows that have nulls in that column. There may be variants on this theme that allow other types of error to appear.
Notes
If you want to repeat this test and try some variations on the theme you will need a few non-trivial privileges (which can be granted through a role). For the basic dbms_redefinition package you will need: execute on dbms_redefinition, select any table, create any table, alter any table, lock any table, drop any table; and to execute the copy_table_dependents() procedure you will also need create any index, create any trigger.
The copy_table_dependents() procedure appeared (I think) in the 10g time-line; prior to that you had to sort out all the constraints and dependencies “by hand” – which you would probably manage online through the sync_interim_table() procedure (viz: “create an index, sync interim table, create next index, sync etc.) before calling the finish_redef_table() procedure. That being the case my immediate response to this issue was that if you don’t want the not null declaration on text then you can have to exclude the copy_constraints option when copying the table dependants; if you did want the not null declaration then you should have included it in the initial definition of the interim table because the start_redef_table() call would then have failed, raising:
ORA-12008: error in materialized view or zonemap refresh path
ORA-01400: cannot insert NULL into ("{schema}"."TEST_NULLS_INTERIM"."TEXT")
[…] Online redefinition defect (May 2022): using copy_table_dependencies produce metadata inconsistencies. […]
Pingback by Bug Catalogue | Oracle Scratchpad — May 4, 2022 @ 12:27 pm BST May 4,2022 |
[…] dbms_redefinition defect (May 2022): using copy_table_dependencies() can produce metadata inconsistencies. […]
Pingback by Infrastructure Catalogue | Oracle Scratchpad — May 4, 2022 @ 12:28 pm BST May 4,2022 |
Hi Jonathan – thanks for raising this one. I filed bug 34137095 for this problem. You can’t see this one since the problem is there in all Releases. As soon as it’s fixed we’ll backport it to RUs.
Comment by Hermann Baer — May 4, 2022 @ 7:55 pm BST May 4,2022 |
Hermann,
Thanks for following up on this one.
I think there will be a few other people looking out for the fix.
Regards
Jonathan Lewis
Comment by Jonathan Lewis — May 4, 2022 @ 8:22 pm BST May 4,2022 |