initial commit

This commit is contained in:
Daniel Noulet 2026-09-19 20:31:14 +02:00
commit 6ffc0d2c99
54 changed files with 11900 additions and 0 deletions

View file

@ -0,0 +1,136 @@
-- +goose Up
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(320) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role VARCHAR(32) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE traits (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
normalized VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE job_postings (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT NOT NULL,
seniority VARCHAR(50) NOT NULL,
position VARCHAR(120) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE questions (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES job_postings(id) ON DELETE CASCADE,
text TEXT NOT NULL,
type VARCHAR(32) NOT NULL CHECK (type IN ('open', 'multiple_choice', 'radio', 'code_review')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE question_options (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
text TEXT NOT NULL
);
CREATE TABLE participants (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
birthday DATE,
email VARCHAR(320) NOT NULL UNIQUE,
contact_info TEXT,
photo BYTEA,
photo_type VARCHAR(100),
cv BYTEA,
cv_filename VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE job_posting_labels (
job_posting_id BIGINT NOT NULL REFERENCES job_postings(id) ON DELETE CASCADE,
trait_id BIGINT NOT NULL REFERENCES traits(id) ON DELETE CASCADE,
PRIMARY KEY (job_posting_id, trait_id)
);
CREATE TABLE job_required_skills (
job_posting_id BIGINT NOT NULL REFERENCES job_postings(id) ON DELETE CASCADE,
trait_id BIGINT NOT NULL REFERENCES traits(id) ON DELETE CASCADE,
PRIMARY KEY (job_posting_id, trait_id)
);
CREATE TABLE job_additional_skills (
job_posting_id BIGINT NOT NULL REFERENCES job_postings(id) ON DELETE CASCADE,
trait_id BIGINT NOT NULL REFERENCES traits(id) ON DELETE CASCADE,
PRIMARY KEY (job_posting_id, trait_id)
);
CREATE TABLE participant_traits (
participant_id BIGINT NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
trait_id BIGINT NOT NULL REFERENCES traits(id) ON DELETE CASCADE,
PRIMARY KEY (participant_id, trait_id)
);
INSERT INTO users (email, password_hash, role)
VALUES ('interviewer@go-guess.local', crypt('admin123', gen_salt('bf')), 'interviewer');
INSERT INTO traits (name, normalized) VALUES
('Go', 'go'), ('PostgreSQL', 'postgresql'), ('Docker', 'docker'),
('React', 'react'), ('TypeScript', 'typescript'), ('Kubernetes', 'kubernetes'),
('Backend', 'backend');
INSERT INTO job_postings (title, description, seniority, position) VALUES
('Senior Go Backend Engineer', 'Build reliable APIs for passenger-facing railway systems.', 'Senior', 'Backend Engineer'),
('Full-stack Product Engineer', 'Deliver accessible operational tools from database to browser.', 'Medior', 'Full-stack Engineer');
INSERT INTO job_posting_labels (job_posting_id, trait_id)
SELECT 1, id FROM traits WHERE normalized IN ('backend', 'go')
ON CONFLICT DO NOTHING;
INSERT INTO job_required_skills (job_posting_id, trait_id)
SELECT 1, id FROM traits WHERE normalized IN ('go', 'postgresql', 'docker');
INSERT INTO job_additional_skills (job_posting_id, trait_id)
SELECT 1, id FROM traits WHERE normalized = 'kubernetes';
INSERT INTO job_posting_labels (job_posting_id, trait_id)
SELECT 2, id FROM traits WHERE normalized IN ('react', 'typescript');
INSERT INTO job_required_skills (job_posting_id, trait_id)
SELECT 2, id FROM traits WHERE normalized IN ('go', 'react', 'typescript');
INSERT INTO job_additional_skills (job_posting_id, trait_id)
SELECT 2, id FROM traits WHERE normalized = 'docker';
INSERT INTO questions (job_id, text, type) VALUES
(1, 'How do you design graceful shutdown for an HTTP service?', 'open'),
(1, 'Which construct protects shared data across goroutines?', 'radio'),
(2, 'Review this component and identify unnecessary renders.', 'code_review');
INSERT INTO question_options (question_id, text) VALUES
(2, 'sync.Mutex'), (2, 'context.Context'), (2, 'http.Client');
INSERT INTO participants (first_name, last_name, birthday, email, contact_info, cv, cv_filename) VALUES
('Ada', 'Lovelace', '1990-12-10', 'ada@example.com', '+32 470 00 00 01', encode('Senior backend engineer with Go, PostgreSQL and Docker experience.', 'escape')::bytea, 'ada-cv.txt'),
('Grace', 'Hopper', '1988-12-09', 'grace@example.com', '+32 470 00 00 02', encode('Full-stack developer working with Go, React and TypeScript.', 'escape')::bytea, 'grace-cv.txt');
INSERT INTO participant_traits (participant_id, trait_id)
SELECT 1, id FROM traits WHERE normalized IN ('go', 'postgresql', 'docker');
INSERT INTO participant_traits (participant_id, trait_id)
SELECT 2, id FROM traits WHERE normalized IN ('go', 'react', 'typescript');
-- +goose Down
DROP TABLE IF EXISTS participant_traits;
DROP TABLE IF EXISTS job_additional_skills;
DROP TABLE IF EXISTS job_required_skills;
DROP TABLE IF EXISTS job_posting_labels;
DROP TABLE IF EXISTS participants;
DROP TABLE IF EXISTS question_options;
DROP TABLE IF EXISTS questions;
DROP TABLE IF EXISTS job_postings;
DROP TABLE IF EXISTS traits;
DROP TABLE IF EXISTS users;

View file

@ -0,0 +1,61 @@
-- +goose Up
ALTER TABLE job_postings
ADD COLUMN status VARCHAR(32) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'deprecated')),
ADD COLUMN duration_minutes INTEGER NOT NULL DEFAULT 60
CHECK (duration_minutes BETWEEN 1 AND 480);
UPDATE job_postings SET status = 'published';
ALTER TABLE questions ADD COLUMN deprecated BOOLEAN NOT NULL DEFAULT false;
CREATE TABLE job_posting_questions (
job_posting_id BIGINT NOT NULL REFERENCES job_postings(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE RESTRICT,
PRIMARY KEY (job_posting_id, question_id)
);
INSERT INTO job_posting_questions (job_posting_id, question_id)
SELECT job_id, id FROM questions WHERE job_id IS NOT NULL;
ALTER TABLE questions DROP COLUMN job_id;
CREATE TABLE invitations (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES job_postings(id) ON DELETE CASCADE,
participant_id BIGINT NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
token VARCHAR(64) NOT NULL UNIQUE,
status VARCHAR(32) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'accepted', 'completed')),
accepted_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (job_id, participant_id)
);
CREATE TABLE invitation_answers (
id BIGSERIAL PRIMARY KEY,
invitation_id BIGINT NOT NULL REFERENCES invitations(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE RESTRICT,
answer TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (invitation_id, question_id)
);
-- +goose Down
DROP TABLE IF EXISTS invitation_answers;
DROP TABLE IF EXISTS invitations;
ALTER TABLE questions ADD COLUMN job_id BIGINT REFERENCES job_postings(id) ON DELETE CASCADE;
UPDATE questions q
SET job_id = links.job_posting_id
FROM (
SELECT question_id, min(job_posting_id) AS job_posting_id
FROM job_posting_questions
GROUP BY question_id
) links
WHERE links.question_id = q.id;
DROP TABLE IF EXISTS job_posting_questions;
ALTER TABLE questions DROP COLUMN deprecated;
ALTER TABLE job_postings DROP COLUMN duration_minutes;
ALTER TABLE job_postings DROP COLUMN status;

View file

@ -0,0 +1,12 @@
-- +goose Up
ALTER TABLE invitations
ADD COLUMN duration_minutes INTEGER NOT NULL DEFAULT 60
CHECK (duration_minutes BETWEEN 1 AND 480);
UPDATE invitations i
SET duration_minutes = j.duration_minutes
FROM job_postings j
WHERE j.id = i.job_id;
-- +goose Down
ALTER TABLE invitations DROP COLUMN duration_minutes;

View file

@ -0,0 +1,13 @@
-- +goose Up
ALTER TABLE questions
ADD COLUMN reference_answer TEXT NOT NULL DEFAULT '';
UPDATE questions
SET reference_answer = CASE
WHEN type = 'open' THEN 'Use context cancellation, stop accepting work, drain active requests, and close dependencies within a deadline.'
WHEN type = 'code_review' THEN 'Identify unstable dependencies or state updates that cause avoidable renders and propose memoization only where measured.'
ELSE reference_answer
END;
-- +goose Down
ALTER TABLE questions DROP COLUMN reference_answer;