一键导入
starsector-database
SQLite database design, indexing engine, transaction strategies, and query service in Starsector Ship Editor.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
SQLite database design, indexing engine, transaction strategies, and query service in Starsector Ship Editor.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Core guidelines, coordinate transformation mathematics, high-performance drawing techniques, and state recovery patterns for OpenGL rendering in Starsector Ship Editor.
Core system architecture including EventBus, Undo/Redo, Layer System, Threading Model, and Startup Sequence.
Guidelines for Jackson configuration, JSON pre-processing, CSV serialization rules, and entity ID extraction in Starsector Ship Editor.
Information about the required technology stack, environment JVM flags, and build plugins.
Guidelines for JUnit, jqwik property-based testing, SpotBugs static analysis, and verification rules.
| name | starsector-database |
| description | SQLite database design, indexing engine, transaction strategies, and query service in Starsector Ship Editor. |
This skill is organized as follows:
SKILL.md: Main instructions (this file).resources/: Configurations and schemas.
examples/: Code references.
scripts/: Tooling.
The editor uses an embedded SQLite database (ship_editor_database.sqlite) managed by DatabaseManager.java. The database indexes all game and mod files by entity type and ID, enabling instant lookups without full-disk rescans on every launch.
mods TableCREATE TABLE IF NOT EXISTS mods (
id TEXT PRIMARY KEY, -- Internal mod ID or "starsector-core"
name TEXT NOT NULL, -- Friendly display name
folder_path TEXT NOT NULL, -- Absolute path to the mod folder
last_scanned INTEGER NOT NULL -- Epoch milliseconds of last full scan
);
indexed_files TableCREATE TABLE IF NOT EXISTS indexed_files (
uuid TEXT PRIMARY KEY, -- UUID as string (not native SQLite UUID)
mod_id TEXT, -- FK → mods.id
entity_id TEXT, -- hullId / skinHullId / variantId / weapon id
entity_name TEXT, -- Friendly name (filename without extension)
entity_type TEXT NOT NULL, -- SHIP, SKIN, WEAPON, VARIANT, PROJECTILE, *_CSV, *_JSON
file_name TEXT NOT NULL, -- Just the filename
file_path TEXT NOT NULL, -- Absolute path on disk
last_modified INTEGER NOT NULL, -- File's lastModified epoch ms
FOREIGN KEY(mod_id) REFERENCES mods(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_entity_id ON indexed_files(entity_id);
CREATE INDEX IF NOT EXISTS idx_entity_type ON indexed_files(entity_type);
entity_type | Source Files |
|---|---|
SHIP | .ship files |
SKIN | .skin files |
WEAPON | .wpn files |
VARIANT | .variant files |
PROJECTILE | .proj files |
SHIP_CSV | data/hulls/ship_data.csv |
WEAPON_CSV | data/weapons/weapon_data.csv |
HULLMOD_CSV | data/hullmods/hull_mods.csv |
SHIPSYSTEM_CSV | data/shipsystems/ship_systems.csv |
WING_CSV | data/hulls/wing_data.csv |
ENGINE_STYLE_JSON | data/config/engine_styles.json |
HULL_STYLE_JSON | data/config/hull_styles.json |
public static Connection getConnection() throws SQLException {
Class.forName("org.sqlite.JDBC");
String dbUrl = "jdbc:sqlite:" + path + "?busy_timeout=5000";
Connection conn = DriverManager.getConnection(dbUrl);
// PRAGMAs applied per-connection
stmt.execute("PRAGMA foreign_keys = ON;");
stmt.execute("PRAGMA synchronous = NORMAL;");
stmt.execute("PRAGMA cache_size = -64000;"); // 64 MB page cache
stmt.execute("PRAGMA temp_store = MEMORY;");
return conn;
}
There is no connection pool. Every getConnection() call opens a fresh connection, applies PRAGMAs, and returns it. Connections are closed via try-with-resources at call sites.
| PRAGMA | Value | Why |
|---|---|---|
foreign_keys | ON | Enforce ON DELETE CASCADE from mods → indexed_files |
synchronous | NORMAL | Faster writes than FULL, acceptable crash risk (data is rebuildable from disk) |
cache_size | -64000 | Negative value = KB. 64 MB page cache for fast repeated queries |
temp_store | MEMORY | Temp tables/indexes in RAM, not disk |
journal_mode | WAL | Set during initializeDatabase() only. Enables concurrent reads during writes |
busy_timeout | 5000 | 5-second wait on locked database before failing (URL parameter) |
Class.forName("org.sqlite.JDBC")The JDBC driver is loaded reflectively on every connection. This is technically redundant with JDBC 4.0+ auto-loading, but exists as a safety net because the JPMS module system can interfere with service provider discovery.
getDatabaseFilePath().toAbsolutePath().toString().replace("\\", "/")
SQLite's JDBC URL requires forward slashes even on Windows. The backslash replacement prevents jdbc:sqlite:C:\Users\... from being misinterpreted.
isDatabaseValid() performs three checks:
PRAGMA integrity_check returns "ok"?SELECT count(*) FROM mods and SELECT count(*) FROM indexed_files execute without error?If any check fails, initializeDatabase() deletes the database file and recreates it from scratch:
if (databaseExists() && !isDatabaseValid()) {
log.warn("Database file exists but is invalid or corrupted. Deleting and recreating...");
Files.deleteIfExists(getDatabaseFilePath());
}
This is acceptable because the database is a derived cache — all data can be reconstructed by rescanning the filesystem.
IndexScannerTaskIndexScannerTask.java orchestrates the full indexing pipeline.
On subsequent runs, the scanner compares the last_modified timestamp of each mod folder against the stored last_scanned value. Only changed folders are rescanned.
For each mod folder being scanned, the scanner:
getFilesLastModifiedMap(conn, modId) to get all known files and their timestamps.file.lastModified() against the database value.The entire scan runs in a single SQLite transaction:
conn.setAutoCommit(false);
// ... all upserts ...
conn.commit();
This is critical for performance — without a transaction, each INSERT would trigger a separate WAL flush, making a full index of ~10,000 files take minutes instead of seconds.
PreparedStatement batches are flushed every 500 operations (batchCount % 500 == 0), balancing memory usage against round-trip overhead.
After scanning, deleteOrphanedFiles(conn, modId, activePaths) purges database records for files that no longer exist on disk. The implementation:
file_path values for the mod.activePaths set (built during scanning).LibModFilter (Quirk)Mods identified as "library mods" (e.g., LazyLib, MagicLib) are skipped during scanning. These mods contain no ship/weapon data — only API code — and scanning them wastes time.
The core Starsector game folder is always indexed with the hardcoded mod ID "starsector-core".
On first run or when changes are detected, the scanner prompts the user via JOptionPane (on EDT via SwingUtilities.invokeAndWait). On first run the dialog is informational (OK only). On subsequent runs it's a Yes/No confirmation. In headless mode (e.g., testing), the prompt is skipped and the scan proceeds automatically.
DatabaseQueryServiceDatabaseQueryService.java is a static utility class with no instance state.
upsertMod(), deleteMod(), upsertIndexedFile(), deleteIndexedFile(), deleteOrphanedFiles()deleteOrphanedFiles which takes a shared Connection parameter to participate in the scanner's transaction).getFileNameForEntity(entityId, type) — Returns the filename for an entity.getFilePathForEntity(entityId, type) — Returns the absolute Path for an entity.getFileByPath(path) — Reverse lookup by absolute path.getFilesByType(type) — All indexed files of a given type, ordered by mod_id ASC, entity_id ASC.getFilesByTypeGroupedByMod(type) — Same, but returned as Map<String, List<IndexedFile>>.getFilesByTypeAsync(type) — Returns CompletableFuture<List<IndexedFile>>. Runs the query on ForkJoinPool.commonPool().getFilesByModAndTypeAsync(modId, type) — Filtered by mod.These async methods are used by the UI to populate tree views and tables without blocking the Swing EDT.
IndexedFile RecordIndexedFile.java is a Lombok @Builder POJO. All fields are final — it's an immutable value object. The filePath is stored as a java.nio.file.Path (converted from the stored TEXT column via Path.of()).
The database is co-located with the settings file. DatabaseManager.getDatabaseFilePath() resolves the settings file path, takes its parent directory, and appends ship_editor_database.sqlite. This ensures the database lives alongside the application's configuration, not in a temp directory.