Update and log only changed rows with SQL in SQLite - sql

I am writing an application/script in R that updates a SQLite database.
My apologies - I am not experienced with this.
My table consists of 4 fields Id,Name,LVL,Notes:
CREATE TABLE members (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
Name TEXT NOT NULL,
LVL INTEGER NOT NULL,
Notes TEXT
);
INSERT INTO members (Name,LVL,Notes)
VALUES ('Jean',12,'First stage'),
('Jacques',1,'Second stage'),
('Amelie',1,'Second stage'),
('Louis',13,'Some other note altogether')
;
I want to check it against another table tmp
CREATE TABLE tmp (
Name TEXT NOT NULL,
LVL INTEGER NOT NULL,
Notes TEXT
);
INSERT INTO tmp (Name,LVL,Notes)
VALUES ('Jean',13,'First stage'),
('Jacques',1,'Second stage'),
('Amelie',1,'Third stage'),
('Louis',14,'Fourth stage')
;
and if there are changes in LVL and/or Notes fields (as LVL for Jean and Louis and Notes for Amelie and Louis) I want to update the members table with new values after I record the previous values (as whole rows) with a timestamp in member_changes table.
What would be the minimal set of queries to achieve this?
And what is the better design of the member_changes table? Would it be the same as members but with added rowID as primary key and timestamp fields? And naturally memberID would allow duplicates.
Many thanks,
Rob
SYNOPSIS of expanded answer
Thanks to #forpas kind answer I put this small system together with 2 additional triggers. New information comes in via tmp table. Member names are presumed to be unique; possibly primary key on members.Id was not needed. Nevertheless:
-- CREATE members table for current guild members
-- Id is prim key and Name has unique index
CREATE TABLE members (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
Name TEXT NOT NULL UNIQUE,
LVL INTEGER NOT NULL,
Notes TEXT
);
-- SAMPLE DATA
INSERT INTO members (Name,LVL,Notes) VALUES
('Jean',12,'First stage'),
('Jacques',1,'Second stage'),
('Amelie',1,'Second stage'),
('Louis',13,'Some other note altogether');
-- LOG table to see membership changes over time
CREATE TABLE members_changes (
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
Id INTEGER REFERENCES members(Id),
Name TEXT NOT NULL,
LVL INTEGER NOT NULL,
Notes TEXT
);
-- TABLE through which the updates will come in via rvest
-- presumed cannot contain duplicate names
CREATE TABLE tmp (
Name TEXT NOT NULL UNIQUE,
LVL INTEGER NOT NULL,
Notes TEXT
);
-- TRIGGERS (3)
-- (1) UPDATES MEMBERS if insertion in tmp shows changes
-- also LOGS this change in members_changes
CREATE TRIGGER IF NOT EXISTS tr_insert_tmp AFTER INSERT ON tmp
BEGIN
INSERT INTO members_changes(Id,Name,LVL,Notes)
SELECT Id,Name,LVL,Notes
FROM members
WHERE Name = NEW.NAME AND (LVL IS NOT NEW.LVL OR Notes IS NOT NEW.Notes);
UPDATE members
SET LVL = NEW.LVL, Notes = NEW.Notes
WHERE Name = NEW.Name AND (LVL IS NOT NEW.LVL OR Notes IS NOT NEW.Notes);
END;
-- (2) LOGS DELETIONS from members
CREATE TRIGGER IF NOT EXISTS tr_delete_members BEFORE DELETE ON members
BEGIN
INSERT INTO members_changes(Id,Name,LVL,Notes)
SELECT Id,Name,LVL,Notes || " :Deleted"
FROM members
WHERE Name = OLD.Name;
END;
-- (3) LOGS INSERTS into members (new members)
CREATE TRIGGER IF NOT EXISTS tr_insert_members AFTER INSERT ON members
BEGIN
INSERT INTO members_changes(Id,Name,LVL,Notes)
SELECT Id,Name,LVL,Notes || " :Inserted"
FROM members
WHERE Name = NEW.Name;
END;
-- this shows all defined triggers
select * from sqlite_master where type = 'trigger';
-- QUERIES to be run from the script after tmp is updated (b,c,d)
-- ADD NEW MEMBERS
-- it should mostly fail (changes are slow and few)
-- this is logged via tr_insert_members
INSERT OR IGNORE INTO members(Name,LVL,Notes) SELECT Name, LVL, Notes FROM tmp;
-- DELETE OLD MEMBERS
-- logged via tr_delete_members
DELETE FROM members WHERE Name NOT IN (SELECT Name FROM tmp);
-- EMPTY tmp at the end of the script run
DELETE FROM tmp;
When application runs the only queries that need to be called are:
a) the one which populates tmp (from dataframe gathered by rvest)
b) query to add new members from tmp
c) query to delete members not in tmp
d) query to empty tmp
This is thanks to database setup kindly suggested by #forpas. I had never used triggers and finally made some sense of them. Very helpful for logging changes.

