Writing a SQLite database trigger to hide specific data from SELECT requests - sqlite

Is it possible to write a SQLite trigger and/or stored procedure that would return alternate data in a SELECT request if the data is not recent? I know how this could be done when selecting a specific record, because then the record ID could be passed to a stored procedure for evaluation. But I want a SELECT * FROM TABLE statement to obscure only relevant data, regardless of the conditions specified in a subsequent WHERE clause. Each individual record must be evaluated. This could be accomplished by modifying the existing data or by simply returning a false value. For instance, something like this pseudocode:
for (RECORD in ALL_RECORDS_REQUESTED) {
// test if field contains value of interest and is more than one hour old
if ( ( fieldOfInterest LIKE '%VALUE_I_AM_CENSORING%' ) && ( recordTimestamp > ( recordTimestamp + 60 ) ) ) {
// change value of field before returning (or simply return false value)
fieldOfInterest = 'value I want you to see';
}
}
Suffice is to say that I do not want a trigger to modify any data when it is initially INSERTED or UPDATED in the table, as I want the original value to be available for at least one hour. After that I want it to be changed if any SELECT statements request the record(s).
Is this possible in SQLite or would this require modifying the application that interacts with the database? I am aware of how to code this within the application but would prefer to handle this within the database itself.
This could also be accomplished by a trigger that automatically appends a condition to every SQL statement, allowing the exclusion of data:
WHERE NOT EXISTS (SELECT fieldOfInterest, recordTimestamp
FROM tblOfInterest
WHERE fieldOfInterest LIKE '%VALUE_I_AM_CENSORING%'
AND recordTimestamp > recordTimestamp + 60)
Or something to this effect....

Triggers only fire whenever a DELETE, INSERT, or UPDATE of a particular database table occurs, or whenever an UPDATE occurs on on one or more specified columns of a table.
You could use a VIEW to accomplish what you want, but then all data access would have to go through that view.

Related

SQLite: How to exectue statements based on other conditions?

A video game exists with 2 stages: Set-Up, and Gameplay. Players can use Set-Up, to alter a database, which affects Gameplay. However some features of Set-Up must account for other features of Set-Up when interacting with the database, and must therefore alter statements of execution depending on other factors in the database.
For example:
In Set-Up, a player can either choose to enable or disable a setting called "complex commerce", which creates new rows in table "Buildings" with the IDs "BUILDING_WAYSTATION", and "BUILDING_MINT", among many other changes in the database. A player can choose to enable a second setting called "easier commerce" which updates the table "ModiferArguments", increasing the value in the column "VALUE" for the column ID associated through many key constraints to "BUILDING_MARKET" (which will always exist, no matter what) in table "Buildings". However, if "complex commerce" is enabled, then we will want to change update the column "VALUE" differently for market, as well as other buildings introduced by "complex commerce" setting inserts.
Is it possible to perform this kind of logical interaction within an SQLite statement?
Code attempt:
IF
(
EXISTS
(
SELECT
*
FROM
Buildings
WHERE
BuildingType = 'BUILDING_WAYSTATION'
;
)
)
THEN
(
Update
ModifierArguments
SET
Value = Value+1
WHERE
ModifierID = 'MARKET_TRADE_ROUTE_CAPACITY'
;
Update
ModifierArguments
SET
Value = Value+2
WHERE
ModifierID = 'MINT_TRADE_ROUTE_CAPACITY'
;
Update
ModifierArguments
SET
Value = Value+3
WHERE
ModifierID = 'WAYSTATION_TRADE_ROUTE_CAPACITY'
;
)
ELSE
(
Update
ModifierArguments
SET
Value = Value+2
WHERE
ModifierID = 'MARKET_TRADE_ROUTE_CAPACITY'
)
;
I am very sorry about the eye-sore formatting, but I wanted to make sure that the logic of what I am trying to do is as clear as possible.
Code theory:
Using IF and EXISTS we can see in the database whether or not the "complex commerce" set of database updates have occurred, simply by seeing if there is a "Building" table entry row with the Id of "BUILDING_WAYSTATION". This allows to choose which set of UPDATEs to execute for the setting of "easier commerce". If "complex commerce" has not been enabled, then we only need to update a single value to "2". However, if it has been enabled, then that single value must instead be updated to "1" AND we must update other values that would otherwise not exist.
Alternatively:
I have looked at using CASE, but I am not sure if it is capable of fulfilling the same purpose, simply just substituting IF for CASE WHEN. I have also looked at using iif(), but have the same issue in that it does not seem fit for purpose outside of replacing return data from SELECT statements upon display.
You can use CASE expressions to apply your logic:
UPDATE ModifierArguments
SET Value = Value +
CASE
WHEN EXISTS (SELECT * FROM Buildings WHERE BuildingType = 'BUILDING_WAYSTATION')
THEN CASE ModifierID
WHEN 'MARKET_TRADE_ROUTE_CAPACITY' THEN 1
WHEN 'MINT_TRADE_ROUTE_CAPACITY' THEN 2
WHEN 'WAYSTATION_TRADE_ROUTE_CAPACITY' THEN 3
END
ELSE CASE WHEN ModifierID = 'MARKET_TRADE_ROUTE_CAPACITY' THEN 2 ELSE 0 END
END
WHERE ModifierID IN ('MARKET_TRADE_ROUTE_CAPACITY', 'MINT_TRADE_ROUTE_CAPACITY', 'WAYSTATION_TRADE_ROUTE_CAPACITY');

