Issue Details (XML | Word | Printable)

Key: CORE-3362
Type: Improvement Improvement
Status: Resolved Resolved
Resolution: Fixed
Priority: Major Major
Assignee: Vlad Khorsun
Reporter: Vlad Khorsun
Votes: 4
Watchers: 7
Operations

If you were logged in you would be able to see more operations.
Firebird Core

Cursors should ignore changes made by the same statement

Created: 25/Feb/11 09:08 AM   Updated: 25/Apr/18 05:29 AM
Component/s: Engine
Affects Version/s: 2.1.0, 2.0.4, 2.1.1, 2.0.5, 2.1.2, 2.1.3, 1.5.6, 2.0.6, 2.5.0
Fix Version/s: 3.0 RC2

Issue Links:
Duplicate
 
Relate
 

QA Status: Done with caveats
Test Details:
Test ("core_3362_basic.fbt") verifies BASIC issues that were accumulated in miscenaleous tickets.
More complex cases (which involve not only SQL but also PSQL features) will
follow in separate .fbt in order to keep size of each test in reasonable limits.

See also test for CORE-5794


 All   Comments   Change History   Subversion Commits      Sort Order: Ascending order - Click to sort in descending order
Vlad Khorsun added a comment - 25/Feb/11 09:29 AM
Firebird used implicit cursors for INSERT FROM SELECT, UPDATE and DELETE statements.

For example INSERT FROM SELECT executed internally as

FOR SELECT ...
 DO INSERT ...

UPDATE\DELETE executed as

FOR SELECT ...
         AS CURSOR c
 DO UPDATE\DELETE ... WHERE CURRENT OF c

where FOR SELECT forms an implicit cursor based on UPDATE\DELETE statement.


SQL Standard (introduced after initial Interbase implementation of DML statements) requires that set of records which will be affected by UPDATE\DELETE statements should be evaluated first and real action (UPDATE or DELETE) should use this stable recordset. I.e. action implementation should not affect updating recordset in any way.

Firebird implementation not satisfied this requirement because no intermediate record set is created (no bookmarks is created, etc).
There is many examples of such non-standard behavior, some of them already reported in related tracker tickets.

The goal of this ticket is to formally describe issue, collect related bug-reports and to fix issue finally.

Adriano dos Santos Fernandes added a comment - 19/Jun/11 03:17 PM
After patches applied til today, there is still related bugs with FOR SELECT. The code below causes an infinity loop:

-------------
create table t (
  n integer
)!

insert into t values (1)!
insert into t values (2)!
insert into t values (3)!
insert into t values (4)!

create or alter procedure sp1 returns (n integer)
as
begin
  for select n from t into n
  do
  begin
      insert into t values (:n);
      suspend;
  end
end!

select * from sp1!
-------------

Dmitry Yemanov added a comment - 19/Jun/11 04:06 PM
AFAIK, the SQL specification declares stability for IUD[M] statements only. I'm not sure compound statements should count here. In the ideal world it would surely be desirable, but IIRC it's quite hard to implement it now. Vlad will hopefully correct me if I'm wrong.

Adriano dos Santos Fernandes added a comment - 19/Jun/11 04:23 PM
May be, but after initial bugs Vlad said:

"Savepoint mechanism was choosen after long and careful thinking on issue
and still looks most cheap way to have stable implicit cursors (and FOR SELECT
ones) to me."

This test case was sent to him, I'm just putting it here in the tracker to not get lost forever.

Vlad Khorsun added a comment - 19/Jun/11 05:14 PM
I still not closed this ticket exactly by this reason.

We discussed with Alex (privately) possible solution for stability of PSQL-cursors and i found it overcomplicated.

We should decide if we need stable PSQL-cursors at all, if its corresponds to Firebird's PSQL nature, and if related
costs (implementation, run-time performance penalty, increased code complexity) is acceptible.

