001package com.studentgui.apphelpers; 002 003import java.io.IOException; 004import java.nio.file.Files; 005import java.nio.file.Path; 006import java.sql.Connection; 007import java.sql.DriverManager; 008import java.sql.SQLException; 009import java.sql.Statement; 010 011import org.slf4j.Logger; 012import org.slf4j.LoggerFactory; 013 014/** 015 * SQL schema generator for the normalized application database. 016 * 017 * This class ensures the SQLite database file exists and creates the 018 * canonical tables used by the application. Safe to call repeatedly on 019 * application startup. 020 */ 021/** 022 * Utility responsible for creating/validating the on-disk SQLite database 023 * and canonical schema used by the application. Safe to call multiple times. 024 */ 025public class SqlGenerate { 026 private static final Path DB = Helpers.DATABASE_PATH; 027 private static final Logger LOG = LoggerFactory.getLogger(SqlGenerate.class); 028 // Ported schema from Python appHelpers/sqlgenerate.py 029 private static final String[] SCHEMA = new String[] { 030 // Core student table 031 """ 032 CREATE TABLE IF NOT EXISTS Student ( 033 id INTEGER PRIMARY KEY AUTOINCREMENT, 034 name TEXT NOT NULL, 035 birthdate TEXT, 036 notes TEXT 037 ); 038 """, 039 // ProgressType 040 """ 041 CREATE TABLE IF NOT EXISTS ProgressType ( 042 id INTEGER PRIMARY KEY AUTOINCREMENT, 043 name TEXT NOT NULL UNIQUE, 044 description TEXT 045 ); 046 """, 047 // ProgressSession 048 """ 049 CREATE TABLE IF NOT EXISTS ProgressSession ( 050 id INTEGER PRIMARY KEY AUTOINCREMENT, 051 student_id INTEGER NOT NULL, 052 progress_type_id INTEGER NOT NULL, 053 date TEXT NOT NULL, 054 notes TEXT, 055 FOREIGN KEY(student_id) REFERENCES Student(id) ON DELETE CASCADE, 056 FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE 057 ); 058 """, 059 // KeyboardingResult 060 """ 061 CREATE TABLE IF NOT EXISTS KeyboardingResult ( 062 id INTEGER PRIMARY KEY AUTOINCREMENT, 063 session_id INTEGER NOT NULL, 064 program TEXT NOT NULL, 065 topic TEXT NOT NULL, 066 speed INTEGER NOT NULL, 067 accuracy INTEGER NOT NULL, 068 FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE 069 ); 070 """, 071 // TrialResult 072 """ 073 CREATE TABLE IF NOT EXISTS TrialResult ( 074 id INTEGER PRIMARY KEY AUTOINCREMENT, 075 session_id INTEGER NOT NULL, 076 task TEXT NOT NULL, 077 lesson TEXT, 078 session_label TEXT, 079 trial_number INTEGER NOT NULL, 080 score INTEGER, 081 FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE 082 ); 083 """, 084 // TrialSessionSummary 085 """ 086 CREATE TABLE IF NOT EXISTS TrialSessionSummary ( 087 id INTEGER PRIMARY KEY AUTOINCREMENT, 088 session_id INTEGER NOT NULL UNIQUE, 089 median FLOAT, 090 notes TEXT, 091 FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE 092 ); 093 """, 094 // AssessmentPart 095 """ 096 CREATE TABLE IF NOT EXISTS AssessmentPart ( 097 id INTEGER PRIMARY KEY AUTOINCREMENT, 098 progress_type_id INTEGER NOT NULL, 099 code TEXT NOT NULL, 100 description TEXT, 101 UNIQUE(progress_type_id, code), 102 FOREIGN KEY(progress_type_id) REFERENCES ProgressType(id) ON DELETE CASCADE 103 ); 104 """, 105 // AssessmentResult 106 """ 107 CREATE TABLE IF NOT EXISTS AssessmentResult ( 108 id INTEGER PRIMARY KEY AUTOINCREMENT, 109 session_id INTEGER NOT NULL, 110 part_id INTEGER NOT NULL, 111 score INTEGER, 112 FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE, 113 FOREIGN KEY(part_id) REFERENCES AssessmentPart(id) ON DELETE CASCADE 114 ); 115 """ 116 , 117 // ContactLog details tied to a ProgressSession 118 """ 119 CREATE TABLE IF NOT EXISTS ContactLog ( 120 id INTEGER PRIMARY KEY AUTOINCREMENT, 121 session_id INTEGER NOT NULL, 122 student_name TEXT, 123 date TEXT, 124 guardian_name TEXT, 125 contact_method TEXT, 126 phone_number TEXT, 127 email_address TEXT, 128 contact_response TEXT, 129 contact_general TEXT, 130 contact_specific TEXT, 131 contact_notes TEXT, 132 FOREIGN KEY(session_id) REFERENCES ProgressSession(id) ON DELETE CASCADE 133 ); 134 """ 135 }; 136 137 /** 138 * Ensure the database file and canonical schema exist. This method is idempotent 139 * and safe to call on application startup. It will create the parent folder 140 * for the DB file if necessary and apply the embedded SCHEMA statements. 141 */ 142 public static void initializeDatabase() { 143 try { 144 Path parent = DB.getParent(); 145 if (parent != null && !Files.exists(parent)) { 146 Files.createDirectories(parent); 147 } 148 if (Files.exists(DB) && Files.isDirectory(DB)) { 149 LOG.error("Path is a directory, cannot create DB file: {}", DB); 150 return; 151 } 152 if (Files.exists(DB)) { 153 LOG.info("Database already exists at {}", DB); 154 // even if the DB exists, ensure schema is present by connecting and executing schema statements 155 } 156 // create/connect to SQLite database file by opening a connection 157 String url = "jdbc:sqlite:" + DB.toString(); 158 try (Connection conn = DriverManager.getConnection(url)) { 159 if (conn != null) { 160 executeSchema(conn); 161 } 162 } 163 LOG.info("Database initialized/validated at {}", DB); 164 } catch (SQLException | IOException e) { 165 LOG.error("Error initializing database", e); 166 } 167 } 168 169 /** 170 * Execute the SCHEMA statements on the provided connection. 171 * Extracted to make the schema application clearer and easier to test. 172 * 173 * @param conn established JDBC connection to the target SQLite DB 174 * @throws SQLException if applying any schema statement fails 175 */ 176 private static void executeSchema(final Connection conn) throws SQLException { 177 try (Statement st = conn.createStatement()) { 178 for (String sql : SCHEMA) { 179 st.execute(sql); 180 } 181 } 182 } 183 184 /** 185 * Private constructor to prevent instantiation of this utility class. 186 */ 187 private SqlGenerate() { 188 throw new AssertionError("Not instantiable"); 189 } 190}