Need to get data from a table using database link where database name is dynamic

I am working on a system where I need to create a view.I have two databases
1.CDR_DB
2.EMS_DB
I want to create the view on the EMS_DB using table from CDR_DB. This I am trying to do via dblink.
The dblink is created at the runtime, i.e. DB Name is decided at the time user installs the database, based on the dbname dblink is decided.
My issue is I am trying to create a query like below to create a view from a table which name is decided at run time. Please see below query :
select count(*)
from (SELECT CONCAT('cdr_log#', alias) db_name
FROM ems_dbs a,
cdr_manager b
WHERE a.db_type = 'CDR'
and a.ems_db_id = b.cdr_db_id
and b.op_state = 4 ) db_name;
In this query cdr_log#"db_name" is the runtime table name(db_name get's created at runtime).
When I'm trying to run above query, I'm not getting the desired result. The result of the above query is '1'.
When running only the sub-query from the above query :
SELECT CONCAT('cdr_log#', alias) db_name
FROM ems_dbs a,
cdr_manager b
WHERE a.db_type = 'CDR'
and a.ems_db_id = b.cdr_db_id
and b.op_state = 4;
i'm getting the desired result, i.e. cdr_log#cdrdb01
but when i'm trying to run the full query, getting result as '1'.
Also, when i'm trying to run as
select count(*) from cdr_log#cdrdb01;
I'm getting the result as '24' which is correct.
Expected Result is that I should get the same output similar to the query :
select count(*) from cdr_log#cdrdb01;
---24
But the desired result is coming as '1' using the full query mentioned initially.
Please let me know a way to solve the above problem. I found a way to do it via a procedure, but i'm not sure how can I invoke this procedure.
Can this be done as part of sub query as I have used above?
You're not going to be able to create a view that will dynamically reference an object over a database link unless you do something like create a pipelined table function that builds the SQL dynamically.
If the database link is created and named dynamically at installation time, it would probably make the most sense to create any objects that depend on the database link (such as the view) at installation time too. Dynamic SQL tends to be much harder to write, maintain, and debug than static SQL so it would make sense to minimize the amount of dynamic SQL you need. If you can dynamically create the view at installation time, that's likely the easiest option. Even better than directly referencing the remote object in the view, particularly if there are multiple objects that need to reference the remote object, would probably be to have the view reference a synonym and create the synonym at install time. Something like
create synonym cdr_log_remote
for cdr#<<dblink name>>
create or replace view view_name
as
select *
from cdr_log_remote;
If you don't want to create the synonym/ view at installation time, you'd need to use dynamic SQL to reference the remote object. You can't use dynamic SQL as the SELECT statement in a view so you'd need to do something like have a view reference a pipelined table function that invokes dynamic SQL to call the remote object. That's a fair amount of work but it would look something like this
-- Define an object that has the same set of columns as the remote object
create type typ_cdr_log as object (
col1 number,
col2 varchar2(100)
);
create type tbl_cdr_log as table of typ_cdr_log;
create or replace function getAllCDRLog
return tbl_cdr_log
pipelined
is
l_rows typ_cdr_log;
l_sql varchar(1000);
l_dblink_name varchar(100);
begin
SELECT alias db_name
INTO l_dblink_name
FROM ems_dbs a,
cdr_manager b
WHERE a.db_type = 'CDR'
and a.ems_db_id = b.cdr_db_id
and b.op_state = 4;
l_sql := 'SELECT col1, col2 FROM cdr_log#' || l_dblink_name;
execute immediate l_sql
bulk collect into l_rows;
for i in 1 .. l_rows.count
loop
pipe row( l_rows(i) );
end loop;
return;
end;
create or replace view view_name
as
select *
from table( getAllCDRLog );
Note that this will not be a particularly efficient way to structure things if there are a large number of rows in the remote table since it reads all the rows into memory before starting to return them back to the caller. There are plenty of ways to make the pipelined table function more efficient but they'll tend to make the code more complicated.

Alternative to using subquery inside CHECK constraint?