A proper design for members_changes is this:
CREATE TABLE members_changes (
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
Id INTEGER REFERENCES members(Id),
Name TEXT NOT NULL,
LVL INTEGER NOT NULL,
Notes TEXT
);
The column timestamp's default value is the current timestamp.
You need an AFTER INSERT trigger for the table tmp, so that for every inserted row in tmp, the respective row from members will be inserted in members_changes (if any value of LVL or Notes is different) and after that the new row from tmp will update the row of members:
CREATE TRIGGER IF NOT EXISTS tr_insert_tmp AFTER INSERT ON tmp
BEGIN
INSERT INTO members_changes(Id,Name,LVL,Notes)
SELECT Id,Name,LVL,Notes
FROM members
WHERE Name = NEW.NAME AND (LVL IS NOT NEW.LVL OR Notes IS NOT NEW.Notes);
UPDATE members
SET LVL = NEW.LVL, Notes = NEW.Notes
WHERE Name = NEW.Name AND (LVL IS NOT NEW.LVL OR Notes IS NOT NEW.Notes);
END;
See the demo.

Related

How to efficiently insert ENUM value into table?

Consider the following schema:
CREATE TABLE IF NOT EXISTS snippet_types (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER NOT NULL PRIMARY KEY,
title TEXT,
content TEXT,
type INTEGER NOT NULL,
FOREIGN KEY(type) REFERENCES snippet_types(id)
);
This schema assumes a one-to-many relationship between tables and allows efficiently maintaining a set of ENUMs in the snippet_types table. Efficiency comes from the fact that we don't need to store the whole string describing snippet type in the snippets table, but this decision also leads us to some inconvenience: upon inserting we need to retrieve snippet id from snippet_types and this leads to one more select and check before inserting:
SELECT id FROM snippet_types WHERE name = "foo";
-- ...check that > 0 rows returned...
INSERT INTO snippets (title, content, type) values ("bar", "buz", id);
We could also combine this insert and select into one select like that:
INSERT INTO snippets (title, content, type)
SELECT ("bar", "buz", id) FROM snippet_types WHERE name = "foo"
However, if "foo" type is missing in snippet_types then 0 rows would have been inserted and no error returned and I don't see a possibility to get a number of rows sqlite actually inserted.
How can I insert ENUM-containing tuple in one query?

Oracle SQL Check

