Constraint "ON DELETE RESTRICT" not being respected - sqlite

I'm trying to learn sqlite and am following this tutorial series.
On the terminal, I started this tiny "example" database:
amrsa % sqlite3 example
SQLite version 3.32.3 2020-06-18 14:16:19
Enter ".help" for usage hints.
As a minimal working example to show the unexpected behavior I noticed, I need two tables:
sqlite> CREATE TABLE people(
...> person_id INTEGER PRIMARY KEY,
...> name TEXT NOT NULL
...> );
and
sqlite> CREATE TABLE jobs(
...> job_id INTEGER PRIMARY KEY,
...> description TEXT NOT NULL,
...> person_id INTEGER,
...> FOREIGN KEY (person_id)
...> REFERENCES people (person_id)
...> ON UPDATE RESTRICT
...> ON DELETE RESTRICT
...> );
Now I add some rows to the tables:
sqlite> INSERT INTO people(name)
...> VALUES("Bob"),("Mary");
sqlite> SELECT * FROM people;
1|Bob
2|Mary
Check: Bob and Mary are in the "people" table
sqlite> INSERT INTO jobs(description,person_id)
...> VALUES("job1",1),("job2",1),("job3",2);
sqlite> SELECT * FROM jobs;
1|job1|1
2|job2|1
3|job3|2
two jobs for Bob, and one for Mary: so far so good.
Now, let's try to delete "Bob".
It shouldn't be possible without deleting his jobs from the "jobs" table, given the constraint "ON DELETE RESTRICT", right?
However, it silently deletes the row:
sqlite> DELETE FROM people
...> WHERE person_id = 1;
sqlite> SELECT * FROM people;
2|Mary
sqlite> SELECT * FROM jobs;
1|job1|1
2|job2|1
3|job3|2
sqlite>
Confirmed that Bob is no longer in the "people" table.
Even worse, the jobs referring to him are still in the "jobs" table;
so it's like there was no constraint at all, right?
Am I doing something wrong here?
Or am I assuming something about constraints which is not right?
Thanks in advance.

In SQLite you must enable foreign key support with:
PRAGMA foreign_keys = ON;
because it is disabled by default.
So even if you define foreign key constraints they are ignored, unless you enable them.
See the demo.

Related

Sqlite3 ON DELETE CASCADE does not work

I have got multiple tables in my database. I am going to use only 2 of them as an example.
Basket table:
CREATE TABLE Basket(
id_basket integer primary key autoincrement,
title text)
Computer table:
CREATE TABLE Computer(
id_computer integer primary key autoincrement,
basket integer,
title text,
foreign key (basket) references Basket(id_basket) on delete cascade)
Then I execute the pragma foreign_keys = on query.
According to what I found here earlier, should I delete a row from Basket table to which some of the Computer table rows refer, these rows from Computer table shall be deleted too. But for some reason I get this error: Query Error: FOREIGN KEY constraint failed Unable to fetch row.
I will be grateful for any suggestions, thank you in advance.
Here are some images with table data:
select * from basket query
select * from computer query
Sqlite version: 3.8.2

Creating a trigger with WHERE clause to a table

I started SQLite yesterday and trying stuff. I searched google and stackoverflow for an answer but couldn't find any :( I've created a database and inside that created a few tables income and expense. Then I've inserted 20 entries and tried to make a trigger to log any changes done to entries between 5 - 10. But an error message comes Error: near "where": syntax error
sqlite> CREATE TABLE expense(
...> ID INTEGER PRIMARY KEY AUTOINCREMENT,
...> Category text,
...> Amount real
...> );
sqlite> CREATE TABLE logger(
...> ID int,
...> Time text
...> );
sqlite> CREATE TRIGGER log AFTER UPDATE OF amount ON expense WHERE ID BETWEEN 5 AND 10;
...> BEGIN
...> INSERT INTO logger(ID, time) VALUES(new.ID, datetime('now'));
...> END;
Why doesn't WHERE working? :(
In defining triggers in SQLite you should use WHEN instead of WHERE. Try this:
CREATE TRIGGER log AFTER UPDATE OF amount ON expense
FOR EACH ROW WHEN new.ID BETWEEN 5 AND 10
BEGIN
INSERT INTO logger(ID, time) VALUES(new.ID, datetime('now'));
END;
Note also that I added FOR EACH ROW. It is optional for now in SQLite, but it is useful for documentation.

