Postgres: Check if two boxes overlap - stored-procedures

I currently have the following tables in a database:
create table map (
id bigint not null unique,
zone box not null,
...
primary key(id)
);
create table other_map (
id bigint not null unique,
zone box not null,
...
primary key(id),
foreign key(id) references map(id)
);
I don't want to allow a new row to be inserted in other_map if there is a row in map whose id is equal to the new entry's id and their zone attributes overlap. I found this answer, which explains how to detect overlapping boxes, but I'd like to know how to (best) apply that in Postgres.
This is what I've come up with so far, using a trigger and a stored procedure:
CREATE OR REPLACE FUNCTION PROC_other_map_IU()
RETURNS TRIGGER AS $PROC_other_map_IU$
DECLARE
id bigint;
zone box;
BEGIN
SELECT map.id, map.zone INTO id, zone
FROM map
WHERE map.id = NEW.id;
IF zone = NEW.zone THEN
RAISE EXCEPTION '%\'s zone overlaps with existing %\'s zone', NEW.id, id;
END IF;
RETURN NEW;
END;
$PROC_other_map_IU$ LANGUAGE plpgsql;
CREATE TRIGGER TR_other_map_IU
AFTER INSERT OR UPDATE
ON other_map
FOR EACH ROW EXECUTE PROCEDURE PROC_other_map_IU();
Now obviously this is wrong, because it simply checks if the zone attributes are equal.
Thank you in advance for your input! Cheers!

Took me a while, but Postgres' geometric functions and operators (more specifically the && - or overlap - operator) do exactly what I wanted:
IF zone && NEW.zone THEN

Related

utc_current as default value in informix

Use utc_current as default value in a field of an informix database table.
My idea is to do something like this, so that when that record is inserted or updated, the value is automatically increased since datetime or timestamp doesn't work for me.
CREATE TABLE tab1
(
id VARCHAR(128) NOT NULL,
update_ts integer DEFAULT dbinfo('utc_current') ,
modcount BIGINT,
);

Calculated time of day field in Postgres from timestamp

