Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
target

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
Expand Down
70 changes: 69 additions & 1 deletion task-1/schema.sql
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
-- Write here your DDL statements to create the tables for the database.
CREATE TABLE member (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to enforce a basic shape at the database level, you could add a small sanity CHECK constraint like below, but don’t try to fully validate every possible email format in SQL. The important database constraint here is UNIQUE, because each member email should identify one member.

email VARCHAR(255) NOT NULL UNIQUE 
    CHECK (email ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$')

phone_number VARCHAR(255) NOT NULL,
address VARCHAR(255) NOT NULL
);

create table card (
id SERIAL PRIMARY KEY,
member_id INT NOT NULL UNIQUE REFERENCES member(id) ON DELETE CASCADE,
number VARCHAR(255) NOT NULL UNIQUE,
issued_on DATE NOT NULL,
expires_on DATE NOT NULL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For data integrity need to validate that expiry date is not before issued_on date. You could add a constraint like
CHECK (expires_on > issued_on)

);

CREATE TABLE author (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE publisher (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);

CREATE TABLE book (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
isbn VARCHAR(255) NOT NULL UNIQUE ,
publisher_id INT NOT NULL REFERENCES publisher(id) ON DELETE CASCADE

);

CREATE TABLE book_author (
book_id INT NOT NULL REFERENCES book(id) ON DELETE CASCADE,
author_id INT NOT NULL REFERENCES author(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, author_id)
);

CREATE TABLE tag (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE book_tag (
book_id INT NOT NULL REFERENCES book(id) ON DELETE CASCADE,
tag_id INT NOT NULL REFERENCES tag(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, tag_id)
);

CREATE TABLE book_copy (
id SERIAL PRIMARY KEY,
book_id INT NOT NULL REFERENCES book(id) ON DELETE CASCADE,
barcode VARCHAR(255) NOT NULL UNIQUE,
shelf_code VARCHAR(255) NOT NULL
);
CREATE TABLE borrowing (
id SERIAL PRIMARY KEY,
member_id INT NOT NULL REFERENCES member(id) ON DELETE CASCADE,
book_copy_id INT NOT NULL REFERENCES book_copy(id) ON DELETE CASCADE,
borrowed_at TIMESTAMP NOT NULL,
due_date TIMESTAMP NOT NULL,
returned_at TIMESTAMP ,
fine_eur NUMERIC(8, 2) NOT NULL DEFAULT 0,

CHECK (fine_eur >= 0),
CHECK (returned_at IS NULL OR returned_at >= borrowed_at),
CHECK (due_date >= borrowed_at::DATE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because borrowed_at::DATE removes the time part, this can allow a due date that is earlier than the actual borrowing timestamp on the same day. For example, a book borrowed at 10:00 could have a due date of 09:00 on the same date and still pass the date-only comparison.

Use the full timestamp comparison instead:

CHECK (due_date >= borrowed_at)

);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, clean schema overall. Good normalization, sensible bridge tables, and the CHECK constraints are spot on! One thing to note though: you've used ON DELETE CASCADE on every foreign key, and in several places that's dangerous.

Take the borrowing table:

member_id INT NOT NULL REFERENCES member(id) ON DELETE CASCADE,
book_copy_id INT NOT NULL REFERENCES book_copy(id) ON DELETE CASCADE,

Borrowings are the library's financial/bookkeeping records (loans, due dates, fines). With CASCADE, deleting a member or even a single damaged book copy, silently wipes out the entire loan history attached to it, including unpaid fines.
A library can never explain "where did €1.75 of fines go?" if the records vanish with the row.
Historical/transactional records should almost never cascade away.

The same applies to book.publisher_id:

publisher_id INT NOT NULL REFERENCES publisher(id) ON DELETE CASCADE

Deleting one publisher would delete all of its books, which cascades further into book_copy, book_author, book_tag… one DELETE FROM publisher could empty half the catalog. Here you want the database to protect you instead:

  • CASCADE only when the child row is meaningless without the parent (e.g. bridge rows like book_author/book_tag, or card belonging to a member).
  • RESTRICT (or default: NO ACTION) when deleting the parent would destroy valuable data: borrowing.member_id, borrowing.book_copy_id, book.publisher_id, book_copy.book_id.

Loading