SQLite does not give syntax error on mistyped data type

I am new to sqlite3 but this seems strange for any dbms
I was trying to create the following table but mistyped decimal to dcimal
sqlite> CREATE TABLE ac(
...> name char(4),
...> pay dcimal(18,5)
...> );
However it allowed me to create the table.
To add to my surprise I tested the below CREATE TABLE
sqlite> CREATE TABLE ac(
...> name r(4),
...> pay l(18,5)
...> );
in both cases not only it allows me to create table but also allows me to insert and retrieve data from it.
sqlite> insert into ac values ('dbcd',18.2);
sqlite> select * from ac;
dbcd|18.2
Is there any way to enforce strict syntax checks, so that it error outs on these junk data types ?
Thanks
Sam
No.
SQLite uses dynamic typing, so it does not care whether you are using dcimal or fluffy bunnies or no data type at all.

How can one see the structure of a table in SQLite? [duplicate]

This question already has answers here:
Is there an SQLite equivalent to MySQL's DESCRIBE [table]?
(7 answers)
Closed 7 years ago.
How can I see the structure of table in SQLite as desc was in Oracle?
PRAGMA table_info(table_name);
This will work for both: command-line and when executed against a connected database.
A link for more details and example. thanks
SQLite Pragma Command
Invoke the sqlite3 utility on the database file, and use its special dot commands:
.tables will list tables
.schema [tablename] will show the CREATE statement(s) for a table or tables
There are many other useful builtin dot commands -- see the documentation at http://www.sqlite.org/sqlite.html, section Special commands to sqlite3.
Example:
sqlite> entropy:~/Library/Mail>sqlite3 Envelope\ Index
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
addresses ews_folders subjects
alarms feeds threads
associations mailboxes todo_notes
attachments messages todos
calendars properties todos_deleted_log
events recipients todos_server_snapshot
sqlite> .schema alarms
CREATE TABLE alarms (ROWID INTEGER PRIMARY KEY AUTOINCREMENT, alarm_id,
todo INTEGER, flags INTEGER, offset_days INTEGER,
reminder_date INTEGER, time INTEGER, argument,
unrecognized_data BLOB);
CREATE INDEX alarm_id_index ON alarms(alarm_id);
CREATE INDEX alarm_todo_index ON alarms(todo);
Note also that SQLite saves the schema and all information about tables in the database itself, in a magic table named sqlite_master, and it's also possible to execute normal SQL queries against that table. For example, the documentation link above shows how to derive the behavior of the .schema and .tables commands, using normal SQL commands (see section: Querying the database schema).
You can query sqlite_master
SELECT sql FROM sqlite_master WHERE name='foo';
which will return a create table SQL statement, for example:
$ sqlite3 mydb.sqlite
sqlite> create table foo (id int primary key, name varchar(10));
sqlite> select sql from sqlite_master where name='foo';
CREATE TABLE foo (id int primary key, name varchar(10))
sqlite> .schema foo
CREATE TABLE foo (id int primary key, name varchar(10));
sqlite> pragma table_info(foo)
0|id|int|0||1
1|name|varchar(10)|0||0
You should be able to see the schema by running
.schema <table>
.schema TableName
Where TableName is the name of the Table
You will get the structure by typing the command:
.schema <tableName>
If you are using PHP you can get it this way:
<?php
$dbname = 'base.db';
$db = new SQLite3($dbname);
$sturturequery = $db->query("SELECT sql FROM sqlite_master WHERE name='foo'");
$table = $sturturequery->fetchArray();
echo '<pre>' . $table['sql'] . '</pre>';
$db->close();
?>
You can use the Firefox add-on called SQLite Manager to view the database's structure clearly.

Sqlite insert into with unique names, getting id