I have a timestamp with time zone field named statdate and the entry looks like this 2021-11-17 12:47:54-08. I want to create a field with just the time of day expressed locally, so it would look like 12:47:54. (This data was recorded on an iPhone and it's 12:28 PST). (Go to bottom of post for solution using views from #AdrianKalver)
select *,statdate :: timestamp :: time as stattime from table
works in PGAdmin and an example result is 12:47:54 as desired. How do I make this an alter table
ALTER TABLE tablename add COLUMN stattime timestamp generated always AS (select *,statdate :: timestamp :: time as stattime from tablename) stored;
is the wrong syntax.
ALTER TABLE tablename add COLUMN stattime timestamp generated always AS ( EXTRACT(HOUR FROM statdate) || ':' || EXTRACT(MINUTE FROM statdate) || ':' || EXTRACT(SECOND FROM statdate)) stored;
ERROR: generation expression is not immutable which I'm presuming is a type problem, although postgres can concatenate strings and numbers with this syntax.
Just tried something else
ALTER TABLE tablename add COLUMN stattime timestamp generated always AS ( Cast(EXTRACT(HOUR FROM statdate) as text) || ':' || cast(EXTRACT(MINUTE FROM statdate) as text) || ':' || cast(EXTRACT(SECOND FROM statdate) as text) ) stored; -- ERROR: generation expression is not immutable
I'm using the hours and minutes for a graph and I can't get in the middle of the Chartkick. Could do it in High Charts, but think it will be simpler to create the view chart and use that. The Rails/Chartkick looks like
<%= line_chart TableName.where(statdate: start..current_date).pluck(:statdate, :y_axis) %>
and can't break that apart. So will go with creating a View Table.
What's the right way to do this? I've looked here and at the postgresql docs and not having much luck.
Following comments, the solution
CREATE OR REPLACE VIEW public.view_bp_with_time AS
SELECT
id,
statdate,
statdate :: time AS stattime,
y-axis
FROM table_name
ORDER BY statdate
Now to bring into Rails. Not as straightforward as I thought. And I'm off the computer for the next week.
Per here:
https://www.postgresql.org/docs/current/sql-createtable.html
GENERATED ALWAYS AS ( generation_expr ) STORED
This clause creates the column as a generated column. The column cannot be written to, and when read the result of the specified expression will be returned.
The keyword STORED is required to signify that the column will be computed on write and will be stored on disk.
The generation expression can refer to other columns in the table, but not other generated columns. Any functions and operators used must be immutable. References to other tables are not allowed.
Basically the cast from timestamptz to timestamp is not immutable as there are time zones involved.
For more information see:
https://www.postgresql.org/docs/14/xfunc-volatility.html
Either:
Create a view that does the conversion.
Include it in your query as you show for the pgAdmin4 example.
Create a timestamp field on the table and either add the value to that field as part of INSERT\UPDATE or add a trigger that does that.

select for update in stored procedure (concurrently increment a field)

I want to retrieve the value of a field and increment it safely in Informix 12.1 when multiple users are connected.
What I want in C terms is lastnumber = counter++; in a concurrent environment.
The documentation mentions one way of doing this which is to make everyone connect with a wait parameter, lock the row, read the data, increment it and release the lock.
So this is what I tried:
begin work;
select
lastnum
from tbllastnums
where id = 1
for update;
And I can see that the row is locked until I commit or end my session.
However when I put this in a stored procedure:
create procedure "informix".select_for_update_test();
define vLastnum decimal(15);
begin work;
select
lastnum
into vLastnum
from tbllastnums
where id = 1
for update;
commit;
end procedure;
The database gives me a syntax error. (tried with different editors) So why is it a syntax error to write for update clause within a stored procedure? Is there an alternative to this?
Edit
Here's what I ended up with:
DROP TABLE if exists tstcounter;
^!^
CREATE TABLE tstcounter
(
id INTEGER NOT NULL,
counter INTEGER DEFAULT 0 NOT NULL
)
EXTENT SIZE 16
NEXT SIZE 16
LOCK MODE ROW;
^!^
ALTER TABLE tstcounter
ADD CONSTRAINT PRIMARY KEY (id)
CONSTRAINT tstcounter00;
^!^
insert into tstcounter values(1, 0);
^!^
select * from tstcounter;
^!^
drop function if exists tstgetlastnumber;
^!^
create function tstgetlastnumber(pId integer)
returning integer as lastCounter
define vCounter integer;
foreach curse for
select counter into vCounter from tstcounter where id = pId
update tstcounter set counter = vCounter + 1 where current of curse;
return vCounter with resume;
end foreach;
end function;
^!^
SPL and cursors 'FOR UPDATE'
If you manage to find the right bit of the manual — Updating or Deleting Rows Identified by Cursor Name under the FOREACH statement in the SPL (Stored Procedure Language) section of the Informix Guide to SQL: Syntax manual — then you'll find the magic information:
Specify a cursor name in the FOREACH statement if you intend to use the WHERE CURRENT OF cursor clause in UPDATE or DELETE statements that operate on the current row of cursor within the FOREACH loop. Although you cannot include the FOR UPDATE keywords in the SELECT ... INTO segment of the FOREACH statement, the cursor behaves like a FOR UPDATE cursor.
So, you'll need to create a FOREACH loop with a cursor name and take it from there.
Access to the manuals
Incidentally, if you go to the IBM Informix Knowledge Center and see this icon:
that is the 'show table of contents' icon and you need to press it to see the useful information for navigating to the manuals. If you see this icon:
it is the 'hide table of contents' icon, but you should be able to see the contents down the left side. It took me a while to find out this trick. And I've no idea why the contents were hidden by default for me, but I think that was a UX design mistake if other people also suffer from it.

Is it possible to have a DB uniqueness constraint across columns of two tables?

I have a mysql DB with rails, and a column "shorthand" (string) that I'd like to make unique across multiple tables. Is there a way I can do this without making a third table?
Expression
id
shorthand
...
etc
Variable
id
shorthand
...
etc
I want the values in the 'shorthand' columns of both tables to be unique between each other ie. a record shorthand value "xyz" in Expression would be rejected if a Variable with shorthand value "xyz" were to exist in the DB already.
Any thoughts appreciated, even "you have to use a third table" :)
Here an example using a third table:
-- TEMP SCHEMA for testing
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE shorthand
( shorthand varchar NOT NULL PRIMARY KEY
, one_or_two varchar NOT NULL
);
CREATE TABLE table_one
( one_id INTEGER NOT NULL PRIMARY KEY
, shorthand varchar NOT NULL REFERENCES shorthand(shorthand)
ON DELETE CASCADE ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED
, etc_one varchar
);
CREATE TABLE table_two
( two_id INTEGER NOT NULL PRIMARY KEY
, shorthand varchar NOT NULL REFERENCES shorthand(shorthand)
ON DELETE CASCADE ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED
, etc_two varchar
);
-- Trigger function for BOTH tables
CREATE FUNCTION set_one_or_two( ) RETURNS TRIGGER
AS $func$
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO shorthand (shorthand, one_or_two)
VALUES(new.shorthand, TG_TABLE_NAME)
;
ELSEIF (TG_OP = 'UPDATE') THEN
UPDATE shorthand SET shorthand = new.shorthand
WHERE shorthand = old.shorthand
;
ELSEIF (TG_OP = 'DELETE') THEN
DELETE FROM shorthand
WHERE shorthand = old.shorthand
;
END IF;
RETURN NULL;
END
$func$ LANGUAGE plpgsql
;
-- Triggers for I/U/D
CREATE CONSTRAINT TRIGGER check_one
AFTER INSERT OR UPDATE OR DELETE
ON table_one
FOR EACH ROW
EXECUTE PROCEDURE set_one_or_two ( )
;
CREATE CONSTRAINT TRIGGER check_two
AFTER INSERT OR UPDATE OR DELETE
ON table_two
FOR EACH ROW
EXECUTE PROCEDURE set_one_or_two ( )
;
-- Some tests (incomplete)
INSERT INTO table_one (one_id,shorthand,etc_one) VALUES (1, 'one' , 'one' );
INSERT INTO table_two (two_id,shorthand,etc_two) VALUES (1, 'two' , 'two' );
SELECT * FROM shorthand;
\echo this should fail
INSERT INTO table_one (one_id,shorthand,etc_one) VALUES (11, 'two' , 'eleven' );
SELECT * FROM shorthand;
UPDATE table_one SET shorthand = 'eleven' WHERE one_id = 1;
SELECT * FROM shorthand;
I think this older article does exactly what you are looking for (simulating multi table constraints):
http://classes.soe.ucsc.edu/cmps180/Winter04/constraints.html
You might also like to investigate postgres CREATE CONSTRAINT TRIGGER using a function similar to the check_nojoin() function in the article.
http://www.postgresql.org/docs/9.0/static/sql-createconstraint.html
Once you have the exact SQL you need you can put it in your rails migration with execute "the required SQL"
An alternative approach is to use a third table 'shorthands' with columns 'shorthand' and 'src'. Define shorthand as the unique primary key on that table. On each of your other two tables define 'src' as a single char field defaulting to 'A' and 'B' on each table respecitively. Add a foreign key constraint on each of your two tables consisting of both 'shorthand' and 'src' and referencing table 'shorthands'. When inserting or updating rows in either of your two tables you need to ensure the 'shorthands' table is updated either explicity as part of your transaction or via a trigger and set both 'shorthand', and 'src' to the respective table ie 'A' or 'B'.
What the foreign key constraints do is ensure that the shorthand value exists in the shorthand's table for the respective src table but because of the uniqueness constraint on just the 'shorthand' column in the shorthand's table if the other table has already defined the shorthand value a key violation will occur thus guaranteeing uniqueness across two (or even more) tables.
Whatever you do, it is best to put the referential integrity into the database, not in orm/active record validations.