Pavel Zotov added a comment - 18/Oct/11 08:49 AM
(may be it is duplicate, but...)
I could not find any issues about wrong (and also unpredictable) result of UPDATE in such cases:

CASE_1.
=======
recreate table t(id int primary key, f01 int);
commit;
insert into t values(1, 100);
insert into t values(2, 200);
insert into t values(3, 300);
commit;

SQL> update t set f01=(select sum(f01) from t);
SQL> select * from t;

          ID F01
============ ============
           1 600
           2 1100
           3 2000


CASE_2.
=======
recreate table t(id int primary key, f01 int);
commit;
-- "reverse" order of values in f01 for same IDs: from big to small
insert into t values(1, 300);
insert into t values(2, 200);
insert into t values(3, 100);
commit;

SQL> recreate table t(id int primary key, f01 int);
SQL> commit;
SQL> insert into t values(1, 300);
SQL> insert into t values(2, 200);
SQL> insert into t values(3, 100);
SQL> commit;
SQL> update t set f01=(select sum(f01) from t);
SQL> select * from t;

          ID F01
============ ============
           1 600
           2 900
           3 1600

Right result in all cases shoud be the same as in the following EB:
set term ^;
execute block returns(id int,f01 int) as
declare s01 int;
begin
  for select sum(f01) from t into s01
   do update t set f01=:s01;
  for select id,f01 from t into id,f01 do suspend;
end^
set term ;^

Vlad Khorsun added a comment - 20/Oct/11 08:54 AM
Pavel,

your case works correctly in FB3, i.e. after update all rows have value 600 in field F01.

Adriano dos Santos Fernandes added a comment - 30/Aug/12 03:32 PM
Vlad,

This code is incorrectly returning 2, 1. The correct is 2, 2.


recreate table z1 (n integer)!
insert into z1 values (1)!

execute block returns (x1 integer, x2 integer)
as
begin
    insert into z1 values (2);
    select max(n) from z1 into x1;
    x2 = (select max(n) from z1);
    suspend;
end!

Vlad Khorsun added a comment - 04/Sep/12 12:09 PM
Adriano,

good example.

Looks like we have internal savepoint for statement

select max(n) from z1 into x1;

and have no such savepoint for statement

x2 = (select max(n) from z1);

The simplest way to fix this issue is to add savepoint around (select max(n) from z1). It will be empty and will cost almost no cpu\memory.
What do you think ?

Adriano dos Santos Fernandes added a comment - 06/Sep/12 12:37 AM
Vlad, I do not know much about savepoints internals.

If you say it has no side effect, then go for it.

Vlad Khorsun added a comment - 24/Sep/12 02:34 PM
New patch is committed

Pavel Zotov added a comment - 03/Jan/14 05:07 PM
It seems that we have a trouble in FB-3 when update table with FK constraint that references to itself and have been created with update cascade clause.

DDL:
====
SQL> create database 'tmp.fdb';
SQL> show version;
ISQL Version: LI-T3.0.0.30813 Firebird 3.0 Alpha 2
Server version:
Firebird/Linux/AMD/Intel/x64 (access method), version "LI-T3.0.0.30813 Firebird 3.0 Alpha 2"
on disk structure version 12.0
SQL> create table t(
CON> x int, y int,
CON> constraint t_pk primary key(x),
CON> constraint t_fk foreign key(y) references t(x) on update cascade -- SELF-REFERENCING
CON> );

SQL> insert into t(x, y) values(1, null);
SQL> insert into t(x, y) values(2, 1 );
SQL> insert into t(x, y) values(3, 2 );
SQL> insert into t(x, y) values(4, 3 );
SQL> insert into t(x, y) values(5, 4 );
SQL> update t set y=5 where x=1; -- "closure" of chain
SQL> select * from t;

           X Y
============ ============
           1 5
           2 1
           3 2
           4 3
           5 4

SQL> commit;

