How to organize CRUD for table with composite key - sql

I have a table to store simple parameters in format param name / param value. But it primary key is composite key.
What is the best way to organize adding/updating/deleting parameters with locking. So for example if someone start putting into table next parameters
first_comp_id second_comp_id paramName paramValue
12 5 param_1 sdgfsdf
12 5 param_2 sdgfsdf
12 5 param_3 sdgfsdf
12 5 param_4 sdgfsdf
Anyone else cannot add/edit or delete any record with {first_comp_id:12, second_comp_id:5} ?
Below my table structure (I slightly changed names but idea the same):
CREATE TABLE [dbo].[SomeTable](
[first_comp_id] [int] NOT NULL,
[second_comp_id] [int] NOT NULL,
[paramName] [varchar](50) NOT NULL,
[paramValue] [varchar](200) NOT NULL,
PRIMARY KEY
(
[first_comp_id] ASC,
[second_comp_id] ASC,
[paramName] ASC
)
)

if i got this correctly you have user1 entering those records
and user1 can edit them and add new records with {first_comp_id:12, second_comp_id:5}
you would need to add a col containing a unique reference to the user
then you would in your code accessing the db add the check that the user has to be correct
update sometable (paramValue) where first = ee and second = bb and name = rr and user = current user
insert would become
IF not EXISTS (SELECT * FROM sometable WHERE first = ee and second = bb and name = rr and user <> current user)
BEGIN
insert into sometable (first,second, name, val, user)
values (ee,bb,rr,vv,uu)
END
or if you want it completely on db level you could use triggers
before update check if correct user
before insert check if first and second aren't used by another user

Related

Update and log only changed rows with SQL in SQLite

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.

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

SQL Constraint on column value depending on value of other column

First, I have simple [SomeType] table, with columns [ID] and [Name].
Also I have [SomeTable] table, with fields like:
[ID],
[SomeTypeID] (FK),
[UserID] (FK),
[IsExpression]
Finally, I have to made on database layer a constraint that:
for concrete [SomeType] IDs (actually, for all but one),
for same UserID,
only one entry should have [IsExpression] equal to 1
(IsExpression is of BIT type)
I don't know if it's complex condition or not, but I have no idea how to write it. How would you implement such constraint?
You can do this with filtered index:
CREATE UNIQUE NONCLUSTERED INDEX [IDX_SomeTable] ON [dbo].[SomeTable]
(
[UserID] ASC
)
WHERE ([SomeTypeID] <> 1 AND [IsExpression] = 1)
or:
CREATE UNIQUE NONCLUSTERED INDEX [IDX_SomeTable] ON [dbo].[SomeTable]
(
[UserID] ASC,
[SomeTypeID] ASC
)
WHERE ([SomeTypeID] <> 1 AND [IsExpression] = 1)
Depends on what you are trying to achieve. Only one [IsExpression] = 1 within one user without consideration of [SomeTypeID] or you want only one [IsExpression] = 1 within one user and one [SomeTypeID].

How can I create a view that does a joins a table to itself and gives a means to select based on a key value

I have the following table DDL:
CREATE TABLE [dbo].[Test] (
[UserId] NVARCHAR (128) NOT NULL,
[TestId] INT IDENTITY (1, 1) NOT NULL,
[ExamId] INT NOT NULL,
[Title] NVARCHAR (100) NULL,
[Status] INT NOT NULL,
[CurrentQuestion] INT DEFAULT ((999)) NOT NULL,
CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED ([TestId] ASC)
);
This table contains:
A list of ALL Exams and Tests that are available. These rows have a UserId = 0
A list of Exam and Test that are in progress for a particular user (UserId != 0)
A user may have one or may have multiple rows in this table.
Is there a way that I can create a view that shows a list of all the tests and where it exists the additional information for a user if that user has started a test. I'm really not sure if this needs to be a view or a stored procedure as it would need to take as input a UserId.
For the UserId = 1 who in this example has started two of the available tests:
ExamId TestId Title Status
1 1 Exam1Test1 Started
1 2 Exam1Test2
2 3 Exam1Test3 Started
2 4 Exam1Test4
or for the UserId = 2 who in this example has started three of the available tests:
1 1 Exam1Test1 Started
1 2 Exam1Test2
2 3 Exam1Test3 Started
2 4 Exam1Test4 Started
Your schema implies that any given test may be taken by at most one user at a time.
The query that will return the results you want is:
select
ExamId,
TestId,
Title,
case when userId = ? then "Started" else "" end as Status
from Tests
Of course you would replace the ? in the query with the actual user you're interested in.

A table that has relation to itself issue

I've defined table with this schema :
CREATE TABLE [dbo].[Codings]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[ParentId] [int] NULL,
[CodeId] [int] NOT NULL,
[Title] [nvarchar](50) COLLATE Arabic_CI_AI NOT NULL,
CONSTRAINT [PK_Codings]
PRIMARY KEY CLUSTERED ([Id] ASC) WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
And fill it up with data like this :
Id ParentId CodeId Title
----------- ----------- ----------- ----------
1 NULL 0 Gender
2 1 1 Male
3 1 2 Female
4 NULL 0 Educational Level
5 4 1 BS
6 4 2 MS
7 4 3 PHD
Now I'm looking for a solution, in order, when I delete a record that is parent (like Id = 1 or 4), it deletes all children automatically (all records that have a ParentId = 1 or 4).
I supposed I can do it via relation between Id and Parent Id (and set cascade for delete rule), but when I do that in MMS, the Delete Rule or Update Rule in Properties is disabled.
My question is: what can I do to accomplish this?
Thank you
Edit:
When I wrote (like Id = 1 or 4) I meant the records that are parent, not a child, and I don't mean query like like Id = 1 or 4
Some friend implied I can do it via a delete trigger, but I supposed I can do it via relation
Maybe you have to define a index on the ParentID column first. You can't put constraints on columns that aren't indexed.
I always script for solutions to this, IE in the application search for the ID, then delete where all parent IDs = ID, then delete the parent record.
You can accomplish it with a "DELETE TRIGGER" - just use it to delete any rows that have matching parents. It's essentially the same thing a cascade-delete would do.
CREATE TRIGGER t_Codings_delete
ON Codings
AFTER DELETE
AS
BEGIN
DELETE Codings
FROM Codings c
JOIN DELETED d -- Virtual table containing rows you just deleted
ON c.ParentId = d.Id
END
How about an OR clause?
Delete From TableName
Where ID = 4 OR ParentId = 4
I don't think you can have cascading deletes on a self-refrencing table. You can probably do it with a join.
Delete from codings
inner join codings2 on codings.id = codings2.parentid
where codings.id = myid
Note not tested.