Force Explicit Setting of Column Value with SQLite Trigger - database

Is it possible to declare a trigger in SQLite3 which forces the user to explictly provide a value within an UPDATE statement?
Lets assume we have an Article table:
CREATE TABLE Article (
Id INTEGER PRIMARY KEY,
Title TEXT NOT NULL UNIQUE,
Content TEXT,
UserInserted TEXT NOT NULL,
UserUpdated TEXT
);
I can declare the following trigger which prohibits empty values for the column UserUpdated:
CREATE TRIGGER IF NOT EXISTS Trig_Article_BEFORE_UPDATE
BEFORE UPDATE OF Title, Content ON Article
BEGIN
SELECT
CASE
WHEN new.UserUpdated IS NULL THEN RAISE(ABORT, 'UserUpdated must not be NULL.')
WHEN length(new.UserUpdated) = 0 THEN RAISE(ABORT, 'UserUpdated must not be NULL.')
END;
END;
Insertion works as expected:
INSERT INTO Article(Title, Content, UserInserted) VALUES('Foo', '', '<user_A>');
Updating without providing a UserUpdated in the first place works as well:
UPDATE Article SET Content = 'Bar' WHERE Id = 1;
-- Error: UserUpdated must not be NULL.
UPDATE Article SET Content = 'Bar', UserUpdated = '' WHERE Id = 1;
-- Error: UserUpdated must not be NULL.
But once a UserUpdated has been set it is no longer required to provide the column explicitly.
UPDATE Article SET Content = 'Bar', UserUpdated = '<user_B>' WHERE Id = 1;
UPDATE Article SET Content = 'Foo Bar' WHERE Id = 1;
-- No error
Is there a way to declare a trigger so that the last statement throws an error as well?
Update 22.11.2019
Thanks to C Perkins' answer I came up with a solution using an extra column.
An extra column CurrentUser is added to Article:
CREATE TABLE Article (
-- ...
CurrentUser TEXT
);
A BEFORE UPDATE trigger ensures that this column is set:
CREATE TRIGGER IF NOT EXISTS Trig_Article_BEFORE_UPDATE
BEFORE UPDATE ON Article
WHEN old.Title <> new.Title OR
old.Content <> new.Content OR
old.CurrentUser <> new.CurrentUser
BEGIN
SELECT
CASE
WHEN new.CurrentUser IS NULL THEN RAISE(ABORT, 'CurrentUser must not be NULL.')
WHEN length(new.CurrentUser) = 0 THEN RAISE(ABORT, 'CurrentUser must not be NULL.')
END;
END;
An AFTER UPDATE Trigger (if CurrentUser is not null) copies the value from CurrentUser to UserUpdated and clears CurrentUser again.
CREATE TRIGGER IF NOT EXISTS Trig_Article_AFTER_UPDATE
AFTER UPDATE ON Article
WHEN new.CurrentUser IS NOT NULL
BEGIN
UPDATE Article SET UserUpdated = new.CurrentUser, CurrentUser = NULL WHERE Id = new.Id;
END;
To prevent direct updates of UserUpdated another trigger is used:
CREATE TRIGGER IF NOT EXISTS Trig_Article_UserUpdated_BEFORE_UPDATE
BEFORE UPDATE ON Article
WHEN old.UserUpdated <> new.UserUpdated AND
old.CurrentUser IS NULL
BEGIN
SELECT RAISE(ABORT, 'You must not UPDATE UserUpdated.');
END;
After all I get the desired behaviour. Every time Content or Title are updated the Column CurrentUser has to be provided explicitly within the update statemant and UserUpdated reflects the last user who updated the values.

Utilize another update-only column. Here's a quick outline of what would be involved:
Add an "update-only" column to the table: UserUpdateONLY
On the BEFORE UPDATE trigger, require that New.UserUpdateONLY IS NOT NULL AND length(New.UpdateUpdateONLY) != 0 and that New.UserUpdated == Old.UserUpdated OR (New.UserUpdated IS NULL AND Old.UserUpdated IS NULL) to avoid having contradiction of both columns with updated data (raise error if either condition is false).
On the INSTEAD OF UPDATE trigger, copy the value from the update-only column to the normal storage column: SET UserUpdateONLY = NULL, UserUpdated = NEW.UserUpdateONLY
The only possible problem is if NULL updates are allowed on the normal column, because then the obvious "trigger" value will not work in that case. If it might be a problem, instead store an unlikely value as the default for the UserUpdateONLY column, something like '<NOT VALID>' so that a new valid will always be detected.

