package com.cyynote.dso; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Logger; import javax.sql.DataSource; public class DsHelper { private static final Logger log = Logger.getLogger(DsHelper.class.getName()); private static boolean inited = false; public static void initData(DataSource ds) { if (inited) return; inited = true; try (Connection conn = ds.getConnection()) { // 1. Initial schema: create all base tables if not present if (!tableExists(conn, "appx")) { log.info("[DB] Running initial schema..."); for (String sql : loadSqlFile("/db/schema.sql")) { runSqlIgnoreError(conn, sql, null); } } // 2. Versioned migrations runMigrations(conn); } catch (SQLException e) { throw new RuntimeException("DB init failed", e); } } private static void runMigrations(Connection conn) throws SQLException { runSql(conn, "CREATE TABLE IF NOT EXISTS db_version (" + "version INTEGER PRIMARY KEY, applied_at TEXT)"); List migrationFiles = listMigrationFiles(); Collections.sort(migrationFiles); for (String fileName : migrationFiles) { int version = parseMigrationVersion(fileName); if (version < 0) continue; if (isMigrationApplied(conn, version)) continue; log.info("[DB] Running migration: " + fileName); String[] statements = loadSqlFile("/db/migrations/" + fileName); for (String sql : statements) { runSqlIgnoreError(conn, sql, "duplicate column"); } PreparedStatement ps = conn.prepareStatement( "INSERT OR IGNORE INTO db_version(version, applied_at) VALUES(?, datetime('now'))"); ps.setInt(1, version); ps.executeUpdate(); ps.close(); log.info("[DB] Migration V" + version + " applied."); } } private static boolean isMigrationApplied(Connection conn, int version) throws SQLException { PreparedStatement ps = conn.prepareStatement( "SELECT 1 FROM db_version WHERE version = ?"); ps.setInt(1, version); ResultSet rs = ps.executeQuery(); boolean exists = rs.next(); ps.close(); return exists; } private static List listMigrationFiles() { List files = new ArrayList<>(); try { URL dirUrl = DsHelper.class.getResource("/db/migrations/"); if (dirUrl == null) return files; String protocol = dirUrl.getProtocol(); if ("file".equals(protocol)) { java.io.File dir = new java.io.File(dirUrl.toURI()); if (dir.isDirectory()) { for (java.io.File f : dir.listFiles()) { if (f.getName().startsWith("V") && f.getName().endsWith(".sql")) { files.add(f.getName()); } } } } else if ("jar".equals(protocol)) { String jarPath = dirUrl.getPath(); String jarFile = jarPath.substring(5, jarPath.indexOf("!")); String prefix = jarPath.substring(jarPath.indexOf("!") + 2); try (java.util.jar.JarFile jar = new java.util.jar.JarFile(jarFile)) { java.util.Enumeration entries = jar.entries(); while (entries.hasMoreElements()) { String name = entries.nextElement().getName(); if (name.startsWith(prefix) && name.endsWith(".sql")) { String fileName = name.substring(name.lastIndexOf('/') + 1); if (fileName.startsWith("V")) { files.add(fileName); } } } } } } catch (Exception e) { log.warning("[DB] Could not list migration files: " + e.getMessage()); } return files; } private static int parseMigrationVersion(String fileName) { try { String num = fileName.substring(1, fileName.indexOf("__")); return Integer.parseInt(num); } catch (Exception e) { return -1; } } private static boolean tableExists(Connection conn, String tableName) throws SQLException { PreparedStatement ps = conn.prepareStatement( "SELECT name FROM sqlite_master WHERE type='table' AND name=?"); ps.setString(1, tableName); ResultSet rs = ps.executeQuery(); boolean exists = rs.next(); ps.close(); return exists; } private static String[] loadSqlFile(String path) { try { InputStream ins = DsHelper.class.getResourceAsStream(path); if (ins == null) return new String[0]; byte[] bs = ins.readAllBytes(); String str = new String(bs, StandardCharsets.UTF_8); String[] parts = str.split(";"); List result = new ArrayList<>(); for (String p : parts) { String trimmed = p.trim(); if (!trimmed.isEmpty()) result.add(trimmed); } return result.toArray(new String[0]); } catch (Exception ex) { throw new RuntimeException("Failed to load SQL file: " + path, ex); } } private static void runSql(Connection conn, String sql) throws SQLException { Statement st = conn.createStatement(); st.execute(sql); st.close(); } private static void runSqlIgnoreError(Connection conn, String sql, String ignoreContaining) { try { String trimmed = sql.trim(); if (trimmed.isEmpty()) return; log.info("[DB SQL] " + trimmed.substring(0, Math.min(80, trimmed.length()))); Statement st = conn.createStatement(); st.execute(trimmed); st.close(); } catch (SQLException e) { if (ignoreContaining != null && e.getMessage() != null && e.getMessage().toLowerCase().contains(ignoreContaining.toLowerCase())) { log.info("[DB] Ignored: " + e.getMessage()); } else { log.warning("[DB] SQL error (non-fatal): " + e.getMessage() + " | SQL: " + sql.trim().substring(0, Math.min(80, sql.trim().length()))); } } } }