whaleprep/src/db.rs
dev d3001612b2 PYQ clone: builder, CBT exam screen, grading, analytics dashboard
Axum + SQLite full-stack copy of the ZenithPrep PYQ practice app.
Paper builder with unseen-question exclusion, timed CBT screen with
palette and autosave, +4/-1 grading with numerical tolerance, per-question
analysis, history, dashboard with weak-subtopic tracking and coach notes.
2026-10-11 20:06:26 +05:30

117 lines
12 KiB
Rust

use sqlx::SqlitePool;
pub async fn init_db(pool: &SqlitePool) -> anyhow::Result<()> {
sqlx::query(
"CREATE TABLE IF NOT EXISTS users(id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS chapters(id INTEGER PRIMARY KEY, subject TEXT NOT NULL, name TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS subtopics(id INTEGER PRIMARY KEY, chapter_id INTEGER NOT NULL REFERENCES chapters(id), name TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS questions(id INTEGER PRIMARY KEY, subject TEXT NOT NULL, chapter_id INTEGER NOT NULL, subtopic_id INTEGER NOT NULL, qtype TEXT NOT NULL, stem TEXT NOT NULL, opt_a TEXT, opt_b TEXT, opt_c TEXT, opt_d TEXT, answer TEXT NOT NULL, difficulty TEXT NOT NULL, year INTEGER NOT NULL, shift TEXT NOT NULL, solution TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS papers(id TEXT PRIMARY KEY, user_id TEXT NOT NULL, title TEXT NOT NULL, duration_min INTEGER NOT NULL, created_at TEXT NOT NULL, share_token TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS paper_questions(paper_id TEXT NOT NULL, question_id INTEGER NOT NULL, ord INTEGER NOT NULL, PRIMARY KEY(paper_id, question_id));
CREATE TABLE IF NOT EXISTS attempts(id TEXT PRIMARY KEY, paper_id TEXT NOT NULL, user_id TEXT NOT NULL, started_at TEXT NOT NULL, submitted_at TEXT, score INTEGER DEFAULT 0, max_score INTEGER DEFAULT 0, used_secs INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS answers(attempt_id TEXT NOT NULL, question_id INTEGER NOT NULL, selected TEXT DEFAULT '', marked_review INTEGER DEFAULT 0, time_secs INTEGER DEFAULT 0, PRIMARY KEY(attempt_id, question_id));
CREATE TABLE IF NOT EXISTS mistake_tags(attempt_id TEXT NOT NULL, question_id INTEGER NOT NULL, tag TEXT NOT NULL, PRIMARY KEY(attempt_id, question_id, tag));
CREATE TABLE IF NOT EXISTS sessions(token TEXT PRIMARY KEY, user_id TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS seen_questions(user_id TEXT NOT NULL, question_id INTEGER NOT NULL, PRIMARY KEY(user_id, question_id));",
)
.execute(pool)
.await?;
let (n,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM questions")
.fetch_one(pool)
.await?;
if n == 0 {
seed(pool).await?;
}
Ok(())
}
struct SeedQ<'a> {
subject: &'a str,
chapter: &'a str,
subtopic: &'a str,
qtype: &'a str,
stem: &'a str,
opts: [&'a str; 4],
answer: &'a str,
difficulty: &'a str,
year: i64,
shift: &'a str,
solution: &'a str,
}
pub async fn seed(pool: &SqlitePool) -> anyhow::Result<()> {
let chapters: Vec<(&str, &str, Vec<&str>)> = vec![
("Physics", "Mechanics", vec!["Kinematics", "Laws of Motion", "Work Energy Power"]),
("Physics", "Electromagnetism", vec!["Electrostatics", "Current Electricity"]),
("Physics", "Optics", vec!["Ray Optics", "Wave Optics"]),
("Chemistry", "Physical Chemistry", vec!["Mole Concept", "Chemical Kinetics"]),
("Chemistry", "Organic Chemistry", vec!["GOC", "Aldehydes and Ketones"]),
("Chemistry", "Inorganic Chemistry", vec!["Periodic Table", "Coordination Compounds"]),
("Maths", "Algebra", vec!["Quadratic Equations", "Matrices and Determinants"]),
("Maths", "Calculus", vec!["Limits and Continuity", "Definite Integration"]),
("Maths", "Geometry", vec!["Straight Lines", "Parabola"]),
];
for (subj, ch, subs) in &chapters {
use sqlx::Row;
let id: i64 = sqlx::query("INSERT INTO chapters(subject,name) VALUES(?,?) RETURNING id")
.bind(subj)
.bind(ch)
.fetch_one(pool)
.await?
.get(0);
for s in subs {
sqlx::query("INSERT INTO subtopics(chapter_id,name) VALUES(?,?)")
.bind(id)
.bind(s)
.execute(pool)
.await?;
}
}
let qs: Vec<SeedQ> = vec![
SeedQ{subject:"Physics",chapter:"Mechanics",subtopic:"Kinematics",qtype:"MCQ",stem:"A particle starts from rest with constant acceleration 2 m/s2. Distance covered in the 5th second is:",opts:["9 m","8 m","10 m","5 m"],answer:"A",difficulty:"Easy",year:2024,shift:"S1",solution:"Sn = u + a(2n-1)/2 = 0 + 2(9)/2 = 9 m."},
SeedQ{subject:"Physics",chapter:"Mechanics",subtopic:"Kinematics",qtype:"MCQ",stem:"Velocity-time graph is a straight line parallel to time axis. The motion is:",opts:["uniform velocity","uniform acceleration","non-uniform velocity","at rest"],answer:"A",difficulty:"Easy",year:2023,shift:"S2",solution:"Slope zero means acceleration zero, velocity constant."},
SeedQ{subject:"Physics",chapter:"Mechanics",subtopic:"Laws of Motion",qtype:"MCQ",stem:"A 2 kg block on a frictionless surface is pulled by 10 N. Its acceleration is:",opts:["5 m/s2","20 m/s2","2 m/s2","0.2 m/s2"],answer:"A",difficulty:"Easy",year:2024,shift:"S1",solution:"a = F/m = 10/2 = 5 m/s2."},
SeedQ{subject:"Physics",chapter:"Mechanics",subtopic:"Work Energy Power",qtype:"NUM",stem:"A 1 kg mass drops from 20 m (g=10). Speed just before impact in m/s is:",opts:["","","",""],answer:"20",difficulty:"Medium",year:2022,shift:"S1",solution:"v = sqrt(2gh) = sqrt(400) = 20 m/s."},
SeedQ{subject:"Physics",chapter:"Electromagnetism",subtopic:"Electrostatics",qtype:"MCQ",stem:"Force between two point charges doubles the distance. The force becomes:",opts:["one-fourth","half","double","same"],answer:"A",difficulty:"Easy",year:2023,shift:"S1",solution:"Coulomb force scales as 1/r2."},
SeedQ{subject:"Physics",chapter:"Electromagnetism",subtopic:"Current Electricity",qtype:"MCQ",stem:"A wire of resistance R is stretched to double length. New resistance is:",opts:["4R","2R","R/2","R"],answer:"A",difficulty:"Medium",year:2024,shift:"S2",solution:"R = rho l/A; volume constant gives R proportional to l2, so 4R."},
SeedQ{subject:"Physics",chapter:"Optics",subtopic:"Ray Optics",qtype:"MCQ",stem:"Focal length of an equiconvex lens (n=1.5, R=20 cm) is:",opts:["20 cm","40 cm","10 cm","30 cm"],answer:"A",difficulty:"Medium",year:2022,shift:"S2",solution:"1/f = (n-1)(2/R) = 0.5(2/20) = 1/20."},
SeedQ{subject:"Physics",chapter:"Optics",subtopic:"Wave Optics",qtype:"MCQ",stem:"In YDSE, fringe width is proportional to:",opts:["wavelength","1/wavelength","screen distance squared","slit width"],answer:"A",difficulty:"Easy",year:2023,shift:"S1",solution:"beta = lambda D/d."},
SeedQ{subject:"Chemistry",chapter:"Physical Chemistry",subtopic:"Mole Concept",qtype:"MCQ",stem:"Moles in 4 g of NaOH (M=40) are:",opts:["0.1","0.4","1","4"],answer:"A",difficulty:"Easy",year:2024,shift:"S1",solution:"n = 4/40 = 0.1."},
SeedQ{subject:"Chemistry",chapter:"Physical Chemistry",subtopic:"Chemical Kinetics",qtype:"MCQ",stem:"For a first order reaction, t1/2 = 10 min. Rate constant k is:",opts:["0.0693 min-1","0.693 min-1","10 min-1","1 min-1"],answer:"A",difficulty:"Medium",year:2023,shift:"S2",solution:"k = 0.693/t1/2 = 0.0693 min-1."},
SeedQ{subject:"Chemistry",chapter:"Organic Chemistry",subtopic:"GOC",qtype:"MCQ",stem:"Most stable carbocation among: methyl, ethyl, isopropyl, tert-butyl:",opts:["tert-butyl","methyl","ethyl","isopropyl"],answer:"A",difficulty:"Easy",year:2022,shift:"S1",solution:"Hyperconjugation plus inductive effect: 3 degree most stable."},
SeedQ{subject:"Chemistry",chapter:"Organic Chemistry",subtopic:"Aldehydes and Ketones",qtype:"MCQ",stem:"Tollens reagent gives silver mirror with:",opts:["aldehydes","ketones","alkenes","ethers"],answer:"A",difficulty:"Easy",year:2024,shift:"S2",solution:"Aldehydes reduce Tollens; ketones do not."},
SeedQ{subject:"Chemistry",chapter:"Inorganic Chemistry",subtopic:"Periodic Table",qtype:"MCQ",stem:"Highest first ionisation enthalpy among second period:",opts:["Ne","Li","B","O"],answer:"A",difficulty:"Easy",year:2023,shift:"S1",solution:"Noble gas Ne has maximum IE1 in period 2."},
SeedQ{subject:"Chemistry",chapter:"Inorganic Chemistry",subtopic:"Coordination Compounds",qtype:"MCQ",stem:"Oxidation state of Fe in [Fe(CN)6]4- is:",opts:["+2","+3","+4","0"],answer:"A",difficulty:"Medium",year:2024,shift:"S1",solution:"x + 6(-1) = -4 gives x = +2."},
SeedQ{subject:"Maths",chapter:"Algebra",subtopic:"Quadratic Equations",qtype:"MCQ",stem:"Roots of x2 - 5x + 6 = 0 are:",opts:["2, 3","1, 6","-2, -3","5, 1"],answer:"A",difficulty:"Easy",year:2024,shift:"S1",solution:"Factors (x-2)(x-3) = 0."},
SeedQ{subject:"Maths",chapter:"Algebra",subtopic:"Matrices and Determinants",qtype:"NUM",stem:"Determinant of [[2,0],[0,3]] is:",opts:["","","",""],answer:"6",difficulty:"Easy",year:2023,shift:"S1",solution:"Diagonal product 2x3 = 6."},
SeedQ{subject:"Maths",chapter:"Calculus",subtopic:"Limits and Continuity",qtype:"MCQ",stem:"lim x->0 sin(x)/x equals:",opts:["1","0","infinity","-1"],answer:"A",difficulty:"Easy",year:2022,shift:"S2",solution:"Standard limit equals 1."},
SeedQ{subject:"Maths",chapter:"Calculus",subtopic:"Definite Integration",qtype:"MCQ",stem:"Integral 0 to pi of sin(x) dx equals:",opts:["2","0","1","pi"],answer:"A",difficulty:"Medium",year:2024,shift:"S2",solution:"[-cos x]0^pi = 1 + 1 = 2."},
SeedQ{subject:"Maths",chapter:"Geometry",subtopic:"Straight Lines",qtype:"MCQ",stem:"Slope of line 2x + 3y = 6 is:",opts:["-2/3","2/3","3/2","-3/2"],answer:"A",difficulty:"Easy",year:2023,shift:"S2",solution:"y = -2x/3 + 2, slope -2/3."},
SeedQ{subject:"Maths",chapter:"Geometry",subtopic:"Parabola",qtype:"MCQ",stem:"Focus of y2 = 8x is:",opts:["(2,0)","(0,2)","(4,0)","(8,0)"],answer:"A",difficulty:"Medium",year:2024,shift:"S1",solution:"4a = 8 gives a = 2, focus (a,0)."},
SeedQ{subject:"Physics",chapter:"Mechanics",subtopic:"Laws of Motion",qtype:"MCQ",stem:"A car rounds a curve at constant speed. Its acceleration is:",opts:["centripetal, nonzero","zero","tangential only","infinite"],answer:"A",difficulty:"Medium",year:2023,shift:"S2",solution:"Direction changes so velocity changes; centripetal acceleration nonzero."},
SeedQ{subject:"Chemistry",chapter:"Physical Chemistry",subtopic:"Mole Concept",qtype:"NUM",stem:"Molarity of 0.5 mol solute in 2 L solution is (mol/L):",opts:["","","",""],answer:"0.25",difficulty:"Easy",year:2022,shift:"S1",solution:"M = 0.5/2 = 0.25."},
SeedQ{subject:"Maths",chapter:"Algebra",subtopic:"Quadratic Equations",qtype:"MCQ",stem:"Discriminant of x2 + 4x + 4 is:",opts:["0","16","-16","4"],answer:"A",difficulty:"Easy",year:2023,shift:"S1",solution:"D = 16 - 16 = 0, equal roots."},
SeedQ{subject:"Maths",chapter:"Calculus",subtopic:"Definite Integration",qtype:"NUM",stem:"Integral 0 to 1 of 2x dx equals:",opts:["","","",""],answer:"1",difficulty:"Easy",year:2022,shift:"S1",solution:"[x2]0^1 = 1."},
];
use sqlx::Row;
for q in &qs {
let ch_id: i64 = sqlx::query("SELECT id FROM chapters WHERE subject=? AND name=?")
.bind(q.subject)
.bind(q.chapter)
.fetch_one(pool)
.await?
.get(0);
let st_id: i64 = sqlx::query("SELECT id FROM subtopics WHERE chapter_id=? AND name=?")
.bind(ch_id)
.bind(q.subtopic)
.fetch_one(pool)
.await?
.get(0);
sqlx::query("INSERT INTO questions(subject,chapter_id,subtopic_id,qtype,stem,opt_a,opt_b,opt_c,opt_d,answer,difficulty,year,shift,solution) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
.bind(q.subject).bind(ch_id).bind(st_id).bind(q.qtype).bind(q.stem)
.bind(q.opts[0]).bind(q.opts[1]).bind(q.opts[2]).bind(q.opts[3])
.bind(q.answer).bind(q.difficulty).bind(q.year).bind(q.shift).bind(q.solution)
.execute(pool).await?;
}
Ok(())
}