I'm trying to implement an Oracle SQL database, in one of my tables I must introduce a restriction which does not allow to have more than 4 people in the same group:
I've tried this:
CREATE TABLE PERSON (name VARCHAR (20) PRIMARY KEY, group VARCHAR (3), CHECK (COUNT (*) group FROM PERSON) <=4);
also this (among others):
CREATE TABLE PERSON (name VARCHAR (20) PRIMARY KEY, group VARCHAR (3), CHECK NOT EXISTS (Select COUNT(*) FROM PERSON GROUP BY group HAVING COUNT(*) > 4);
But I'm getting errors every time (ORA-00934: group function is not allowed here or ORA-02251: subquery not allowed here.
What is the correct way to do it?
You have multiple issues with this
CREATE TABLE PERSON (
name VARCHAR(20) PRIMARY KEY,
group VARCHAR(3),
CHECK (COUNT (*) group FROM PERSON) <=4);
);
Oracle explicitly prefers VARCHAR2() to VARCHAR().
GROUP is a really bad name for a column, because it is a keyword. Surely you can find something like group_name or whatever for the name.
CHECK constraints only work within a single row.
Probably the best way to handle this is:
Create a new table called groups -- or whatever. It should have a group_id as well as group_name and num_persons.
Add triggers to person to keep the counter up-to-date for inserts, deletes, and updates to person.
Add a check constraint to groups, say check (num_persons <= 4).
You need to create the table as following:
CREATE TABLE PERSON (
name VARCHAR2(20) PRIMARY KEY,
group_ VARCHAR2(3) -- added _ after column name
); -- used varchar2 as data type of column
Then create before insert trigger as following:
create trigger person_trg
before insert on person
for each row
declare
group_cnt number;
begin
select count(distinct name)
into group_cnt
from person
where group_ = :new.group_;
if group_cnt = 4 then
raise_application_error(-20001, 'more than 4 persons are not allowed in the group');
end if;
end;
/
I have used distinct person name as more than 4 distinct persons are not allowed in the group as per your requirement.
db<>fiddle demo
Cheers!!

How to add values in one table automatically when a condition is true