SQL> show table t;
X INTEGER Not Null
Y INTEGER Nullable
CONSTRAINT T_FK:
  Foreign key (Y) References T (X) On Update Cascade
CONSTRAINT T_PK:
  Primary key (X)

TEST:
=====
SQL> update t set x=y+1;
SQL> select * from t;

           X Y
============ ============
           6 5
           2 1 -- <<< ??? child key WITHOUT parent ??? <<<
           3 2
           4 3
           5 4

-- aux. testing query to insure that we have "orphan" child key:
SQL> select d.x child_x, d.y child_y, m.x parent_x, m.y parent_y from t d left join t m on d.y=m.x where m.x is null;

     CHILD_X CHILD_Y PARENT_X PARENT_Y
============ ============ ============ ============
           2 1 <null> <null>


Compare with 2.5.3.26726 (13-dec-2013):
=====================
SQL> update t set x=y+1;
SQL> select * from t;

           X Y
============ ============
           6 10
           7 6
           8 7
           9 8
          10 9

(no "orphan" childs as we can see)

PS.
Attempt to backup and restore database with "orphan" child record in FB-3 leads, of course, to abend:
. . .
gbak:creating indexes
gbak: activating and creating deferred index T_PK
gbak: activating and creating deferred index T_FK
gbak:cannot commit index T_FK
gbak: ERROR:violation of FOREIGN KEY constraint "T_FK" on table "T"
gbak: ERROR: Foreign key reference target does not exist
gbak:committing metadata
gbak:fixing system generators
gbak:finishing, closing, and going home
gbak:Database is not online due to failure to activate one or more indices.
gbak:Run gfix -online to bring database online without active indices.

Pavel Zotov added a comment - 03/Jan/14 05:13 PM
Some days ago I've found a couple more samples which make me think that FB still have something unsolved in cursor stability.

SAMPLE #1.
##########
DDL:
===
recreate table t(x int, y int);
commit;
insert into t values(1, 1);
insert into t values(2, 2);
insert into t values(3, 3);
insert into t values(4, 4);
insert into t values(5, 5);
commit;

DML:
====
update t set x=null, y=(select c from (select count(x)over() c from t) rows 1);
select * from t;

Result:

      X Y
======= ============
 <null> 5
 <null> 4
 <null> 3
 <null> 2
 <null> 1


PS-1. Same test in Oracle 11.2g:

set null '<null>'
update t set x=null, y=( select c from (select count(x)over() c from t) where rownum=1 );
select * from t;

         X Y
---------- ----------
<null> 5
<null> 5
<null> 5
<null> 5
<null> 5


PS-2. Same test in MS SQL 2005 Dev Edition:

create table t(x int, y int)
go
insert into t values(1,1)
insert into t values(2,2)
insert into t values(3,3)
insert into t values(4,4)
insert into t values(5,5)
go

update t set x=null, y=( select top 1 c from (select count(x)over() c from t) t );
select * from t

x y
NULL 5
NULL 5
NULL 5
NULL 5
NULL 5


SAMPLE #2
##########

recreate table t(x int, y int);
commit;
insert into t values(1, 1);
insert into t values(2, 2);
insert into t values(3, 3);
insert into t values(4, 4);
insert into t values(5, 5);
commit;


Firebird:
=======
update t
    set y=(select (select sum(x) from t tx where tx.x<=tt.x)
             from t tt
             order by y desc rows 1
          );
select * from t;
  X Y
=== ============
  1 15
  2 1
  3 1
  4 1
  5 1


Same test in Oracle 11.2g:
=====================

update t
   set y=(select s from (
                          select (select sum(x) from t tx where tx.x<=tt.x) s
                          from t tt order by y desc
                        )
             where rownum<2
         );
select * from t;

         X Y
---------- ----------
         1 15
         2 15
         3 15
         4 15
         5 15

Vlad Khorsun added a comment - 09/Jan/14 10:33 AM
Cursor stability should be fixed now.

