Is it possible to have a DB uniqueness constraint across columns of two tables? - ruby-on-rails

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.

Related

Postgres: Check if two boxes overlap

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

How to execute an Informix ISQL query with a join

I have a table that already has values in it. The value I want to update is g_fuel_prft.billed_qty. I need to multiple a number from this table times a number from another table to get the value.
So table names are inv_header which contains inv_header.rpt_factor and g_fuel_prft which contains g_fuel_prft.billed_qty. The criteria is where inv_header.link=g_fuel_prft.lnk AND inv_header.rpt_factor = 0.
once I have those rows selected I want to update them to billed_qty * rpt_factor
update g_fuel_prft
set billed_qty = (inv_header.rpt_factor * g_fuel_prft.billed_qty)
where exists
(select billed_qty,ivh_rpt_factor from g_fuel_prft,inv_header
where g_fuel_prft.prodlnk = inv_header.ivh_link
and inv_header.ivh_rpt_factor = 0)
I am getting an 201 syntax error
You can't refer to inv_header.rpt_factor like that. You'll need to place it into a sub-query.
UPDATE g_fuel_prft
SET billed_qty = ((SELECT inv_header.rpt_factor
FROM inv_header
WHERE g_fuel_prft.prodlnk = inv_header.ivh_link) *
g_fuel_prft.billed_qty)
WHERE EXISTS (SELECT *
FROM g_fuel_prft
JOIN inv_header ON g_fuel_prft.prodlnk = inv_header.ivh_link
WHERE inv_header.ivh_rpt_factor = 0)
With an EXISTS query, the select-list doesn't matter and * is conventional. You might need more restrictions in the sub-select within the SET clause. It depends on what sort of relationship there is between the joining columns (1:1, 1:N, N:1, M:N).
Given the minimal schema below, the query above is syntactically valid. It's hard to test it without valid sample data.
DROP TABLE IF EXISTS g_fuel_prft;
CREATE TABLE g_fuel_prft
(
prodlnk INTEGER NOT NULL,
billed_qty DECIMAL(8,2) NOT NULL
);
DROP TABLE IF EXISTS inv_header;
CREATE TABLE inv_header
(
rpt_factor DECIMAL(8,4) NOT NULL,
ivh_link INTEGER NOT NULL,
ivh_rpt_factor DECIMAL(8,4) NOT NULL
);

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
;

optimize query for last-auto-inc value

Sybase Advantage Database
I am doing a query
INSERT INTO nametable
SELECT * FROM nametable WHERE [indexkey]=32;
UPDATE nametable Set FieldName=1
WHERE [IndexKey]=(SELECT max([indexKey]) FROM nametable);
The purpose is to copy a given record into a new record, and then update the newly created record with some new values. The "indexKey" is declared as autoinc and is the primary key to the table.
I am not sure if this can be achieved in a single statement with better speed or;;; suggestions appreciated.
It can be achieved with a single statement but it will make the code more susceptible to schema changes. Suppose that there are 2 additional columns in the table besides the FieldName and the indexKey columns. Then the following statement will achieve your objective.
INSERT INTO nametable ( FieldName, Column2, Column3 )
SELECT 1, Column2, Column3 FROM nametable WHERE [indexkey]=32
However, if the table structure changes, this statement will need to be updated accordingly.
BTW, your original implementation is not safe in multi-user scenarios. The max( [indexKey] ) in the UPDATE statement may not be the one generated by the INSERT statement. Another user could have inserted another row between the two statements. To use your original approach, you should use the LastAutoInc() scalar.
UPDATE nametable Set FieldName=1
WHERE [IndexKey] = LastAutoInc( STATEMENT )

How to convert a table column to another data type

I have a column with the type of Varchar in my Postgres database which I meant to be integers... and now I want to change them, unfortunately this doesn't seem to work using my rails migration.
change_column :table1, :columnB, :integer
Which seems to output this SQL:
ALTER TABLE table1 ALTER COLUMN columnB TYPE integer
So I tried doing this:
execute 'ALTER TABLE table1 ALTER COLUMN columnB TYPE integer USING CAST(columnB AS INTEGER)'
but cast doesn't work in this instance because some of the column are null...
any ideas?
Error:
PGError: ERROR: invalid input syntax for integer: ""
: ALTER TABLE table1 ALTER COLUMN columnB TYPE integer USING CAST(columnB AS INTEGER)
Postgres v8.3
It sounds like the problem is that you have empty strings in your table. You'll need to handle those, probably with a case statement, such as:
execute %{ALTER TABLE "table1" ALTER COLUMN columnB TYPE integer USING CAST(CASE columnB WHEN '' THEN NULL ELSE columnB END AS INTEGER)}
Update: completely rewritten based on updated question.
NULLs shouldnt be a problem here.
Tell us your postgresql version and your error message.
Besides, why are you quoting identifiers ? Be aware that unquoted identifiers are converted to lowercase (default behaviour), so there might be a problem with your "columnB" in your query - it appears quoted first, unquoted in the cast.
Update: Before converting a column to integer, you must be sure that all you values are convertible. In this case, it means that columnB should contains only digits (or null).
You can check this by something like
select columnB from table where not columnB ~ E'^[0-9]+$';
If you want your empty strings to be converted to NULL integers, then run first
UPDATE table set columnB = NULL WHERE columnB = '';

Resources