So, Here is the condition.
I have a User_tbl whose code is as follow
CREATE TABLE Users_tbl (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT,
password TEXT,
user_type INT
);
user_type is either 0,1 or 2 .. If it is 0 then it is player , 1 is for coach and 2 is for audience.
Now, I want to create a new table which has lists of coach inside it . Whose schema will be
CREATE TABLE coach_tbl(
coach_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT,
password TEXT
)
and what I need is that, when the entry which is placed in Users_tbl has user_type =1 then it should Trigger one other query which will create an entry in coach_tbl and fill the columns. It should happen dynamically .
The following TRIGGER would accomplish what you want :-
CREATE TRIGGER setup_coach
AFTER INSERT ON Users_tbl
WHEN new.user_type = 1
BEGIN
INSERT INTO coach_tbl (username, password) VALUES(new.username,new.password);
END
;
The following was used for testing the above :-
DROP TABLE IF EXISTS Users_tbl;
CREATE TABLE IF NOT EXISTS Users_tbl (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT,
password TEXT,
user_type INT
);
DROP TABLE IF EXISTS coach_tbl;
CREATE TABLE IF NOT EXISTS coach_tbl(
coach_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT,
password TEXT
);
CREATE TRIGGER setup_coach
AFTER INSERT ON Users_tbl
WHEN new.user_type = 1
BEGIN
INSERT INTO coach_tbl (username, password) VALUES(new.username,new.password);
END
;
INSERT INTO Users_tbl (username, password,user_type)
VALUES
('Fred','fred1234',0),
('Bert','bert1234',1),
('Harold','harold1234',0),
('Alan','alan1234',1);
The result being :-
Supplementary (aka Why not the above)
Storing the exact same values. i.e. duplicating them, is contrary to normalisation and could lead to issues. e.g. if (using the above) "Bert"'s name or "password" changed you'd have to change it in two places. As it stands there is no need to duplicate this data as it would be easily available
For example, if you wanted to list the coaches you could use :-
SELECT username FROM coach_tbl;
Using the following, though, returns exactly the same but without the additional table :-
SELECT username FROM Users_tbl WHERE user_type = 1;
Supposing you had coach specific information say for example the number of times coaching then you could have an additional table such as :-
CREATE TABLE IF NOT EXISTS coaching_information
(
user_id_reference INTEGER REFERENCES Users_tbl(id),
number_of_times_coaching INTEGER DEFAULT 0
)
;
Along with a TRIGGER to automatically add default coaching information :-
CREATE TRIGGER autoadd_coaching_information_entry
AFTER INSERT ON Users_tbl
WHEN new.user_type = 1
BEGIN
INSERT INTO coaching_information
(user_id_reference)
VALUES (new.id)
;
END
;
Note! no need to set the number_of_times_coaching column as it will default to 0.
Assuming that all TABLES have been emptied then using (again):-
INSERT INTO Users_tbl (username, password,user_type)
VALUES
('Fred','fred1234',0),
('Bert','bert1234',1),
('Harold','harold1234',0),
('Alan','alan1234',1);
results in :-
2 refers to Bert, 4 to Alan
You could then list all coaches who have not coached (chosen for laziness of not having to update coaching inforamtion) :-
SELECT 'Coach '||username||' is up for a coaching experience.' AS coaching_needed
FROM coaching_information
JOIN Users_tbl ON Users_tbl.id = coaching_information.user_id_reference
WHERE coaching_information.number_of_times_coaching < 1
The result would be :-
Say Bert changed name to Charles e.g. using UPDATE Users_tbl SET username = 'Charles' WHERE username = 'Bert';
Then just with the 1 change the results from the above query would be :-
The full SQL used for testing the above was :-
DROP TABLE IF EXISTS coach_tbl;
DROP TABLE IF EXISTS Users_tbl;
CREATE TABLE IF NOT EXISTS Users_tbl (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT,
password TEXT,
user_type INT
);
DROP TRIGGER IF EXISTS setup_coach; -- get rid of defunct TRIGGER
CREATE TABLE IF NOT EXISTS coaching_information
(
user_id_reference INTEGER REFERENCES Users_tbl(id) ON DELETE CASCADE,
-- Note ON DELETE CASCADE will automatically delete a coach should
-- the coach's User_tbl entry be deleted i.e. the deletion is propoagted
-- to the children of the parent.
-- (could also use ON UPDATE CASCADE but unlikely to change the underlying id)
number_of_times_coaching INTEGER DEFAULT 0
)
;
DROP TRIGGER IF EXISTS autoadd_coaching_information_entry;
CREATE TRIGGER autoadd_coaching_information_entry
AFTER INSERT ON Users_tbl
WHEN new.user_type = 1
BEGIN
INSERT INTO coaching_information
(user_id_reference)
VALUES (new.id)
;
END
;
INSERT INTO Users_tbl (username, password,user_type)
VALUES
('Fred','fred1234',0),
('Bert','bert1234',1),
('Harold','harold1234',0),
('Alan','alan1234',1)
;
--
-- Note! oncommmenting this will change Bert's name to Charles
--UPDATE Users_tbl SET username = 'Charles' WHERE username = 'Bert';
--
SELECT 'Coach '||username||' is up for a coaching experience.' AS coaching_needed
FROM coaching_information
JOIN Users_tbl ON Users_tbl.id = coaching_information.user_id_reference
WHERE coaching_information.number_of_times_coaching < 1

How can I fill empty table rows which references another table's column ID?