As for self-referencing FK with UPDATE CASCADE - it was never works correctly and stable cursor just slightly changed already wrong behavior.
The core of the issue is order in which cascading triggers are fired. They should fire after whole base UPDATE is finished while currently
cascading triggers are fired while records are updated. With non self-referencing FK there is no problem with current implementation.

Pavel Zotov added a comment - 01/Mar/14 02:22 PM - edited
Seems that at least one issue still exists.

Test case:
========
SQL> show version;
ISQL Version: LI-T3.0.0.30889 Firebird 3.0 Alpha 2
Server version:
Firebird/Linux/AMD/Intel/x64 (access method), version "LI-T3.0.0.30889 Firebird 3.0 Alpha 2"
Firebird/Linux/AMD/Intel/x64 (remote server), version "LI-T3.0.0.30889 Firebird 3.0 Alpha 2/tcp (oel64)/P13"
Firebird/Linux/AMD/Intel/x64 (remote interface), version "LI-T3.0.0.30889 Firebird 3.0 Alpha 2/tcp (oel64)/P13"
on disk structure version 12.0
SQL> recreate table t(id int primary key, x int);
SQL> commit;
SQL> insert into t(id, x) select row_number()over(),row_number()over() from rdb$types rows 3;
SQL> commit;
SQL> update t set x=null where x not in(select x from t where x is null);
SQL> select * from t;

          ID X
============ ============
           1 <null>
           2 2
           3 3

Compare with Oracle:
========
test@ora112g>set null '<null>'
test@ora112g>set feed off;
test@ora112g>set timing off;
test@ora112g>create table t(id number primary key, x number);
test@ora112g>insert into t values(1, 1);
test@ora112g>insert into t values(2, 2);
test@ora112g>insert into t values(3, 3);
test@ora112g>commit;
test@ora112g>update t set x=null where x not in(select x from t where x is null);
test@ora112g>select * from t;

        ID X
---------- ----------
         1 <null>
         2 <null>
         3 <null>

(same result in MS SQL: all THREE records are updated with NULL).

