Search code examples
oracle-databasedateplsqloverlapping

Managing dates when they're overlapping


How to write a procedure so that when adding a new insert, rows are added appropriately?

Let's say i have a table:

create table test_table
(
code varchar2(10) not null,
type varchar2(50) not null,
start_date date not null,
end_date date not null,
parameter number
);

1. First test case:

In table we have:

insert into test_table values ('CODE', 'a', to_date('01.01.2021', 'DD,MM,YYYY'),  to_date('10.01.2021', 'DD,MM,YYYY'), 1);

[2021-01-01 - 2021-01-10] type = "a" parameter = 1

and when we want to insert:

insert into test_table values ('CODE', 'a', to_date('01.01.2021', 'DD,MM,YYYY'),  to_date('20.01.2021', 'DD,MM,YYYY'), 1)

[2021-01-11 - 2021-01-20] type = "a" parameter = 1

*Result should be:
2021-01-01 - 2021-01-20 type = "a" parameter = 1*

2. Second test case:

In table we have:

insert into test_table values ('CODE', 'a', to_date('01.01.2021', 'DD,MM,YYYY'),  to_date('10.01.2021', 'DD,MM,YYYY'), 1)

[2021-01-01 - 2021-01-10] type = "a" parameter = 1

and when we want to insert:

insert into test_table values ('CODE', 'a', to_date('06.01.2021', 'DD,MM,YYYY'),  to_date('20.01.2021', 'DD,MM,YYYY'), 2)

[2021-01-06 - 2021-01-20] type = "a" parameter = 2

*in result we should have:
[2021-01-01 - 2021-01-05] type = "a" parameter = 1
[2021-01-06 - 2021-01-20] type = "a" parameter = 2*

3. Third test case:

In table we have:

insert into test_table values ('CODE', 'a', to_date('01.01.2021', 'DD,MM,YYYY'),  to_date('10.01.2021', 'DD,MM,YYYY'), 1)

[2021-01-01 - 2021-01-20] type = "a" parameter = 1

and when we want to insert:

insert into test_table values ('CODE', 'a', to_date('06.01.2021', 'DD,MM,YYYY'),  to_date('15.01.2021', 'DD,MM,YYYY'), 2)

[2021-01-06 - 2021-01-15] type = "a" parameter = 2

*in result we should have:
[2021-01-01 - 2021-01-05] type = "a" parameter = 1
[2021-01-06 - 2021-01-15] type = "a" parameter = 2
[2021-01-16 - 2021-01-20] type = "a" parameter = 1*

Solution

  • When you insert a new date range that is completely contained in the middle of an existing date range then you need to: INSERT of the new range; UPDATE the existing range to the portion of that range before the new range; and INSERT a new range for the portion of the existing range after the new range. So you need a total of 3 changes.

    Similarly, when you insert a new date range that completely contains an existing range then you need to: INSERT the new range; and DELETE the existing range (or do a single UPDATE statement).

    You can use a single MERGE statement for all of these actions:

    MERGE INTO test_table dst
    USING (
      WITH new_data (code, type, start_date, end_date, parameter) AS (
        SELECT 'CODE2', 'a', DATE '2021-01-01', DATE '2021-01-20', 2 FROM DUAL
      )
      SELECT NULL AS rid,
             n.*,
             0 AS status -- Insert
      FROM   new_data n
      UNION ALL
      -- Existing rows overlapping before
      SELECT t.ROWID,
             t.code,
             t.type,
             t.start_date,
             n.start_date - INTERVAL '1' DAY,
             t.parameter,
             1 -- Update overlap before
      FROM   test_table t
             INNER JOIN new_data n
             ON (   t.start_date <= n.start_date
                AND t.end_date   >= n.start_date)
      UNION ALL
      SELECT t.ROWID,
             t.code,
             t.type,
             n.end_date + INTERVAL '1' DAY,
             t.end_date,
             t.parameter,
             CASE
             WHEN n.start_date <= t.end_date   AND t.end_date <= n.end_date
             THEN 2 -- Delete
             WHEN t.start_date < n.start_date  AND n.end_date <  t.end_date
             THEN 0 -- Insert overlap afterwards
             ELSE 1 -- Update overlap afterwards
             END
      FROM   test_table t
             INNER JOIN new_data n
             ON (   t.start_date <= n.end_date
                AND t.end_date   >= n.start_date)
      WHERE  NOT (t.start_date <= n.start_date AND t.end_date <= n.end_date)
    ) src
    ON (src.rid = dst.ROWID AND status > 0)
    WHEN MATCHED THEN
      UPDATE
      SET    code       = src.code,
             start_date = src.start_date,
             end_date   = src.end_date
      DELETE
      WHERE status = 2
      OR    src.start_date > src.end_date
    WHEN NOT MATCHED THEN
      INSERT (code, type, start_date, end_date, parameter)
      VALUES (src.code, src.type, src.start_date, src.end_date, src.parameter);
    

    db<>fiddle here