I want to fill out empty notification_settings for each user that already exists. IDs (PKs) are auto-generated by Hibernate in each table. Here is the user Table :
CREATE TABLE lottery_user (
id int8 not null,
email varchar(255) not null,
password varchar(255) not null,
notification_settings_id int8,
role varchar (50) default 'USER',
registry_date TIMESTAMP default now(),
primary key (id)
ADD CONSTRAINT FK_USER_TO_NOTIFICATION_SETTINGS
FOREIGN KEY (notification_settings_id) REFERENCES notification_settings
);
And here is the notification_settings table which I need to fill out for users that don't have it filled out for them.
CREATE TABLE notification_settings (
id int8 not NULL ,
test1_events bool DEFAULT TRUE ,
test2_events bool DEFAULT TRUE ,
test3_events bool DEFAULT TRUE ,
test4_events bool DEFAULT TRUE ,
PRIMARY KEY (id)
);
Basically, I need to use "INSERT INTO notification_settings (test1_events, test2_events, test3_events, test4_events) VALUES (True, True, True, True)" something similar to that. And of course, condition should be something like this "where these rows are empty for users". I can't seem to get Syntax right.
BIG NOTE: SQL code is for presentation purpose, so you can have an idea what kind of tables I have. I just need to get INSERT script right. Tables are working fine, just need to generate notification_settings values for users that already exist.
Another Note: Using Flyway, so it's not just about Hibernate. If that has to do with anything.
Are you just looking for:
INSERT INTO notification_settings (id)
SELECT id
FROM user
WHERE id NOT IN (SELECT id FROM notifiation_settings)
You might be looking to insert into an identity field:
SET IDENTITY_INSERT my_table ON
Since your foreign key constraint goes from notification_settings to user, the condition "where these rows are empty for user X" does not apply to your schema. On the other hand - "I want to fill out empty notification_settings for each user that already exists" can be done by using an insert...select construct:
set #rank=0
select #maxid = max(id) from notification_settings
insert into notification_settings (id)
select #maxid + #rank:=#rank+1 as rank
from user
where notification_settings_id is null
What is interesting is how you put those newly generated IDs back into the user table. Homework assignment for next time :)
INSERT INTO notification_settings (id)
SELECT u.id
FROM user u
WHERE
not exists (SELECT * FROM notifiation_settings ns where ns.id=i.id)
I will answer my own question how I dealt with it. Firstly, I insert IDs into notification_settings id, then I get those IDs and set them into lottery_user table's FK (notification_settings_id). Then I just delete unneeded IDs. Yea not perfect but it works.
INSERT INTO notification_settings (id) select lu.id from lottery_user lu where lu.id not in(select ns.id from notification_settings ns);
update lottery_user lu set notification_settings_id = (select ns.id from notification_settings ns where ns.id = lu.id) where lu.notification_settings_id is null;
delete from notification_settings ns where not exists (select * from lottery_user lu where lu.notification_settings_id = ns.id);
Also, script to Alter Sequence for new Lottery_user entities.
do $$
declare maxid int;
begin
select max(id) from lottery_user into maxid;
IF maxid IS NOT NULL THEN
EXECUTE 'ALTER SEQUENCE notification_settings_seq START with '|| maxid;
END IF;
end;
$$ language plpgsql;

Storing a database reference within the database

I want to be able to label the database with a single value, i.e its name, from within the database instead of my application, since it will always be one ID per database. For example, something like this:
DATABASE_A.sql
-- Database Name Table
CREATE TABLE database (
name VARCHAR(10) NOT NULL UNIQUE,
);
CREATE TABLE item (
id SERIAL PRIMARY KEY,
name VARCHAR(10) NOT NULL UNIQUE,
);
Insert Into database (name) values ('A');
DATABASE_B.sql
-- Database Name Table
CREATE TABLE database (
name VARCHAR(10) NOT NULL UNIQUE,
);
CREATE TABLE item (
id SERIAL PRIMARY KEY,
name VARCHAR(10) NOT NULL UNIQUE,
);
Insert Into database (name) values ('B');
This is because when they are combined and stored on a SOLR search server their ID is a combination of their database name and their item ID, such as this:
SOLR ITEM ID's
A1
A2
A3
B1
Is it ok to have a single table to define the prefix so that when I do the look up from my SQL website to SOLR I can just do the following query:
database (name) + item (id) = SolrID
I'd be more inclined to build a procedure in each database that contained the database ID, for example:
CREATE OR REPLACE FUNCTION solrid(IN local_id INTEGER, OUT result TEXT) AS $$
DECLARE
database_id TEXT := 'A';
BEGIN
result := database_id || local_id::TEXT;
END;
$$ LANGUAGE PLPGSQL;
Then you could write your select statement like:
SELECT solrid(id), name FROM item;
which seems to be a cleaner solution.