Pavel Zotov added a comment - 12/Mar/14 04:56 PM
one more sample (maybe related to previous - I don`t know)

DDL:
====
create table t(x int, y int);
insert into t values(1, 100);
insert into t values(2, 200);
insert into t values(3, 300);
commit;
create view v as select y,count(*) cnt from t group by y;

var-1:
=====
update t set y=null where 2 not in(select count(*) cnt from t group by y);
select * from t;

      X Y
======= ============
      2 <null>
      3 <null>
      1 <null>

(that's OK)

var-2:
=====
update t set y=null where 2 not in( select cnt from v );

X Y
= ============
2 <null>
3 <null>
1 100

Ora 11.2g and MS SQL results for same DDL+DML as in "var-2" are:
 X Y
-- ----------
 1 ***null***
 2 ***null***
 3 ***null***

Pavel Zotov added a comment - 16/Mar/14 09:51 AM
The following script gives unpredictable result with each run:

commit;
recreate table t(x int, y int);
recreate table t2(x int, y int);
commit;

insert into t select r1, r2
from (select 6-row_number()over() r1, row_number()over() r2 from rdb$types rows 5)
order by rand();
commit;
/*
Table `t` now contains such data:
SQL> select x, y, x+y s from t;

           X Y S

           1 5 6
           2 4 6
           3 3 6
           4 2 6
           5 1 6
*/

insert into t2 select * from t order by rand();
commit;
--select * from t2;
merge into t using(select x,y from t2) s on t.x=s.y
when matched
  then update set t.y=(select count(*) from t where x<>y);

select * from t;
rollback;

If you run this script several times you'll see that table `t` contains new values int `y` column for every new run.

Vlad Khorsun added a comment - 19/Mar/14 10:54 PM
Cases with NOT IN (from March 1 and March 12) should be fixed currently - patch is committed.

Pavel Zotov added a comment - 14/May/15 06:40 AM
[hvlad, 09/Jan/14 10:33 AM]:
> As for self-referencing FK with UPDATE CASCADE - it was never works correctly

It seems that "ON UPDATE SET NULL" (rather than `CASCADE`) also doesn`t work correctly.
And this looks sorrowful because following code works OK on WI-V2.0.7.13318, WI-V2.1.7.18553 and WI-V2.5.5.26870:

Test:
====

recreate table test(
    id int constraint test_pk_id primary key using index test_pk_id,
    pid int constraint test_fk_pid2id references test(id)
    on update SET NULL
);
commit;
insert into test values( 5, null );
insert into test values( 4, 5 );
insert into test values( 3, 4 );
insert into test values( 2, 3 );
insert into test values( 1, 2 );
commit;
update test set pid=1 where id=5;
commit;

update test set id = id + 1 order by id desc;
select * from test;
rollback;


Result on 2.0 -2.5:

ID PID
== ============
 6 <null>
 5 <null>
 4 <null>
 3 <null>
 2 <null>

Result on 3.0 (checked on WI-T3.0.0.31828):

 ID PID
=== ============
  6 <null>
  5 5
  4 4
  3 3
  2 2

One need to add 2nd `UPDATE` statement in 3.0 to make values in PID become null, i.e.:

update test set id = id + 1 order by id desc;
update test set id = id + 1 order by id desc;
select * from test;

Pavel Zotov added a comment - 13/Sep/15 09:33 PM
One more sample, now with MERGE.

Test script:
=========
recreate table t1(id int);
recreate table t2(id int, x int);
commit;
insert into t1 values(1);
insert into t1 values(2);
insert into t1 values(3);
commit;

set echo on;

insert into t2 (id, x) select id, (select sum(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

insert into t2 (id, x) select id, (select min(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

insert into t2 (id, x) select id, (select max(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

insert into t2 (id, x) select id, (select count(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;


merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select sum(id) from t2) );
set echo off; select * from t2; rollback; set echo on;

merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select min(id) from t2) );
set echo off; select * from t2; rollback; set echo on;

merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select max(id) from t2) );
set echo off; select * from t2; rollback; set echo on;

merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select count(*) from t2) );
set echo off; select * from t2; rollback; set echo on;

Output:
======


insert into t2 (id, x) select id, (select sum(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 <null>
           2 <null>
           3 <null>
-- (i.e. OK: statement "(select sum(id) from t2)" should not be affected by new rows that appear in t2 while it is filled up)

insert into t2 (id, x) select id, (select min(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 <null>
           2 <null>
           3 <null>
-- OK

insert into t2 (id, x) select id, (select max(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 <null>
           2 <null>
           3 <null>
-- OK

insert into t2 (id, x) select id, (select count(id) from t2) from t1;
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 0
           2 0
           3 0
-- OK


merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select sum(id) from t2) );
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 <null>
           2 1
           3 3
-- WRONG ?

merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select min(id) from t2) );
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 <null>
           2 1
           3 1
-- Seems also wrong...

merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select max(id) from t2) );
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 <null>
           2 1
           3 2
-- And this also seems wrong...

merge into t2 using t1 on t1.id=t2.id when not matched then insert (id, x) values(t1.id, (select count(*) from t2) );
set echo off; select * from t2; rollback; set echo on;

          ID X
============ ============
           1 0
           2 1
           3 2
-- And this too.

Vlad Khorsun added a comment - 25/Feb/16 09:28 AM
MERGE should be fixed now.

It seems there is no more known issues with stable cursor (except of issues with self-referencing FK, which is another story, i think)

Pavel Zotov added a comment - 26/Feb/16 02:18 PM - edited
On 3.0.0.32362 all samples with MERGE that were mentioned here work OK.

Vlad Khorsun added a comment - 11/Aug/16 06:01 PM
Cascade updates with self-referencing FK should be fixed now, see CORE-5305