Related

How to create a trigger that switches values of a composite type column

I need to create a trigger that, when i update the value of a field on a column of a table it automatically puts the old value into another field of another composite type column
I tried this:
create or replace function valores_progresso_cliente()
returns trigger as $$
begin
update progresso_cliente set medida_antiga.med_antebraco = (medida_atual).med_antebraco, medida_atual.med_antebraco = (new.med_antebraco(medida_atual)) where rg_progresso_cliente = rg_progresso_cliente;
return new;
end;
$$ language plpgsql;
I need to pass the rg_progresso_cliente through the trigger each time i run the update
update progresso_cliente set medida_atual.med_antebraco = 8.00 where rg_progresso_cliente = '368242365';
but i cant figure out how
This is how my table progresso_cliente is:
create table progresso_cliente(rg_progresso_cliente primary key
medida_atual medidas,
medida_antiga medidas);
what i want to do is basically create a trigger that each time i run an update for example updating one of the fields from medida_atual its previously field get into medida_antiga field
I wouldn't use composite types as table columns if I were you.
Anyway, the way to do this is a BEFORE trigger that does not UPDATE, but assign new values to NEW, something like
DECLARE
swap_var double precision;
BEGIN
swap_var := (NEW.medida_atual).med_antebraco;
(NEW.medida_atual).med_antebraco := (NEW.medida_atual).medida_atual;
(NEW.medida_atual).medida_atual := swap_var;
RETURN NEW;
END;

Error update trigger after new row has inserted into same table

I want to update OrigOrderNbr and OrigOrderType (QT type) because when I create first both of column are Null value. But after S2 was created (QT converted to S2) the OrigOrderType and OrigOrderNbr (S2) take from QT reference. Instead of that, I want to update it to QT also.
http://i.stack.imgur.com/6ipFa.png
http://i.stack.imgur.com/E6qzT.png
CREATE TRIGGER tgg_SOOrder
ON dbo.SOOrder
FOR INSERT
AS
DECLARE #tOrigOrderType char(2),
#tOrigOrderNbr nvarchar(15)
SELECT #tOrigOrderType = i.OrderType,
#tOrigOrderNbr = i.OrderNbr
FROM inserted i
UPDATE dbo.SOOrder
SET OrigOrderType = #tOrigOrderType,
OrigOrderNbr = #tOrigOrderNbr
FROM inserted i
WHERE dbo.SOOrder.CompanyID='2'
and dbo.SOOrder.OrderType=i.OrigOrderType
and dbo.SOOrder.OrderNbr=i.OrigOrderNbr
GO
After I run that trigger, it showed the message 'Error #91: Another process has updated 'SOOrder' record. Your changes will be lost.'.
Per long string of comments, including some excellent suggestions in regards to proper trigger writing techniques by #marc_s and #Damien_The_Unbeliever, as well as my better understanding of your issue at this point, here's the re-worked trigger:
CREATE TRIGGER tgg_SOOrder
ON dbo.SOOrder
FOR INSERT
AS
--Update QT record with S2 record's order info
UPDATE SOOrder
SET OrigOrderType = 'S2'
, OrigOrderNbr = i.OrderNbr
FROM SOOrder dest
JOIN inserted i
ON dest.OrderNbr = i.OrigOrderNbr
WHERE dest.OrderType = 'QT'
AND i.OrderType = 'S2'
AND dest.CompanyID = 2 --Business logic constraint
AND dest.OrigOrderNbr IS NULL
AND dest.OrigOrderType IS NULL
Basically, the idea is to update any record of type "QT" once a matching record of type "S2" is created. Matching here means that OrigOrderNbr of S2 record is the same as OrderNbr of QT record. I kept your business logic constraint in regards to CompanyID being set to 2. Additionally, we only care to modify QT records that have OrigOrderNbr and OrigOrderType set to NULL.
This trigger does not rely on a single-row insert; it will work regardless of the number of rows inserted - which is far less likely to break down the line.