I have a list of strings to insert into a db. They MUST be unique. When i insert i would like their ID (to use as a foreign key in another table) so i use last_insert_rowid. I get 2 problems.
If i use replace, their id
(INTEGER PRIMARY KEY) updates which
breaks my db (entries point to
nonexistent IDs)
If i use ignore, rowid is not updated so i do not get the correct ID
How do i get their Ids? if i dont need to i wouldnt want to use a select statement to check and insert the string if it doesnt exist . How should i do this?
When a UNIQUE constraint violation occurs, the REPLACE algorithm deletes pre-existing rows that are causing the constraint violation prior to inserting or updating the current row and the command continues executing normally. This causes the rowid to change and creates the following problem
Y:> **sqlite3 test**
SQLite version 3.7.4
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> **create table b (c1 integer primary key, c2 text UNIQUE);**
sqlite> **insert or replace into b values (null,'test-1');**
sqlite> **select last_insert_rowid();**
1
sqlite> **insert or replace into b values (null,'test-2');**
sqlite> **select last_insert_rowid();**
2
sqlite> **insert or replace into b values (null,'test-1');**
sqlite> **select last_insert_rowid();**
3
sqlite> **select * from b;**
2|test-2
3|test-1
The work around is to change the definition of the c2 column as follows
create table b (c1 integer primary key, c2 text UNIQUE ON CONFLICT IGNORE);
and to remove the "or replace" clause from your inserts;
then when test after your insert, you will need to execute the following sql: select last_insert_rowid(), changes();
sqlite> **create table b (c1 integer primary key, c2 text UNIQUE ON CONFLICT IGNORE);**
sqlite> **insert into b values (null,'test-1');**
sqlite> **select last_insert_rowid(), changes();**
1|1
sqlite> **insert into b values (null,'test-2');**
sqlite> **select last_insert_rowid(), changes();**
2|1
sqlite> **insert into b values (null,'test-1');**
sqlite> **select last_insert_rowid(), changes();**
2|0
The return value of changes after the 3rd insert will be a notification to your application that you will need to lookup the rowid of "test-1", since it was already on file. Of course if this is a multi-user system, you will need to wrap all this in a transaction as well.
I use the below currently
insert into tbl(c_name) select 'val' where not exists(select id from tbl where c_name ='val');
select id from tbl where c_name ='val';
By "they MUST be unique", do they mean you are sure that they are, or that you want an error as a result if they aren't? If you just make the string itself a key in its table, then I don't understand how either 1 or 2 could be a problem -- you'll get an error as desired in case of unwanted duplication, otherwise the correct ID. Maybe you can clarify your question with a small example of SQL code you're using, the table in question, what behavior you are observing, and what behavior you'd want instead...?
Edited: thanks for the edit but it's still unclear to me what SQL is giving you what problems! If your table comes from, e.g.:
CREATE TABLE Foo(
theid INTEGER PRIMARY KEY AUTOINCREMENT,
aword TEXT UNIQUE ABORT
)
then any attempt to INSERT a duplicated word will fail (the ABORT keyword is optional, as it's the default for UNIQUE) -- isn't that what you want given that you say the words "MUST be unique", i.e., it's an error if they aren't?
The correct answer to your question is: This cannot be done in sqlite. You have to make an additional select query. Quoting the docs for last_insert_rowid:
An INSERT that fails due to a constraint violation is not a successful INSERT and does not change the value returned by this routine. Thus INSERT OR FAIL, INSERT OR IGNORE, INSERT OR ROLLBACK, and INSERT OR ABORT make no changes to the return value of this routine when their insertion fails
Having the same problem in 2022, but since SQLite3 version 3.35.0 (2021-03-12), we have RETURNING.
Combined with UPSERT, it is now possible to achieve this
sqlite> create table test (id INTEGER PRIMARY KEY, text TEXT UNIQUE);
sqlite> insert into test(text) values("a") on conflict do update set id = id returning id;
1
sqlite> insert into test(text) values("a") on conflict do update set id = id returning id;
1
sqlite> insert into test(text) values("b") on conflict do update set id = id returning id;
2
sqlite> insert into test(text) values("b") on conflict do update set id = id returning id;
2
sqlite> select * from test;
1|a
2|b
sqlite> insert into test(text) values("b") on conflict do nothing returning id;
sqlite>
Sadly, this is still a workaround rather than an elegant solution...
On conflict, the insert becomes an update. This means that your update triggers will fire, so you may want to stay away from this!
When the insert is converted into an update, it needs to do something (cf link). However, we don't want to do anything, so we do a no-op by updating id with itself.
Then, returning id gives us the what we want.
Notes:
Our no-op actually does an update, so it costs time, and the trigger on update will fire. But without triggers, it has no effect on the data
Using on conflict do nothing returning id does not fail, but does not return the id.
If usable (again, check your triggers), and if all your tables use the primary key id, then this technique does not need any specialization: just copy/paste on conflict do update set id = id returning id;

Resources