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}