Optionally saving selectively data in one-go only when you want it

I have a table where data does not initially exist until an action is taken that stores all settings made by client in one-go. To illustrate this simply, a button click that stores all column values off a (HTML) table into a database table (let's call this dbo.Settings).
So instead of inserting into this dbo.Settings all the default values prior to user making any changes to their individual settings (ever), I kind of created the pseudo data for them that will be returned whenever requested, kind of like SELECT-ing the default values:
SELECT
CanView = ISNULL(CanView, 1),
CanRead = ISNULL(CanRead, 1),
CanWrite = ISNULL(CanWrite, 0)
FROM
dbo.Settings AS s
WHERE
UserId = #id
Rather than doing:
IF NOT EXISTS(SELECT * FROM dbo.Settings WHERE UserId = #id)
BEGIN
INSERT INTO dbo.Settings (UserId, CanView, CanRead, CanWrite)
VALUES (#id, 1, 1, 0)
END
The problem with this is whenever I need to add a new setting column in the future, I now have to note one more procedure to modify/add the default value for this column as well -- which I don't like. Using TRIGGER would be an option but I wonder what the best practice in managing data like this would be. Or would you do something like this:
CREATE PROC Settings_CreateOrModify
#userId INT,
#canView BIT = NULL,
#canRead BIT = NULL,
#canWrite BIT = NULL
AS
BEGIN
IF EXISTS(SELECT * FROM dbo.Settings WHERE UserId = #userId) BEGIN
UPDATE s
SET
CanView = #canView,
CanRead = #canRead,
CanWrite = #canWrite
FROM
dbo.Settings AS s
WHERE
s.UserId = #userId AND
(#canView IS NULL OR #canView <> s.CanView) AND
(#canRead IS NULL OR #canRead <> s.CanRead) AND
(#canWrite IS NULL OR #canWrite <> s.CanWrite)
END
ELSE BEGIN
INSERT INTO
dbo.Settings(UserId, CanView, CanRead, CanWrite)
SELECT
#userId, #canView, #canRead, #canWrite
END
END
How would you handle data structure like this? Any recommendation or correction would be greatly appreciated. Thanks in advance!
Your SP is a good way to go, and doing it like this is commonly called an "UPSERT".
It also looks to me as if the block:
(#canView IS NULL OR #canView <> s.CanView) AND
(#canRead IS NULL OR #canRead <> s.CanRead) AND
(#canWrite IS NULL OR #canWrite <> s.CanWrite)
is problematic since it causes the UPDATE to run only if ALL parameters changed their value. I don't think that's what you wanted to say. Just SET the three values regardless of what's already there.
You still end up with three places to change when you add a new setting: The Table, the Upsert and the Defaults.
One very different approach is this:
Apply the defaults to the columns in the table definition.
Whenever you need the values for a new user, do: INSERT INTO dbo.Settings(UserId) The defaults will fill the rest of the columns.
Now you can retrieve the values for ALL users (new or not) in the same way from the table.
Since you already inserted the user in step 2, you know the userid is there already and you can always save the changes via a simple update.
This eliminates the SP and the need of providing the defaults in one extra place.

Oracle trigger implementation

I have to implement a trigger which would:
7) Show the DDL for how a trigger can be used to transfer all rental copies from a store being deleted from the Store information table to the central store
8) Show how this trigger can be extended to make
sure that the central store is never deleted from the database
So far I have done this:
CREATE OR REPLACE TRIGGER stores BEFORE DELETE ON stores FOR
EACH ROW BEGIN IF DELETING WHERE cvr = 123456789 THEN
Raise_Application_Error ( num => -20050, msg => 'You can
not delete Main Store.'); END IF; IF DELETING THEN
UPDATE store_id=123456789 ON movies WHERE isActive = 0 END
IF; END;
so main store is with cvr which is written, but it gives me a compilation error. Any help? Thanks in advance.
You have two errors in your code.
there is no "DELETING WHERE" expression, you have to use two boolean exceptions like this:
IF DELETING AND :old.cvr = 123456789 THEN...
:old.cvr refers to value of cvr column in deleted record
Syntax of UPDATE clause is
UPDATE table_name
SET column_name1 = value1,
column_name1 = value2
WHERE where_clause;
in your case probably somethink like this:
UPDATE movies
set store_id = 123456789
WHERE store_id = :old.cvr
I guess, I don't know required functionality

Why triggers try to insert NULL value when using a field from 'inserted' table?

I have to sync changes done in MSSQL with a remote MySQL database. The changes to be synced are adding invoices and users to the system. The remote server is not expected to be always reachable so I'm trying to set up a kind of log table for storing changes done in MSSQL.
Here is a fully working trigger for that:
CREATE TRIGGER [dbo].[dokument_insert]
ON [dbo].[dokument]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [bcg_ekodu].[dbo].[sync_stack] (event,sql, table_name, import_priority)
SELECT
'INSERT',
'INSERT INTO bills SET
date = "'+CONVERT(VARCHAR(19),dok_kuup,120)+'",
total = "'+CAST(kokkusum AS nvarchar)+'",
number = "'+RTRIM(dok_nr)+'",
created = "'+CONVERT(VARCHAR(19),savetime,120)+'",
rounded = "'+CAST(ymardus AS nvarchar)+'",
currency = "'+CAST(valuuta AS nvarchar)+'",
due_date = "'+CONVERT(VARCHAR(19),tasupaev,120)+'",
pk_joosep = "'+CAST(dok_kood AS nvarchar)+'",
joosep_hankija = "'+CAST(hankija AS nvarchar)+'";
UPDATE
bills, users, companies
SET
bills.user_id = users.id,
bills.imported = NOW()
WHERE
bills.imported IS NULL
AND companies.id = users.company_id
AND companies.pk_joosep = 10
AND bills.user_id = users.pk_joosep',
'bills',
'200'
FROM inserted
END
It inserts a row into 'sync_stack' table every time a row is inserted to 'dokument' table. The 'sql' column will contain an SQL to create the same kind of row in another (MySQL) database.
But this trigger is not working:
CREATE TRIGGER [dbo].[klient_insert]
ON [dbo].[klient]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [bcg_ekodu].[dbo].[sync_stack] (event,sql, table_name, import_priority)
SELECT
'INSERT',
'INSERT INTO users SET
username =10'+CAST(kl_kood as nvarchar)+',
password = NULL,
name ="'+LTRIM(RTRIM(kl_nimi))+'",
email ="'+CAST(LTRIM(RTRIM(kl_email)) as nvarchar)+'",
reference_no ="'+CAST(LTRIM(RTRIM(kl_viide)) as nvarchar)+'",
phone ="'+CAST(LTRIM(RTRIM(kl_tel1)) as nvarchar)+'",
logins ="'+CAST(0 as nvarchar)+'",
last_login = NULL,
created ="'+CONVERT(VARCHAR(19),savetime,120)+'",
updated = NULL,
deleted ="0",
address ="'+CAST(LTRIM(RTRIM(kl_aadr1)) as nvarchar)+'",
pk_joosep ="'+CAST(kl_kood as nvarchar)+'"',
'users',
'210'
FROM inserted
END
While the execution of the above SQL to create that trigger completes just fine, when I try to insert some rows to the 'triggered' table, I get the following error:
No row was updated.
The data in row 175 was not committed.
Error Source: .Net SqlClient Data Provider.
Error Message: Cannot insert the value NULL into column 'sql', table 'mydb.dbo.sync_stack'; column does not allow nulls. INSERT fails.
The statement has been terminated.
Correct the errors and retry or press ESC to cancel the change(s).
If I delete this trigger, this error does not occur.
If I insert just plain text for 'sql' column, it works as expected.
If I use any field from the inserted row, even just a text field, it fails again.
If I allow NULL values in 'sql' column, inserting rows succeeds but I get a NULL value in 'sql' column.
How to make the second trigger work as expected, too?
I suspect that at least one of the values from inserted that you are concatenating into your SQL statement is NULL. You can circumvent this by using COALESCE, e.g.
username =10'+COALESCE(CAST(kl_kood as nvarchar), '')+',
Of course you shouldn't be declaring nvarchar without specifying a length, right?
Bad habits to kick : declaring VARCHAR without (length)
Concatenating any value to NULL is NULL:
select 'test' + NULL
Results in null, you should use something like that for your columns:
select isnull(column, '')
This would result in an empty string.

Resources