I'm running the following SQLite workaround to add a primary key to a table that did not have one. I am getting a datatype mismatch on
INSERT INTO cities
SELECT id, name FROM old_cities;
However, the fields have exactly the same type. Is it possible that his happens due to running the queries from DbBrowser for SQLite?
CREATE table cities (
id INTEGER NOT NULL,
name TEXT NOT NULL
);
INSERT INTO cities (id, name)
VALUES ('pan', 'doul');
END TRANSACTION;
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
ALTER TABLE cities RENAME TO old_cities;
--CREATE TABLE cities (
-- id INTEGER NOT NULL PRIMARY KEY,
-- name TEXT NOT NULL
--);
CREATE TABLE cities (
id INTEGER NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (id)
);
SELECT * FROM old_cities;
INSERT INTO cities
SELECT id, name FROM old_cities;
DROP TABLE old_cities;
COMMIT;
You have defined the column id of the table cities to be INTEGER, but with this:
INSERT INTO cities (id, name) VALUES ('pan', 'doul');
you insert the string 'pan' as id.
SQLite does not do any type checking in this case and allows it.
Did you mean to insert 2 rows each having the names 'pan' and 'doul'?
If so, you should do something like:
INSERT INTO cities (id, name) VALUES (1, 'pan'), (2, 'doul');
Later you rename the table cities to old_cities and you recreate cities but you do something different: you define id as INTEGER and PRIMARY KEY.
This definition is the only one that forces type checking in SQLite.
So, when you try to insert the rows from old_cities to cities you get an error because 'pan' is not allowed in the column id as it is defined now.
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.
Basically, users when they create a new record in mytable1, there is an id field that needs to be the same across multiple tables. I achieve this by having mytable2 with the s_id as primary key
My current function looks like
CREATE OR REPLACE FUNCTION test.new_record()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
case when new.s_id in (select s_id from mytable1) then
insert into mytable2 (sprn, date_created) select max(s_id) +1, now() from mytable2 ;
update mytable1 set new.s_id = (select max(b.s_id) from mytable2 b);
end case;
RETURN new;
END;
$function$;
Intended was when the s_id is replicated then it would create a new entry on mytable2. This new entry would then be updated onto mytable1
Problem with this function is that right now it does not recognise the new on the update part of the function.
How to keep the s_id take the value on every new insert ?
If you want to have one "generator" across multiple tables, create one sequence that is used across all those tables for the default value:
create sequence the_id_sequence;
create table one
(
id integer primary key default nextval('the_id_sequence')
.... other columns
);
create table two
(
id integer primary key default nextval('the_id_sequence')
.... other columns ...
);
If you want to replicate an ID from one table to another during insert, you only need one sequence:
create table one
(
-- using identity is the preferred over "serial" to auto-generate PK values
id integer primary key generated always as identity
);
create table two
(
id integer primary key
);
create or replace function insert_two()
returns trigger
as
$$
begin
insert into two (id) values (new.id);
return new;
end;
$$
language plpgsql;
create trigger replicate_id
before insert on one
for each row
execute procedure insert_two();
Then if you run:
insert into one (id) values (default);
A row with exactly the same id value will be inserted into table two.
If you don't have a generated ID column so far, use the following syntax:
alter table one
add testidcolumn bigint generated always as identity;
I'm making a sql script so I have create tables, now I have a new table that have columns. One column has a FOREIGN KEY so I need this value to be SET DEFAULT at the value of the value of the original table. For example consider this two table
PERSON(Name,Surename,ID,Age);
EMPLOYER(Name,Surname,Sector,Age);
In Employer I need AGE to be setted on default on the AGE of Person, this only if PERSON have rows or just 1 row.
ID is Primary key for person and Surname,Sector for employer and AGE is FOREIGN KEY in Employer refferenced from Person
Example sql :
CREATE TABLE PERSON(
name VARCHAR(30) ,
surename VARCHAR(20),
ID VARCHAR(50) PRIMARY KEY,
Age INT NOT NULL,
);
CREATE TABLE EMPLOYER(
name VARCHAR(30) ,
Surename VARCHAR(20),
Sector VARCHAR(20),
Age INT NOT NULL,
PRIMARY KEY (Surename,Sector),
FOREIGN KEY (Age) REFERENCES Person(Age) //HERE SET DEFAULT Person(Age), how'??
);
Taking away the poor design choices of this exercise it is possible to assign the value of a column to that of another one using a trigger.
Rough working example below:
create table a (
cola int,
colb int) ;
create table b (
colc int,
cold int);
Create or replace function fn()
returns trigger
as $$ begin
if new.cold is null then
new.cold = (select colb from a where cola = new.colc);
end if;
return new;
end;
$$ language plpgsql;
CREATE TRIGGER
fn
BEFORE INSERT ON
b
FOR EACH ROW EXECUTE PROCEDURE
fn();
Use a trigger rather than a default. I have done things like this (useful occasionally for aggregated full text vectors among other things).
You cannot use a default here because you have no access to the current row data. Therefore there is nothing to look up if it is depending on your values currently being saved.
Instead you want to create a BEFORE trigger which sets the value if it is not set, and looks up data. Note that this has a different limitation because DEFAULT looks at the query (was a value specified) while a trigger looks at the value (i.e. what does your current row look like). Consequently a default can be avoided by explicitly passing in a NULL. But a trigger will populate that anyway.
I have an SQL below to create a table. It will replace where the name conflict.
CREATE TABLE IF NOT EXISTS MYTABLE (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
NAME TEXT NOT NULL,
DATE INTEGER NOT NULL,
CONSTRAINT UNIQUE_NAME UNIQUE (NAME) ON CONFLICT REPLACE
)
However, I would like to replace only if the date is newer (bigger in number), or ignore the new row if the date is older. How could I alter my SQL (for SQLite) statement above to achieve that?
It is not possible to do this with a table constraint, you have to use triggers instead:
CREATE INDEX just_some_index ON MyTable(Name);
CREATE TRIGGER MyTable_Name_insert_newer
BEFORE INSERT ON MyTable
FOR EACH ROW
WHEN (SELECT Date FROM MyTable WHERE Name = NEW.Name) <= NEW.Date
BEGIN
DELETE FROM MyTable
WHERE Name = NEW.Name;
END;
CREATE TRIGGER MyTable_Name_insert_older
BEFORE INSERT ON MyTable
FOR EACH ROW
WHEN (SELECT Date FROM MyTable WHERE Name = NEW.Name) > NEW.Date
BEGIN
SELECT RAISE(IGNORE);
END;
(In SQLite, a scalar subquery without a result returns just NULL, so inserting a new row makes both WHEN clauses fail.)