I am trying to build a simple hotel room check-in database as a learning exercise.
CREATE TABLE HotelReservations
(
roomNum INTEGER NOT NULL,
arrival DATE NOT NULL,
departure DATE NOT NULL,
guestName CHAR(30) NOT NULL,
CONSTRAINT timeTraveler CHECK (arrival < departure) /* stops time travelers*/
/* CONSTRAINT multipleReservations CHECK (my question is about this) */
PRIMARY KEY (roomNum, arrival)
);
I am having trouble specifying a constraint that doesn't allow inserting a new reservation for a room that has not yet been vacated. For example (below), guest 'B' checks into room 123 before 'A' checks out.
INSERT INTO HotelStays(roomNum, arrival, departure, guestName)
VALUES
(123, date("2017-02-02"), date("2017-02-06"), 'A'),
(123, date("2017-02-04"), date("2017-02-08"), 'B');
This shouldn't be allowed but I am unsure how to write this constraint. My first attempt was to write a subquery in check, but I had trouble figuring out the proper subquery because I don't know how to access the 'roomNum' value of a new insert to perform the subquery with. I then also figured out that most SQL systems don't even allow subquerying inside of check.
So how am I supposed to write this constraint? I read some about triggers which seem like it might solve this problem, but is that really the only way to do it? Or am I just dense and missing an obvious way to write the constraint?
The documentation indeed says:
The expression of a CHECK constraint may not contain a subquery.
While it would be possible to create a user-defined function that goes back to the database and queries the table, the only reasonable way to implement this constraint is with a trigger.
There is a special mechanism to access the new row inside the trigger:
Both the WHEN clause and the trigger actions may access elements of the row being inserted, deleted or updated using references of the form "NEW.column-name" and "OLD.column-name", where column-name is the name of a column from the table that the trigger is associated with.
CREATE TRIGGER multiple_reservations_check
BEFORE INSERT ON HotelReservations
BEGIN
SELECT RAISE(FAIL, "reservations overlap")
FROM HotelReservations
WHERE roomNum = NEW.roomNum
AND departure > NEW.arrival
AND arrival < NEW.departure;
END;

How to assign an ID but then delete if not used

I am unsure on how to do this 'best practice' wise.
I have a web application (asp.net VB) that connects to an MS SQL server 2012. Currently when the page loads the app connects to a DB table and gets the last ID and adds 1 to it and displays this to the user. When the user submits the form the new ID is saved to the DB.
The problem being the app may be opened by 2 users at the same time and therefore they will get assigned the same ref number which will cause problems when the data is saved.
How can I assign different numbers to different users if the app is opened at the same time without saving unnecessary data?
You have multiple solutions for this, I'll try to outline a few approaches. (I'll assume that you need to insert things into a DB that I'll call "orders".)
First of all, you can move the ID-generation to the moment when the order is actually inserted, and not at the moment when the user start to enter the data. That way, you do not generate an ID for a user that never completes the form. Also this scenario is easy to accomplish using autoincrementing values in sql server. You can, for example do:
-- create a table with an identity column
create table Orders (
ID int identity(1,1) not null,
Description nvarchar(max) not null
);
-- insert values, without specifying the ID column
insert into Orders (Description) values ()
-- select the row back
-- returns 1, 'My First Order'
select * from Orders;
Another way to do this is to use SQL Server Sequences. These are things that do nothing except generate numbers in a row. They guarantee that the numbers won't be repeated, and always keep count of the current value, i.e.
-- create a sequence
create sequence OrderIdSequence
start with 1
increment by 1;
-- get the next sequence value
select next value for OrderIdSequence

How do you write a good stored procedure for update?

I want to write a stored procedure (SQL server 2008r2), say I have a table:
person
Columns:
Id int (pk)
Date_of_birth date not null
Phone int allow null
Address int allow null
Name nvarchat(50) not null
Sample data:
Id=1,Date_of_birth=01/01/1987,phone=88888888,address=null,name='Steve'
Update statement in Stored procedure, assume
The parameters are already declare:
Update person set
Date_of_birth=#dob,phone=#phone,address=#address,name=#name where id=#id
The table has a trigger to log any changes.
Now I have an asp.net update page for updating the above person table
The question is, if user just want to update address='apple street' , the above update statement will update all the fields but not check if the original value = new value, then ignore this field and then check the next field. So my log table will log all the event even the columns are not going to be updated.
At this point, my solutions
Select all the value by id and store them into local variables.
Using if-else check and generate the update statement. At last,
dynamically run the generated SQL (sp_executesql)
Select all the value by id and store them into local variables.
Using if-else check and update each field seperately:
If #dob <> #ori_dob
Begin
Update person set date_of_birth=#dob where id=#id
End
May be this is a stupid question but please advice me if you have better idea, thanks!
This is an answer to a comment by the OP and does not address the original question. It would, however, be a rather ugly comment.
You can use a statement like this to find the changes to Address within an UPDATE trigger:
select i.Id, d.Address as OldAddress, i.Address as NewAddress
from inserted as i inner join
deleted as d on d.Id = i.Id
where d.Address <> i.Address
One such statement would be needed for each column that you want to log.
You could accumulate the results of the SELECTs into a single table variable, then summarize the results for each Id. Or you can use INSERT/SELECT to save the results directly to your log table.

Resources