Informix trigger to change inserted values

I would like to change a couple of column values before they get inserted.
I am using Informix as database.
I have a table consisting of 3 columns: Name (NVARCHAR), Type (INT), Plan (NVARCHAR).
Every time a new record is inserted, I would like to check the Name value before inserting it. If the Name starts with an F, I would like to set the Type value to 1 and the Plan Name to "Test"
In short, what I want the trigger to do is:
For every new insertion, first check if Name value starts with F.
If yes, set the Type and Plan to 1 and "Test" then insert.
If no, insert the values as-is.
I have looked up the CREATE TRIGGER statement with BEFORE and AFTER. However, I would like to have a clearer example. My case would probably involve BEFORE though.
The answer of #user3243781 get close, but did not work because it returns the error:
-747 Table or column matches object referenced in triggering statement.
This error is returned when a triggered SQL statement acts on the
triggering table, or when both statements are updates, and the column
that is updated in the triggered action is the same as the column that
the triggering statement updates.
So the alternative is handle with the NEW variable directly.
For that you need to use a procedure with the triggers reference resource, which means the procedure will able to act like the trigger by self.
Below is my example which I run with dbaccess over a Informix v11.70.
This resource is available only for versions +11 of the engine, as far I remember.
create table teste ( Name NVARCHAR(100), Type INT , Plan NVARCHAR(100) );
Table created.
create procedure check_name_values()
referencing new as n for teste ;;
define check_type integer ;;
define check_plan NVARCHAR ;;
if upper(n.name) like 'F%' then
let n.type = 1;;
let n.plan = "Test";;
end if
end procedure ;
Routine created.
;
create trigger trg_tablename_ins
insert on teste
referencing new as new
for each row
(
execute procedure check_name_values() with trigger references
);
Trigger created.
insert into teste values ('cesar',99,'myplan');
1 row(s) inserted.
insert into teste (name) values ('fernando');
1 row(s) inserted.
insert into teste values ('Fernando',100,'your plan');
1 row(s) inserted.
select * from teste ;
name cesar
type 99
plan myplan
name fernando
type 1
plan Test
name Fernando
type 1
plan Test
3 row(s) retrieved.
drop table if exists teste;
Table dropped.
drop procedure if exists check_name_values;
Routine dropped.
create trigger trg_tablename_ins
insert on tablename
referencing new as new
for each row
(
execute procedure check_name_values
(
new.name,
new.type,
new.plan
)
);
create procedure check_name_values
(
name NVARCHAR,
new_type integer,
new_plan NVARCHAR,
)
define check_type integer ;
define check_plan NVARCHAR ;
let check_type = 1;
let check_plan = "Test";
if name = 'F%'
then
insert into tablename (name,type,plan) values (name,check_type,check_plan);
else
insert into tablename (name,type,plan) values (name,new_type,new_plan);
end if ;
end procedure ;
Here is my version an adaptation of an old example I found in the informix usenet group.
It is possible to update columns in a trigger statement but not very straight forward. You have to use stored procedures an the into statement with the execute procedure command.
It worked here for IBM Informix Dynamic Server Version 12.10.FC11WE.
drop table if exists my_table;
drop sequence if exists my_table_seq;
create table my_table (
id INTEGER
NOT NULL,
col_a char(32)
NOT NULL,
col_b char(20)
NOT NULL,
hinweis char(64),
uslu char(12)
DEFAULT USER
NOT NULL,
dtlu DATETIME YEAR TO SECOND
DEFAULT CURRENT YEAR TO SECOND
NOT NULL
)
;
create sequence my_table_seq
increment 1
start 1;
drop procedure if exists get_user_datetime();
create function get_user_datetime() returning char(12),datetime year to second;
return user, current year to second;
end function
;
drop trigger if exists ti_my_table;
create trigger ti_my_table insert on my_table referencing new as n for each row (
execute function get_user_datetime() into uslu, dtlu
)
;
drop trigger if exists tu_my_table;
create trigger tu_my_table update on my_table referencing new as n for each row (
execute function get_user_datetime() into uslu, dtlu
)
;
insert into my_table values (my_table_seq.nextval, "a", "b", null, "witz", mdy(1,1,1900)) ;
SELECT *
FROM my_table
WHERE 1=1
;

Resources