use crate::builder::BuilderError;
use rusqlite::Connection;
use serde_json::Value;
use std::io::BufRead;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ParserConfig {
pub language: String,
pub force_isometric_position_2d: bool,
pub isometric_projected_axis: ProjectedAxis,
pub map_kspace: bool,
pub map_wspace: bool,
pub map_abyssal: bool,
pub map_void: bool,
pub with_gates: bool,
pub with_moons: bool,
}
impl Default for ParserConfig {
fn default() -> Self {
Self {
language: "en".to_string(),
force_isometric_position_2d: false,
isometric_projected_axis: ProjectedAxis::default(),
map_kspace: true,
map_wspace: true,
map_abyssal: true,
map_void: false,
with_gates: true,
with_moons: true,
}
}
}
pub use crate::objects::ProjectedAxis;
pub fn isometric_projection_2d(x: f64, y: f64, z: f64, axis: ProjectedAxis) -> (f64, f64) {
match axis {
ProjectedAxis::Z => (x - z, y + (x + z) / 2.0),
ProjectedAxis::Y => (x - y, z + (x + y) / 2.0),
ProjectedAxis::X => (y - x, z + (y + x) / 2.0),
}
}
fn system_in_scope(wormhole_class_id: Option<i64>, config: &ParserConfig) -> bool {
match wormhole_class_id {
None => config.map_kspace,
Some(_) => config.map_wspace || config.map_abyssal || config.map_void,
}
}
#[derive(Debug, Default)]
pub struct StarTypeState {
pub sun_group_id: Option<i64>,
pub star_type_ids: std::collections::HashMap<i64, i64>,
}
#[derive(Debug, Default)]
pub struct SystemScopeState {
pub systems_in_scope: std::collections::HashSet<i64>,
}
fn iter_jsonl_records(
sde_directory: &Path,
stem: &str,
) -> Result<impl Iterator<Item = Result<Value, BuilderError>>, BuilderError> {
let path = sde_directory.join(format!("{stem}.jsonl"));
let file = std::fs::File::open(&path)?;
let reader = std::io::BufReader::new(file);
Ok(reader.lines().filter_map(|line| match line {
Ok(line) if line.trim().is_empty() => None,
Ok(line) => Some(serde_json::from_str::<Value>(&line).map_err(BuilderError::Json)),
Err(err) => Some(Err(BuilderError::Io(err))),
}))
}
fn localized<'a>(record: &'a Value, field: &str, config: &ParserConfig) -> Option<&'a str> {
match record.get(field) {
Some(Value::Object(map)) => map
.get(config.language.as_str())
.or_else(|| map.get("en"))
.and_then(Value::as_str),
Some(Value::String(s)) => Some(s.as_str()),
_ => None,
}
}
fn required_i64(record: &Value, field: &str) -> Result<i64, BuilderError> {
record.get(field).and_then(Value::as_i64).ok_or_else(|| {
BuilderError::Data(format!(
"record missing required field `{field}` (or it's not an integer): {record}"
))
})
}
fn optional_i64(record: &Value, field: &str) -> Option<i64> {
record.get(field).and_then(Value::as_i64)
}
fn required_localized<'a>(
record: &'a Value,
field: &str,
config: &ParserConfig,
) -> Result<&'a str, BuilderError> {
localized(record, field, config).ok_or_else(|| {
BuilderError::Data(format!(
"record has no localizable field `{field}` in `{}`/`en`: {record}",
config.language
))
})
}
fn optional_bool(record: &Value, field: &str) -> Option<bool> {
record.get(field).and_then(Value::as_bool)
}
fn optional_f64(record: &Value, field: &str) -> Option<f64> {
record.get(field).and_then(Value::as_f64)
}
fn required_str<'a>(record: &'a Value, field: &str) -> Result<&'a str, BuilderError> {
record.get(field).and_then(Value::as_str).ok_or_else(|| {
BuilderError::Data(format!(
"record missing required field `{field}` (or it's not a string): {record}"
))
})
}
fn required_bool(record: &Value, field: &str) -> Result<bool, BuilderError> {
record.get(field).and_then(Value::as_bool).ok_or_else(|| {
BuilderError::Data(format!(
"record missing required field `{field}` (or it's not a boolean): {record}"
))
})
}
fn required_f64(record: &Value, field: &str) -> Result<f64, BuilderError> {
record.get(field).and_then(Value::as_f64).ok_or_else(|| {
BuilderError::Data(format!(
"record missing required field `{field}` (or it's not a number): {record}"
))
})
}
fn optional_i64_array(record: &Value, field: &str) -> Result<Vec<i64>, BuilderError> {
match record.get(field) {
None | Some(Value::Null) => Ok(Vec::new()),
Some(Value::Array(items)) => items
.iter()
.map(|item| {
item.as_i64().ok_or_else(|| {
BuilderError::Data(format!("non-integer element in array `{field}`: {item}"))
})
})
.collect(),
Some(other) => Err(BuilderError::Data(format!(
"field `{field}` is not an array: {other}"
))),
}
}
fn required_position(record: &Value) -> Result<(f64, f64, f64), BuilderError> {
let position = record.get("position").ok_or_else(|| {
BuilderError::Data(format!(
"record missing required field `position`: {record}"
))
})?;
let x = required_f64(position, "x")?;
let y = required_f64(position, "y")?;
let z = required_f64(position, "z")?;
Ok((x, y, z))
}
fn required_nested_i64(record: &Value, outer: &str, inner: &str) -> Result<i64, BuilderError> {
let outer_val = record.get(outer).ok_or_else(|| {
BuilderError::Data(format!("record missing required field `{outer}`: {record}"))
})?;
required_i64(outer_val, inner)
}
fn optional_i64_with_nested_fallback(
record: &Value,
field: &str,
nested_field: &str,
) -> Option<i64> {
optional_i64(record, field).or_else(|| {
record
.get(nested_field)
.and_then(|nested| optional_i64(nested, field))
})
}
fn optional_bool_with_nested_fallback(
record: &Value,
field: &str,
nested_field: &str,
) -> Option<bool> {
optional_bool(record, field).or_else(|| {
record
.get(nested_field)
.and_then(|nested| optional_bool(nested, field))
})
}
fn optional_f64_with_nested_fallback(
record: &Value,
field: &str,
nested_field: &str,
) -> Option<f64> {
optional_f64(record, field).or_else(|| {
record
.get(nested_field)
.and_then(|nested| optional_f64(nested, field))
})
}
fn optional_str<'a>(record: &'a Value, field: &str) -> Option<&'a str> {
record.get(field).and_then(Value::as_str)
}
fn optional_nested_f64(record: &Value, outer: &str, inner: &str) -> Option<f64> {
record.get(outer)?.get(inner).and_then(Value::as_f64)
}
pub fn parse_categories(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<usize, BuilderError> {
let mut insert_category = connection.prepare(
"INSERT INTO invCategories (categoryId, categoryName, published) VALUES (?1, ?2, ?3)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "categories")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let name = required_localized(&record, "name", config)?;
let published = optional_bool(&record, "published");
insert_category.execute(rusqlite::params![id, name, published])?;
count += 1;
}
Ok(count)
}
pub fn parse_groups(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
state: &mut StarTypeState,
) -> Result<usize, BuilderError> {
let mut insert_group = connection.prepare(
"INSERT INTO invGroups (groupId, categoryId, groupName, anchorable) \
VALUES (?1, ?2, ?3, ?4)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "groups")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let category_id = required_i64(&record, "categoryID")?;
let name = required_localized(&record, "name", config)?;
let anchorable = optional_bool(&record, "anchorable");
insert_group.execute(rusqlite::params![id, category_id, name, anchorable])?;
if name == "Sun" {
state.sun_group_id = Some(id);
}
count += 1;
}
Ok(count)
}
fn add_star_type(
connection: &Connection,
type_id: i64,
name: &str,
color: &str,
) -> Result<i64, BuilderError> {
connection.execute(
"INSERT INTO typeStar (typeId, name, color) VALUES (?1, ?2, ?3)",
rusqlite::params![type_id, name, color],
)?;
let star_type_id = connection.query_row(
"SELECT starTypeId FROM typeStar WHERE typeId = ?1",
rusqlite::params![type_id],
|row| row.get(0),
)?;
Ok(star_type_id)
}
pub fn parse_types(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
state: &mut StarTypeState,
) -> Result<usize, BuilderError> {
let mut insert_type = connection.prepare(
"INSERT INTO invTypes (typeId, groupId, typeName, iconId, published, volume) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "types")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let group_id = required_i64(&record, "groupID")?;
let name = required_localized(&record, "name", config)?.to_string();
let icon_id = optional_i64(&record, "iconID");
let published = optional_bool(&record, "published");
let volume = optional_f64(&record, "volume");
insert_type.execute(rusqlite::params![
id, group_id, name, icon_id, published, volume
])?;
if state.sun_group_id == Some(group_id) {
let parts: Vec<&str> = name.split(' ').collect();
if parts.len() >= 3 {
let star_name = parts[1];
let color_token = parts[2];
let color = color_token
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.unwrap_or(color_token);
let star_type_id = add_star_type(connection, id, star_name, color)?;
state.star_type_ids.insert(id, star_type_id);
}
}
count += 1;
}
Ok(count)
}
pub fn parse_races(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<usize, BuilderError> {
let mut insert_race =
connection.prepare("INSERT INTO races (raceId, raceName) VALUES (?1, ?2)")?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "races")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let name = required_localized(&record, "name", config)?;
insert_race.execute(rusqlite::params![id, name])?;
count += 1;
}
Ok(count)
}
pub fn parse_npc_corporations(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<usize, BuilderError> {
let mut insert_corp = connection.prepare(
"INSERT INTO npcCorporations (corporationId, corporationName, tickerName, deleted, iconId, raceId) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "npcCorporations")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let name = required_localized(&record, "name", config)?;
let ticker = required_str(&record, "tickerName")?;
let deleted = required_bool(&record, "deleted")?;
let icon_id = optional_i64(&record, "iconID");
let race_id = optional_i64(&record, "raceID");
insert_corp.execute(rusqlite::params![
id, name, ticker, deleted, icon_id, race_id
])?;
count += 1;
}
Ok(count)
}
pub fn parse_factions(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<usize, BuilderError> {
let mut insert_faction = connection.prepare(
"INSERT INTO factions (factionId, factionName, iconId, sizeFactor, uniqueName, corporationId) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
let mut insert_faction_race =
connection.prepare("INSERT INTO factionRace (factionId, raceId) VALUES (?1, ?2)")?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "factions")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let name = required_localized(&record, "name", config)?;
let icon_id = required_i64(&record, "iconID")?;
let size_factor = required_f64(&record, "sizeFactor")?;
let unique_name = required_bool(&record, "uniqueName")?;
let corporation_id = optional_i64(&record, "corporationID");
let member_races = optional_i64_array(&record, "memberRaces")?;
insert_faction.execute(rusqlite::params![
id,
name,
icon_id,
size_factor,
unique_name,
corporation_id
])?;
for race_id in member_races {
insert_faction_race.execute(rusqlite::params![id, race_id])?;
}
count += 1;
}
Ok(count)
}
pub fn parse_regions(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<usize, BuilderError> {
let mut insert_region = connection.prepare(
"INSERT INTO mapRegions (regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapRegions")? {
let record = record?;
let id = required_i64(&record, "_key")?;
let name = required_localized(&record, "name", config)?;
let faction_id = optional_i64(&record, "factionID");
let nebula = required_i64(&record, "nebulaID")?;
let wormhole_class_id = optional_i64(&record, "wormholeClassID");
let (center_x, center_y, center_z) = required_position(&record)?;
insert_region.execute(rusqlite::params![
id,
name,
faction_id,
center_x,
center_y,
center_z,
nebula,
wormhole_class_id
])?;
count += 1;
}
Ok(count)
}
pub fn parse_constellations(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<usize, BuilderError> {
let mut insert_constellation = connection.prepare(
"INSERT INTO mapConstellations (constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapConstellations")? {
let record = record?;
let id = match optional_i64(&record, "constellationID") {
Some(id) => id,
None => required_i64(&record, "_key")?,
};
let name = required_localized(&record, "name", config)?;
let region_id = required_i64(&record, "regionID")?;
let (center_x, center_y, center_z) = required_position(&record)?;
insert_constellation.execute(rusqlite::params![
id, name, region_id, center_x, center_y, center_z
])?;
count += 1;
}
Ok(count)
}
pub fn parse_solar_systems(
connection: &Connection,
sde_directory: &Path,
config: &ParserConfig,
state: &mut SystemScopeState,
) -> Result<usize, BuilderError> {
let mut insert_system = connection.prepare(
"INSERT INTO mapSolarSystems (solarSystemId, solarSystemName, constellationId, \
corridor, fringe, hub, international, luminosity, radius, centerX, centerY, centerZ, \
regional, security, securityClass, position2DX, position2DY) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapSolarSystems")? {
let record = record?;
let system_id = required_i64(&record, "_key")?;
let wormhole_class_id = optional_i64(&record, "wormholeClassID");
if !system_in_scope(wormhole_class_id, config) {
continue;
}
state.systems_in_scope.insert(system_id);
let name = required_localized(&record, "name", config)?;
let constellation_id = required_i64(&record, "constellationID")?;
let corridor = optional_bool(&record, "corridor");
let fringe = optional_bool(&record, "fringe");
let hub = optional_bool(&record, "hub");
let international = optional_bool(&record, "international");
let luminosity = optional_f64(&record, "luminosity");
let radius = required_f64(&record, "radius")?;
let (center_x, center_y, center_z) = required_position(&record)?;
let regional = optional_bool(&record, "regional");
let security = required_f64(&record, "securityStatus")?;
let security_class = optional_str(&record, "securityClass");
let (position_2d_x, position_2d_y) = if config.force_isometric_position_2d {
let (x2d, y2d) = isometric_projection_2d(
center_x,
center_y,
center_z,
config.isometric_projected_axis,
);
(Some(x2d), Some(y2d))
} else {
(
optional_nested_f64(&record, "position2D", "x"),
optional_nested_f64(&record, "position2D", "y"),
)
};
insert_system.execute(rusqlite::params![
system_id,
name,
constellation_id,
corridor,
fringe,
hub,
international,
luminosity,
radius,
center_x,
center_y,
center_z,
regional,
security,
security_class,
position_2d_x,
position_2d_y,
])?;
count += 1;
}
Ok(count)
}
pub fn parse_stargates(
connection: &Connection,
sde_directory: &Path,
state: &SystemScopeState,
) -> Result<usize, BuilderError> {
let mut insert_gate = connection.prepare(
"INSERT INTO mapSystemGates (systemGateId, solarSystemId, typeId, \
positionX, positionY, positionZ, destinationGateId, destinationSystemId) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapStargates")? {
let record = record?;
let solar_system_id = required_i64(&record, "solarSystemID")?;
if !state.systems_in_scope.contains(&solar_system_id) {
continue;
}
let id = required_i64(&record, "_key")?;
let type_id = required_i64(&record, "typeID")?;
let (pos_x, pos_y, pos_z) = required_position(&record)?;
let destination_gate_id = required_nested_i64(&record, "destination", "stargateID")?;
let destination_system_id = required_nested_i64(&record, "destination", "solarSystemID")?;
insert_gate.execute(rusqlite::params![
id,
solar_system_id,
type_id,
pos_x,
pos_y,
pos_z,
destination_gate_id,
destination_system_id,
])?;
count += 1;
}
Ok(count)
}
pub fn parse_stars(
connection: &Connection,
sde_directory: &Path,
state: &SystemScopeState,
star_state: &StarTypeState,
) -> Result<usize, BuilderError> {
let mut insert_star = connection.prepare(
"INSERT INTO mapStars (starId, solarSystemId, locked, radius, starTypeId) \
VALUES (?1, ?2, ?3, ?4, ?5)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapStars")? {
let record = record?;
let solar_system_id = required_i64(&record, "solarSystemID")?;
if !state.systems_in_scope.contains(&solar_system_id) {
continue;
}
let star_id = required_i64(&record, "_key")?;
let locked = optional_bool_with_nested_fallback(&record, "locked", "statistics");
let radius = optional_i64_with_nested_fallback(&record, "radius", "statistics");
let type_id = required_i64(&record, "typeID")?;
let star_type_id = star_state
.star_type_ids
.get(&type_id)
.copied()
.ok_or_else(|| {
BuilderError::Data(format!(
"star {star_id}: typeId {type_id} isn't in star_type_ids \
(parse_types() didn't detect it as a star type)"
))
})?;
insert_star.execute(rusqlite::params![
star_id,
solar_system_id,
locked,
radius,
star_type_id
])?;
count += 1;
}
Ok(count)
}
pub fn parse_planets(
connection: &Connection,
sde_directory: &Path,
state: &SystemScopeState,
) -> Result<usize, BuilderError> {
let mut insert_planet = connection.prepare(
"INSERT INTO mapPlanets (planetId, solarSystemId, planetaryIndex, fragmented, radius, \
locked, typeId, positionX, positionY, positionZ) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapPlanets")? {
let record = record?;
let solar_system_id = required_i64(&record, "solarSystemID")?;
if !state.systems_in_scope.contains(&solar_system_id) {
continue;
}
let id = required_i64(&record, "_key")?;
let planet_index = required_i64(&record, "celestialIndex")?;
let fragmented = optional_bool_with_nested_fallback(&record, "fragmented", "statistics");
let radius = optional_f64_with_nested_fallback(&record, "radius", "statistics");
let locked = optional_bool_with_nested_fallback(&record, "locked", "statistics");
let type_id = required_i64(&record, "typeID")?;
let (pos_x, pos_y, pos_z) = required_position(&record)?;
insert_planet.execute(rusqlite::params![
id,
solar_system_id,
planet_index,
fragmented,
radius,
locked,
type_id,
pos_x,
pos_y,
pos_z,
])?;
count += 1;
}
Ok(count)
}
pub fn parse_moons(
connection: &Connection,
sde_directory: &Path,
state: &SystemScopeState,
) -> Result<usize, BuilderError> {
let mut insert_moon = connection.prepare(
"INSERT INTO mapMoons (moonId, solarSystemId, moonIndex, planetId, typeId, radius, \
positionX, positionY, positionZ) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
)?;
let mut count = 0usize;
for record in iter_jsonl_records(sde_directory, "mapMoons")? {
let record = record?;
let solar_system_id = required_i64(&record, "solarSystemID")?;
if !state.systems_in_scope.contains(&solar_system_id) {
continue;
}
let id = required_i64(&record, "_key")?;
let moon_index = required_i64(&record, "orbitIndex")?;
let planet_id = optional_i64(&record, "orbitID");
let type_id = required_i64(&record, "typeID")?;
let radius = optional_i64_with_nested_fallback(&record, "radius", "statistics");
let (pos_x, pos_y, pos_z) = required_position(&record)?;
insert_moon.execute(rusqlite::params![
id,
solar_system_id,
moon_index,
planet_id,
type_id,
radius,
pos_x,
pos_y,
pos_z,
])?;
count += 1;
}
Ok(count)
}
pub fn parse_connections(connection: &Connection) -> Result<usize, BuilderError> {
let count = connection.execute(
"INSERT INTO mapSystemConnections (systemA, systemB) \
SELECT MIN(msga.solarSystemId, msgb.solarSystemId), \
MAX(msga.solarSystemId, msgb.solarSystemId) \
FROM mapSystemGates AS msga \
INNER JOIN mapSystemGates AS msgb ON (msgb.systemGateId = msga.destinationGateId) \
WHERE msga.solarSystemId < msgb.solarSystemId",
[],
)?;
Ok(count)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ParseSummary {
pub categories: usize,
pub groups: usize,
pub types: usize,
pub races: usize,
pub npc_corporations: usize,
pub factions: usize,
pub star_types: usize,
pub regions: usize,
pub constellations: usize,
pub solar_systems: usize,
pub stargates: usize,
pub stars: usize,
pub planets: usize,
pub moons: usize,
pub connections: usize,
}
pub fn parse_data(
connection: &mut Connection,
sde_directory: &Path,
config: &ParserConfig,
) -> Result<ParseSummary, BuilderError> {
let tx = connection.transaction()?;
let categories = parse_categories(&tx, sde_directory, config)?;
let mut state = StarTypeState::default();
let groups = parse_groups(&tx, sde_directory, config, &mut state)?;
let types = parse_types(&tx, sde_directory, config, &mut state)?;
let races = parse_races(&tx, sde_directory, config)?;
let npc_corporations = parse_npc_corporations(&tx, sde_directory, config)?;
let factions = parse_factions(&tx, sde_directory, config)?;
let regions = parse_regions(&tx, sde_directory, config)?;
let constellations = parse_constellations(&tx, sde_directory, config)?;
let mut scope = SystemScopeState::default();
let solar_systems = parse_solar_systems(&tx, sde_directory, config, &mut scope)?;
let stargates = if config.with_gates {
parse_stargates(&tx, sde_directory, &scope)?
} else {
0
};
let stars = parse_stars(&tx, sde_directory, &scope, &state)?;
let planets = parse_planets(&tx, sde_directory, &scope)?;
let moons = if config.with_moons {
parse_moons(&tx, sde_directory, &scope)?
} else {
0
};
let connections = parse_connections(&tx)?;
tx.commit()?;
Ok(ParseSummary {
categories,
groups,
types,
races,
npc_corporations,
factions,
star_types: state.star_type_ids.len(),
regions,
constellations,
solar_systems,
stargates,
stars,
planets,
moons,
connections,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
struct TempSdeDir {
path: std::path::PathBuf,
}
impl TempSdeDir {
fn new(test_name: &str, files: &[(&str, &str)]) -> Self {
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
let path = std::env::temp_dir().join(format!(
"sde_parser_test_{}_{}_{}",
test_name,
std::process::id(),
id
));
std::fs::create_dir_all(&path).expect("cannot create temp sde dir");
for (name, content) in files {
std::fs::write(path.join(name), content).expect("cannot write fixture file");
}
Self { path }
}
}
impl Drop for TempSdeDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
#[test]
fn parse_categories_inserts_rows() {
let dir = TempSdeDir::new(
"categories",
&[(
"categories.jsonl",
"{\"_key\": 6, \"name\": {\"en\": \"Ship\"}, \"published\": true}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let count = parse_categories(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 1);
let (name, published): (String, i64) = connection
.query_row(
"SELECT categoryName, published FROM invCategories WHERE categoryId = 6",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(name, "Ship");
assert_eq!(published, 1);
}
#[test]
fn parse_races_inserts_rows() {
let dir = TempSdeDir::new(
"races",
&[(
"races.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Caldari\"}}\n\
{\"_key\": 2, \"name\": {\"en\": \"Minmatar\"}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let count = parse_races(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 2);
let name: String = connection
.query_row("SELECT raceName FROM races WHERE raceId = 1", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(name, "Caldari");
}
#[test]
fn parse_groups_and_types_detect_sun_and_populate_typestar() {
let dir = TempSdeDir::new(
"groups_types",
&[
(
"groups.jsonl",
"{\"_key\": 6, \"categoryID\": 6, \"name\": {\"en\": \"Sun\"}, \"anchorable\": false}\n\
{\"_key\": 7, \"categoryID\": 6, \"name\": {\"en\": \"Frigate\"}, \"anchorable\": false}\n",
),
(
"types.jsonl",
"{\"_key\": 3000, \"groupID\": 6, \"name\": {\"en\": \"Yellow G5 (ffcc00)\"}, \"iconID\": 100, \"published\": true, \"volume\": 0.0}\n\
{\"_key\": 588, \"groupID\": 7, \"name\": {\"en\": \"Rifter\"}, \"iconID\": 200, \"published\": true, \"volume\": 27289.5}\n",
),
],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO invCategories (categoryId, categoryName, published) \
VALUES (6, 'Celestial', 1)",
[],
)
.unwrap();
let config = ParserConfig::default();
let mut state = StarTypeState::default();
let groups = parse_groups(&connection, &dir.path, &config, &mut state).unwrap();
assert_eq!(groups, 2);
assert_eq!(state.sun_group_id, Some(6));
let types = parse_types(&connection, &dir.path, &config, &mut state).unwrap();
assert_eq!(types, 2);
assert_eq!(state.star_type_ids.len(), 1);
let star_type_id = state.star_type_ids[&3000];
let (name, color): (String, String) = connection
.query_row(
"SELECT name, color FROM typeStar WHERE starTypeId = ?1",
rusqlite::params![star_type_id],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(name, "G5");
assert_eq!(color, "ffcc00");
let total_star_types: i64 = connection
.query_row("SELECT COUNT(*) FROM typeStar", [], |row| row.get(0))
.unwrap();
assert_eq!(total_star_types, 1);
}
#[test]
fn parse_categories_missing_required_key_errors() {
let dir = TempSdeDir::new(
"missing_key",
&[("categories.jsonl", "{\"name\": {\"en\": \"Ship\"}}\n")],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_categories(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_categories_missing_name_errors() {
let dir = TempSdeDir::new("missing_name", &[("categories.jsonl", "{\"_key\": 6}\n")]);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_categories(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_categories_missing_file_errors() {
let dir = TempSdeDir::new("missing_file", &[]);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_categories(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn localized_falls_back_to_english() {
let config = ParserConfig {
language: "fr".to_string(),
..Default::default()
};
let record: Value =
serde_json::from_str(r#"{"name": {"en": "Jita", "de": "Jita"}}"#).unwrap();
assert_eq!(localized(&record, "name", &config), Some("Jita"));
}
#[test]
fn localized_uses_requested_language_when_present() {
let config = ParserConfig {
language: "de".to_string(),
..Default::default()
};
let record: Value =
serde_json::from_str(r#"{"name": {"en": "Jita", "de": "Jita (de)"}}"#).unwrap();
assert_eq!(localized(&record, "name", &config), Some("Jita (de)"));
}
#[test]
fn isometric_projection_2d_matches_python_reference_values() {
let (x, y, z) = (100.0, 200.0, 300.0);
assert_eq!(
isometric_projection_2d(x, y, z, ProjectedAxis::X),
(100.0, 450.0)
);
assert_eq!(
isometric_projection_2d(x, y, z, ProjectedAxis::Y),
(-100.0, 450.0)
);
assert_eq!(
isometric_projection_2d(x, y, z, ProjectedAxis::Z),
(-200.0, 400.0)
);
}
#[test]
fn parser_config_default_uses_y_axis_and_does_not_force_isometric() {
let config = ParserConfig::default();
assert!(!config.force_isometric_position_2d);
assert_eq!(config.isometric_projected_axis, ProjectedAxis::Y);
}
#[test]
fn parse_npc_corporations_inserts_rows() {
let dir = TempSdeDir::new(
"npc_corporations",
&[(
"npcCorporations.jsonl",
"{\"_key\": 1000004, \"name\": {\"en\": \"CBD Corporation\"}, \
\"tickerName\": \"CBD\", \"deleted\": false, \"iconID\": 500, \"raceID\": 1}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO races (raceId, raceName) VALUES (1, 'Caldari')",
[],
)
.unwrap();
let config = ParserConfig::default();
let count = parse_npc_corporations(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 1);
let (name, ticker, deleted, icon_id, race_id): (String, String, i64, i64, i64) = connection
.query_row(
"SELECT corporationName, tickerName, deleted, iconId, raceId \
FROM npcCorporations WHERE corporationId = 1000004",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.unwrap();
assert_eq!(name, "CBD Corporation");
assert_eq!(ticker, "CBD");
assert_eq!(deleted, 0);
assert_eq!(icon_id, 500);
assert_eq!(race_id, 1);
}
#[test]
fn parse_npc_corporations_missing_ticker_errors() {
let dir = TempSdeDir::new(
"npc_corp_missing_ticker",
&[(
"npcCorporations.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Test\"}, \"deleted\": false}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_npc_corporations(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_factions_inserts_faction_and_member_races() {
let dir = TempSdeDir::new(
"factions",
&[(
"factions.jsonl",
"{\"_key\": 500001, \"name\": {\"en\": \"Caldari State\"}, \"iconID\": 600, \
\"sizeFactor\": 3.0, \"uniqueName\": true, \"corporationID\": 1000004, \
\"memberRaces\": [1]}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO races (raceId, raceName) VALUES (1, 'Caldari')",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO npcCorporations \
(corporationId, corporationName, tickerName, deleted, iconId, raceId) \
VALUES (1000004, 'CBD Corporation', 'CBD', 0, 500, 1)",
[],
)
.unwrap();
let config = ParserConfig::default();
let count = parse_factions(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 1);
let (name, icon_id, size_factor, unique_name, corporation_id): (
String,
i64,
f64,
i64,
i64,
) = connection
.query_row(
"SELECT factionName, iconId, sizeFactor, uniqueName, corporationId \
FROM factions WHERE factionId = 500001",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.unwrap();
assert_eq!(name, "Caldari State");
assert_eq!(icon_id, 600);
assert_eq!(size_factor, 3.0);
assert_eq!(unique_name, 1);
assert_eq!(corporation_id, 1000004);
let member_race: i64 = connection
.query_row(
"SELECT raceId FROM factionRace WHERE factionId = 500001",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(member_race, 1);
}
#[test]
fn parse_factions_without_member_races_inserts_faction_only() {
let dir = TempSdeDir::new(
"factions_no_members",
&[(
"factions.jsonl",
"{\"_key\": 500002, \"name\": {\"en\": \"Minmatar Republic\"}, \"iconID\": 601, \
\"sizeFactor\": 2.5, \"uniqueName\": true}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let count = parse_factions(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 1);
let total_faction_race: i64 = connection
.query_row("SELECT COUNT(*) FROM factionRace", [], |row| row.get(0))
.unwrap();
assert_eq!(total_faction_race, 0);
}
#[test]
fn parse_factions_with_non_integer_member_race_errors() {
let dir = TempSdeDir::new(
"factions_bad_members",
&[(
"factions.jsonl",
"{\"_key\": 500003, \"name\": {\"en\": \"Bad Faction\"}, \"iconID\": 602, \
\"sizeFactor\": 1.0, \"uniqueName\": false, \"memberRaces\": [1, \"oops\"]}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_factions(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_factions_missing_size_factor_errors() {
let dir = TempSdeDir::new(
"factions_missing_size_factor",
&[(
"factions.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Test\"}, \"iconID\": 1, \"uniqueName\": true}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_factions(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_regions_inserts_rows_with_default_max_proj() {
let dir = TempSdeDir::new(
"regions",
&[(
"mapRegions.jsonl",
"{\"_key\": 10000002, \"name\": {\"en\": \"The Forge\"}, \"nebulaID\": 5, \
\"position\": {\"x\": 100.0, \"y\": 200.0, \"z\": 300.0}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let count = parse_regions(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 1);
let (name, faction_id, cx, cy, cz, nebula, wh_class, max_x, max_y): (
String,
Option<i64>,
f64,
f64,
f64,
i64,
Option<i64>,
f64,
f64,
) = connection
.query_row(
"SELECT regionName, factionId, centerX, centerY, centerZ, nebula, \
wormholeClassId, maxProjX, maxProjY FROM mapRegions WHERE regionId = 10000002",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
row.get(6)?,
row.get(7)?,
row.get(8)?,
))
},
)
.unwrap();
assert_eq!(name, "The Forge");
assert_eq!(faction_id, None);
assert_eq!((cx, cy, cz), (100.0, 200.0, 300.0));
assert_eq!(nebula, 5);
assert_eq!(wh_class, None);
assert_eq!((max_x, max_y), (0.0, 0.0));
}
#[test]
fn parse_regions_missing_nebula_errors() {
let dir = TempSdeDir::new(
"regions_missing_nebula",
&[(
"mapRegions.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Test\"}, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_regions(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_regions_missing_position_errors() {
let dir = TempSdeDir::new(
"regions_missing_position",
&[(
"mapRegions.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Test\"}, \"nebulaID\": 0}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_regions(&connection, &dir.path, &config);
assert!(result.is_err());
}
#[test]
fn parse_constellations_falls_back_to_key_when_constellation_id_absent() {
let dir = TempSdeDir::new(
"constellations_fallback",
&[(
"mapConstellations.jsonl",
"{\"_key\": 20000020, \"name\": {\"en\": \"Kimotoro\"}, \"regionID\": 10000002, \
\"position\": {\"x\": 110.0, \"y\": 210.0, \"z\": 310.0}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
let config = ParserConfig::default();
let count = parse_constellations(&connection, &dir.path, &config).unwrap();
assert_eq!(count, 1);
let (id, name, region_id): (i64, String, i64) = connection
.query_row(
"SELECT constellationId, constellationName, regionId FROM mapConstellations",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
assert_eq!(id, 20000020);
assert_eq!(name, "Kimotoro");
assert_eq!(region_id, 10000002);
}
#[test]
fn parse_constellations_prefers_constellation_id_when_present() {
let dir = TempSdeDir::new(
"constellations_prefer_id",
&[(
"mapConstellations.jsonl",
"{\"_key\": 999, \"constellationID\": 20000020, \"name\": {\"en\": \"Kimotoro\"}, \
\"regionID\": 10000002, \"position\": {\"x\": 1.0, \"y\": 2.0, \"z\": 3.0}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
let config = ParserConfig::default();
parse_constellations(&connection, &dir.path, &config).unwrap();
let id: i64 = connection
.query_row("SELECT constellationId FROM mapConstellations", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(id, 20000020);
}
#[test]
fn system_in_scope_kspace_gates_on_map_kspace() {
let mut config = ParserConfig::default();
assert!(system_in_scope(None, &config)); config.map_kspace = false;
assert!(!system_in_scope(None, &config));
}
#[test]
fn system_in_scope_wormhole_gates_on_any_of_three_flags() {
let mut config = ParserConfig {
map_wspace: false,
map_abyssal: false,
map_void: false,
..Default::default()
};
assert!(!system_in_scope(Some(5), &config));
config.map_wspace = true;
assert!(system_in_scope(Some(5), &config));
}
#[test]
fn parse_solar_systems_inserts_kspace_system_with_ccp_position2d() {
let dir = TempSdeDir::new(
"solar_systems_kspace",
&[(
"mapSolarSystems.jsonl",
"{\"_key\": 30000142, \"name\": {\"en\": \"Jita\"}, \"constellationID\": 20000020, \
\"radius\": 999999999.0, \"position\": {\"x\": -100.0, \"y\": 200.0, \"z\": -300.0}, \
\"securityStatus\": 0.9459, \"securityClass\": \"B\", \"corridor\": false, \
\"fringe\": false, \"hub\": true, \"international\": true, \"regional\": true, \
\"luminosity\": 0.049, \"position2D\": {\"x\": 12.5, \"y\": -7.25}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
let config = ParserConfig::default();
let mut scope = SystemScopeState::default();
let count = parse_solar_systems(&connection, &dir.path, &config, &mut scope).unwrap();
assert_eq!(count, 1);
assert!(scope.systems_in_scope.contains(&30000142));
let (name, security, security_class, p2dx, p2dy): (String, f64, String, f64, f64) =
connection
.query_row(
"SELECT solarSystemName, security, securityClass, \
position2DX, position2DY FROM mapSolarSystems WHERE solarSystemId = 30000142",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.unwrap();
assert_eq!(name, "Jita");
assert_eq!(security, 0.9459);
assert_eq!(security_class, "B");
assert_eq!((p2dx, p2dy), (12.5, -7.25));
}
#[test]
fn parse_solar_systems_force_isometric_ignores_ccp_position2d() {
let dir = TempSdeDir::new(
"solar_systems_force_isometric",
&[(
"mapSolarSystems.jsonl",
"{\"_key\": 30000142, \"name\": {\"en\": \"Jita\"}, \"constellationID\": 20000020, \
\"radius\": 1.0, \"position\": {\"x\": -100.0, \"y\": 200.0, \"z\": -300.0}, \
\"securityStatus\": 0.9459, \"position2D\": {\"x\": 12.5, \"y\": -7.25}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
let config = ParserConfig {
force_isometric_position_2d: true,
..Default::default()
};
let mut scope = SystemScopeState::default();
parse_solar_systems(&connection, &dir.path, &config, &mut scope).unwrap();
let (p2dx, p2dy): (f64, f64) = connection
.query_row(
"SELECT position2DX, position2DY FROM mapSolarSystems WHERE solarSystemId = 30000142",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!((p2dx, p2dy), (-300.0, -250.0));
}
#[test]
fn parse_solar_systems_excludes_out_of_scope_systems() {
let dir = TempSdeDir::new(
"solar_systems_scope",
&[(
"mapSolarSystems.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"KSpace\"}, \"constellationID\": 20000020, \
\"radius\": 1.0, \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \
\"securityStatus\": 0.5}\n\
{\"_key\": 2, \"name\": {\"en\": \"WSpace\"}, \"constellationID\": 20000020, \
\"wormholeClassID\": 5, \"radius\": 1.0, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \"securityStatus\": -1.0}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
let config = ParserConfig {
map_kspace: false,
..Default::default()
};
let mut scope = SystemScopeState::default();
let count = parse_solar_systems(&connection, &dir.path, &config, &mut scope).unwrap();
assert_eq!(count, 1);
assert!(!scope.systems_in_scope.contains(&1));
assert!(scope.systems_in_scope.contains(&2));
let total: i64 = connection
.query_row("SELECT COUNT(*) FROM mapSolarSystems", [], |row| row.get(0))
.unwrap();
assert_eq!(total, 1);
}
#[test]
fn parse_solar_systems_missing_radius_errors() {
let dir = TempSdeDir::new(
"solar_systems_missing_radius",
&[(
"mapSolarSystems.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Test\"}, \"constellationID\": 20000020, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \"securityStatus\": 0.5}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
let config = ParserConfig::default();
let mut scope = SystemScopeState::default();
let result = parse_solar_systems(&connection, &dir.path, &config, &mut scope);
assert!(result.is_err());
}
fn insert_stargate_prerequisites(connection: &Connection) {
connection
.execute(
"INSERT INTO invCategories (categoryId, categoryName, published) \
VALUES (1, 'Celestial', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invGroups (groupId, categoryId, groupName, anchorable) \
VALUES (1, 1, 'Stargate Group', 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invTypes (typeId, groupId, typeName, published) \
VALUES (16, 1, 'Stargate', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
for (id, name) in [(30000001, "A"), (30000002, "B")] {
connection
.execute(
"INSERT INTO mapSolarSystems \
(solarSystemId, solarSystemName, constellationId, radius, centerX, centerY, centerZ, security) \
VALUES (?1, ?2, 20000020, 1.0, 0, 0, 0, 0.5)",
rusqlite::params![id, name],
)
.unwrap();
}
}
const MUTUAL_STARGATES_JSONL: &str = "{\"_key\": 50000001, \"solarSystemID\": 30000001, \"typeID\": 16, \
\"position\": {\"x\": 1.0, \"y\": 2.0, \"z\": 3.0}, \
\"destination\": {\"stargateID\": 50000002, \"solarSystemID\": 30000002}}\n\
{\"_key\": 50000002, \"solarSystemID\": 30000002, \"typeID\": 16, \
\"position\": {\"x\": 4.0, \"y\": 5.0, \"z\": 6.0}, \
\"destination\": {\"stargateID\": 50000001, \"solarSystemID\": 30000001}}\n";
#[test]
fn parse_stargates_without_transaction_fails_on_mutual_reference() {
let dir = TempSdeDir::new(
"stargates_no_tx",
&[("mapStargates.jsonl", MUTUAL_STARGATES_JSONL)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
insert_stargate_prerequisites(&connection);
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
scope.systems_in_scope.insert(30000002);
let result = parse_stargates(&connection, &dir.path, &scope);
assert!(result.is_err());
}
#[test]
fn parse_stargates_within_transaction_inserts_mutual_reference() {
let dir = TempSdeDir::new(
"stargates_tx",
&[("mapStargates.jsonl", MUTUAL_STARGATES_JSONL)],
);
let mut connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
insert_stargate_prerequisites(&connection);
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
scope.systems_in_scope.insert(30000002);
let tx = connection.transaction().unwrap();
let count = parse_stargates(&tx, &dir.path, &scope).unwrap();
assert_eq!(count, 2);
tx.commit().unwrap();
let (dest_gate, dest_system): (i64, i64) = connection
.query_row(
"SELECT destinationGateId, destinationSystemId FROM mapSystemGates \
WHERE systemGateId = 50000001",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(dest_gate, 50000002);
assert_eq!(dest_system, 30000002);
}
#[test]
fn parse_stargates_skips_systems_outside_scope() {
let dir = TempSdeDir::new(
"stargates_scope",
&[(
"mapStargates.jsonl",
"{\"_key\": 50000003, \"solarSystemID\": 30000003, \"typeID\": 16, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \
\"destination\": {\"stargateID\": 50000004, \"solarSystemID\": 30000001}}\n",
)],
);
let mut connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
insert_stargate_prerequisites(&connection);
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
scope.systems_in_scope.insert(30000002);
let tx = connection.transaction().unwrap();
let count = parse_stargates(&tx, &dir.path, &scope).unwrap();
tx.commit().unwrap();
assert_eq!(count, 0);
let total: i64 = connection
.query_row("SELECT COUNT(*) FROM mapSystemGates", [], |row| row.get(0))
.unwrap();
assert_eq!(total, 0);
}
#[test]
fn parse_stargates_missing_type_id_errors() {
let dir = TempSdeDir::new(
"stargates_missing_type",
&[(
"mapStargates.jsonl",
"{\"_key\": 50000001, \"solarSystemID\": 30000001, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \
\"destination\": {\"stargateID\": 50000002, \"solarSystemID\": 30000002}}\n",
)],
);
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
insert_stargate_prerequisites(&connection);
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
let result = parse_stargates(&connection, &dir.path, &scope);
assert!(result.is_err());
}
fn setup_for_parse_stars(dir_prefix: &str) -> (Connection, StarTypeState, SystemScopeState) {
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let types_dir = TempSdeDir::new(
dir_prefix,
&[
(
"groups.jsonl",
"{\"_key\": 6, \"categoryID\": 6, \"name\": {\"en\": \"Sun\"}, \"anchorable\": false}\n",
),
(
"types.jsonl",
"{\"_key\": 3000, \"groupID\": 6, \"name\": {\"en\": \"Yellow G5 (ffcc00)\"}, \
\"iconID\": 100, \"published\": true, \"volume\": 0.0}\n",
),
],
);
connection
.execute(
"INSERT INTO invCategories (categoryId, categoryName, published) \
VALUES (6, 'Celestial', 1)",
[],
)
.unwrap();
let config = ParserConfig::default();
let mut star_state = StarTypeState::default();
parse_groups(&connection, &types_dir.path, &config, &mut star_state).unwrap();
parse_types(&connection, &types_dir.path, &config, &mut star_state).unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapSolarSystems \
(solarSystemId, solarSystemName, constellationId, radius, centerX, centerY, centerZ, security) \
VALUES (30000001, 'A', 20000020, 1.0, 0, 0, 0, 0.5)",
[],
)
.unwrap();
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
(connection, star_state, scope)
}
#[test]
fn parse_stars_inserts_row_using_real_sde_shape() {
let dir = TempSdeDir::new(
"stars_real_shape",
&[(
"mapStars.jsonl",
"{\"_key\": 40000001, \"radius\": 63350000, \"solarSystemID\": 30000001, \
\"statistics\": {\"age\": 4.5e17, \"life\": 6.9e17, \"luminosity\": 0.01575, \
\"spectralClass\": \"K2 V\", \"temperature\": 4567.0}, \"typeID\": 3000}\n",
)],
);
let (connection, star_state, scope) = setup_for_parse_stars("stars_setup_real");
let count = parse_stars(&connection, &dir.path, &scope, &star_state).unwrap();
assert_eq!(count, 1);
let (solar_system_id, locked, radius): (i64, Option<i64>, i64) = connection
.query_row(
"SELECT solarSystemId, locked, radius FROM mapStars WHERE starId = 40000001",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
assert_eq!(solar_system_id, 30000001);
assert_eq!(locked, None);
assert_eq!(radius, 63350000);
}
#[test]
fn parse_stars_locked_falls_back_to_nested_statistics() {
let dir = TempSdeDir::new(
"stars_locked_fallback",
&[(
"mapStars.jsonl",
"{\"_key\": 40000001, \"solarSystemID\": 30000001, \"typeID\": 3000, \
\"statistics\": {\"locked\": true}}\n",
)],
);
let (connection, star_state, scope) = setup_for_parse_stars("stars_setup_fallback");
parse_stars(&connection, &dir.path, &scope, &star_state).unwrap();
let locked: Option<i64> = connection
.query_row(
"SELECT locked FROM mapStars WHERE starId = 40000001",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(locked, Some(1));
}
#[test]
fn parse_stars_skips_systems_outside_scope() {
let dir = TempSdeDir::new(
"stars_scope",
&[(
"mapStars.jsonl",
"{\"_key\": 40000001, \"radius\": 1, \"solarSystemID\": 30000099, \"typeID\": 3000}\n",
)],
);
let (connection, star_state, scope) = setup_for_parse_stars("stars_setup_scope");
let count = parse_stars(&connection, &dir.path, &scope, &star_state).unwrap();
assert_eq!(count, 0);
let total: i64 = connection
.query_row("SELECT COUNT(*) FROM mapStars", [], |row| row.get(0))
.unwrap();
assert_eq!(total, 0);
}
#[test]
fn parse_stars_unknown_star_type_errors() {
let dir = TempSdeDir::new(
"stars_unknown_type",
&[(
"mapStars.jsonl",
"{\"_key\": 40000001, \"radius\": 1, \"solarSystemID\": 30000001, \"typeID\": 9999}\n",
)],
);
let (connection, star_state, scope) = setup_for_parse_stars("stars_setup_unknown");
let result = parse_stars(&connection, &dir.path, &scope, &star_state);
assert!(result.is_err());
}
fn setup_for_parse_planets() -> (Connection, SystemScopeState) {
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO invCategories (categoryId, categoryName, published) \
VALUES (1, 'Celestial', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invGroups (groupId, categoryId, groupName, anchorable) \
VALUES (1, 1, 'Planet', 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invTypes (typeId, groupId, typeName, published) \
VALUES (11, 1, 'Planet (Barren)', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapSolarSystems \
(solarSystemId, solarSystemName, constellationId, radius, centerX, centerY, centerZ, security) \
VALUES (30000001, 'A', 20000020, 1.0, 0, 0, 0, 0.5)",
[],
)
.unwrap();
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
(connection, scope)
}
#[test]
fn parse_planets_inserts_row_using_real_sde_shape() {
let dir = TempSdeDir::new(
"planets_real_shape",
&[(
"mapPlanets.jsonl",
"{\"_key\": 40000002, \"celestialIndex\": 1, \
\"position\": {\"x\": 161891117336.0, \"y\": 21288951986.0, \"z\": -73529712226.0}, \
\"radius\": 5060000, \"solarSystemID\": 30000001, \
\"statistics\": {\"locked\": false}, \"typeID\": 11}\n",
)],
);
let (connection, scope) = setup_for_parse_planets();
let count = parse_planets(&connection, &dir.path, &scope).unwrap();
assert_eq!(count, 1);
let (planetary_index, fragmented, radius, locked, type_id): (
i64,
Option<i64>,
f64,
i64,
i64,
) = connection
.query_row(
"SELECT planetaryIndex, fragmented, radius, locked, typeId \
FROM mapPlanets WHERE planetId = 40000002",
[],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
))
},
)
.unwrap();
assert_eq!(planetary_index, 1);
assert_eq!(fragmented, None);
assert_eq!(radius, 5060000.0);
assert_eq!(locked, 0);
assert_eq!(type_id, 11);
}
#[test]
fn parse_planets_skips_systems_outside_scope() {
let dir = TempSdeDir::new(
"planets_scope",
&[(
"mapPlanets.jsonl",
"{\"_key\": 40000002, \"celestialIndex\": 1, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \
\"radius\": 1, \"solarSystemID\": 30000099, \"typeID\": 11}\n",
)],
);
let (connection, scope) = setup_for_parse_planets();
let count = parse_planets(&connection, &dir.path, &scope).unwrap();
assert_eq!(count, 0);
let total: i64 = connection
.query_row("SELECT COUNT(*) FROM mapPlanets", [], |row| row.get(0))
.unwrap();
assert_eq!(total, 0);
}
#[test]
fn parse_planets_missing_celestial_index_errors() {
let dir = TempSdeDir::new(
"planets_missing_index",
&[(
"mapPlanets.jsonl",
"{\"_key\": 40000002, \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \
\"radius\": 1, \"solarSystemID\": 30000001, \"typeID\": 11}\n",
)],
);
let (connection, scope) = setup_for_parse_planets();
let result = parse_planets(&connection, &dir.path, &scope);
assert!(result.is_err());
}
#[test]
fn parse_planets_missing_position_errors() {
let dir = TempSdeDir::new(
"planets_missing_position",
&[(
"mapPlanets.jsonl",
"{\"_key\": 40000002, \"celestialIndex\": 1, \
\"radius\": 1, \"solarSystemID\": 30000001, \"typeID\": 11}\n",
)],
);
let (connection, scope) = setup_for_parse_planets();
let result = parse_planets(&connection, &dir.path, &scope);
assert!(result.is_err());
}
fn setup_for_parse_moons() -> (Connection, SystemScopeState) {
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO invCategories (categoryId, categoryName, published) \
VALUES (1, 'Celestial', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invGroups (groupId, categoryId, groupName, anchorable) \
VALUES (1, 1, 'Celestial Group', 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invTypes (typeId, groupId, typeName, published) \
VALUES (11, 1, 'Planet (Barren)', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invTypes (typeId, groupId, typeName, published) \
VALUES (12, 1, 'Moon', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapSolarSystems \
(solarSystemId, solarSystemName, constellationId, radius, centerX, centerY, centerZ, security) \
VALUES (30000001, 'A', 20000020, 1.0, 0, 0, 0, 0.5)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapPlanets \
(planetId, solarSystemId, planetaryIndex, typeId, positionX, positionY, positionZ) \
VALUES (40000002, 30000001, 1, 11, 0, 0, 0)",
[],
)
.unwrap();
let mut scope = SystemScopeState::default();
scope.systems_in_scope.insert(30000001);
(connection, scope)
}
#[test]
fn parse_moons_inserts_row_with_planet_reference() {
let dir = TempSdeDir::new(
"moons_with_planet",
&[(
"mapMoons.jsonl",
"{\"_key\": 40000004, \"solarSystemID\": 30000001, \"orbitIndex\": 1, \
\"orbitID\": 40000002, \"typeID\": 12, \"radius\": 100000, \
\"position\": {\"x\": 1.0, \"y\": 2.0, \"z\": 3.0}}\n",
)],
);
let (connection, scope) = setup_for_parse_moons();
let count = parse_moons(&connection, &dir.path, &scope).unwrap();
assert_eq!(count, 1);
let (moon_index, planet_id, type_id, radius): (i64, Option<i64>, i64, Option<i64>) =
connection
.query_row(
"SELECT moonIndex, planetId, typeId, radius FROM mapMoons WHERE moonId = 40000004",
[],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)
.unwrap();
assert_eq!(moon_index, 1);
assert_eq!(planet_id, Some(40000002));
assert_eq!(type_id, 12);
assert_eq!(radius, Some(100000));
}
#[test]
fn parse_moons_without_orbit_id_leaves_planet_id_null() {
let dir = TempSdeDir::new(
"moons_no_planet",
&[(
"mapMoons.jsonl",
"{\"_key\": 40000005, \"solarSystemID\": 30000001, \"orbitIndex\": 2, \
\"typeID\": 12, \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\n",
)],
);
let (connection, scope) = setup_for_parse_moons();
parse_moons(&connection, &dir.path, &scope).unwrap();
let planet_id: Option<i64> = connection
.query_row(
"SELECT planetId FROM mapMoons WHERE moonId = 40000005",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(planet_id, None);
}
#[test]
fn parse_moons_skips_systems_outside_scope() {
let dir = TempSdeDir::new(
"moons_scope",
&[(
"mapMoons.jsonl",
"{\"_key\": 40000004, \"solarSystemID\": 30000099, \"orbitIndex\": 1, \
\"typeID\": 12, \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\n",
)],
);
let (connection, scope) = setup_for_parse_moons();
let count = parse_moons(&connection, &dir.path, &scope).unwrap();
assert_eq!(count, 0);
let total: i64 = connection
.query_row("SELECT COUNT(*) FROM mapMoons", [], |row| row.get(0))
.unwrap();
assert_eq!(total, 0);
}
#[test]
fn parse_moons_missing_orbit_index_errors() {
let dir = TempSdeDir::new(
"moons_missing_index",
&[(
"mapMoons.jsonl",
"{\"_key\": 40000004, \"solarSystemID\": 30000001, \"typeID\": 12, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\n",
)],
);
let (connection, scope) = setup_for_parse_moons();
let result = parse_moons(&connection, &dir.path, &scope);
assert!(result.is_err());
}
#[test]
fn parse_moons_missing_type_id_errors() {
let dir = TempSdeDir::new(
"moons_missing_type",
&[(
"mapMoons.jsonl",
"{\"_key\": 40000004, \"solarSystemID\": 30000001, \"orbitIndex\": 1, \
\"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\n",
)],
);
let (connection, scope) = setup_for_parse_moons();
let result = parse_moons(&connection, &dir.path, &scope);
assert!(result.is_err());
}
#[test]
fn parse_connections_derives_single_pair_from_mutual_gates() {
let mut connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
connection
.execute(
"INSERT INTO invCategories (categoryId, categoryName, published) \
VALUES (1, 'Celestial', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invGroups (groupId, categoryId, groupName, anchorable) \
VALUES (1, 1, 'Stargate Group', 0)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO invTypes (typeId, groupId, typeName, published) \
VALUES (16, 1, 'Stargate', 1)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapRegions \
(regionId, regionName, factionId, centerX, centerY, centerZ, nebula, wormholeClassId) \
VALUES (10000002, 'The Forge', NULL, 0, 0, 0, 5, NULL)",
[],
)
.unwrap();
connection
.execute(
"INSERT INTO mapConstellations \
(constellationId, constellationName, regionId, centerX, centerY, centerZ) \
VALUES (20000020, 'Kimotoro', 10000002, 0, 0, 0)",
[],
)
.unwrap();
for (id, name) in [(30000002, "B"), (30000001, "A")] {
connection
.execute(
"INSERT INTO mapSolarSystems \
(solarSystemId, solarSystemName, constellationId, radius, centerX, centerY, centerZ, security) \
VALUES (?1, ?2, 20000020, 1.0, 0, 0, 0, 0.5)",
rusqlite::params![id, name],
)
.unwrap();
}
{
let tx = connection.transaction().unwrap();
tx.execute(
"INSERT INTO mapSystemGates \
(systemGateId, solarSystemId, typeId, positionX, positionY, positionZ, destinationGateId, destinationSystemId) \
VALUES (50000001, 30000002, 16, 0, 0, 0, 50000002, 30000001)",
[],
)
.unwrap();
tx.execute(
"INSERT INTO mapSystemGates \
(systemGateId, solarSystemId, typeId, positionX, positionY, positionZ, destinationGateId, destinationSystemId) \
VALUES (50000002, 30000001, 16, 0, 0, 0, 50000001, 30000002)",
[],
)
.unwrap();
tx.commit().unwrap();
}
let count = parse_connections(&connection).unwrap();
assert_eq!(count, 1);
let (system_a, system_b): (i64, i64) = connection
.query_row(
"SELECT systemA, systemB FROM mapSystemConnections",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!((system_a, system_b), (30000001, 30000002));
}
#[test]
fn parse_connections_returns_zero_when_no_gates() {
let connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let count = parse_connections(&connection).unwrap();
assert_eq!(count, 0);
let total: i64 = connection
.query_row("SELECT COUNT(*) FROM mapSystemConnections", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(total, 0);
}
#[test]
fn parse_data_happy_path_returns_summary_and_commits() {
let dir = TempSdeDir::new(
"parse_data_happy",
&[
(
"categories.jsonl",
"{\"_key\": 6, \"name\": {\"en\": \"Celestial\"}, \"published\": true}\n",
),
(
"groups.jsonl",
"{\"_key\": 6, \"categoryID\": 6, \"name\": {\"en\": \"Sun\"}, \"anchorable\": false}\n\
{\"_key\": 7, \"categoryID\": 6, \"name\": {\"en\": \"Frigate\"}, \"anchorable\": false}\n",
),
(
"races.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Caldari\"}}\n",
),
(
"npcCorporations.jsonl",
"{\"_key\": 1000004, \"name\": {\"en\": \"CBD Corporation\"}, \
\"tickerName\": \"CBD\", \"deleted\": false, \"iconID\": 500, \"raceID\": 1}\n",
),
(
"factions.jsonl",
"{\"_key\": 500001, \"name\": {\"en\": \"Caldari State\"}, \"iconID\": 600, \
\"sizeFactor\": 3.0, \"uniqueName\": true, \"corporationID\": 1000004, \
\"memberRaces\": [1]}\n",
),
(
"mapRegions.jsonl",
"{\"_key\": 10000002, \"name\": {\"en\": \"The Forge\"}, \"nebulaID\": 5, \
\"position\": {\"x\": 100.0, \"y\": 200.0, \"z\": 300.0}}\n",
),
(
"mapConstellations.jsonl",
"{\"_key\": 20000020, \"name\": {\"en\": \"Kimotoro\"}, \"regionID\": 10000002, \
\"position\": {\"x\": 110.0, \"y\": 210.0, \"z\": 310.0}}\n",
),
(
"mapSolarSystems.jsonl",
"{\"_key\": 30000142, \"name\": {\"en\": \"Jita\"}, \"constellationID\": 20000020, \
\"radius\": 999999999.0, \"position\": {\"x\": -100.0, \"y\": 200.0, \"z\": -300.0}, \
\"securityStatus\": 0.9459, \"securityClass\": \"B\", \"corridor\": false, \
\"fringe\": false, \"hub\": true, \"international\": true, \"regional\": true, \
\"luminosity\": 0.049, \"position2D\": {\"x\": 12.5, \"y\": -7.25}}\n\
{\"_key\": 30002187, \"name\": {\"en\": \"Perimeter\"}, \"constellationID\": 20000020, \
\"radius\": 1.0, \"position\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}, \
\"securityStatus\": 0.9}\n",
),
(
"mapStargates.jsonl",
"{\"_key\": 50000001, \"solarSystemID\": 30000142, \"typeID\": 16, \
\"position\": {\"x\": 1.0, \"y\": 2.0, \"z\": 3.0}, \
\"destination\": {\"stargateID\": 50000002, \"solarSystemID\": 30002187}}\n\
{\"_key\": 50000002, \"solarSystemID\": 30002187, \"typeID\": 16, \
\"position\": {\"x\": 4.0, \"y\": 5.0, \"z\": 6.0}, \
\"destination\": {\"stargateID\": 50000001, \"solarSystemID\": 30000142}}\n",
),
(
"mapStars.jsonl",
"{\"_key\": 40000001, \"radius\": 63350000, \"solarSystemID\": 30000142, \
\"statistics\": {\"age\": 4.5e17, \"life\": 6.9e17, \"luminosity\": 0.01575, \
\"spectralClass\": \"K2 V\", \"temperature\": 4567.0}, \"typeID\": 3000}\n",
),
(
"mapPlanets.jsonl",
"{\"_key\": 40000002, \"celestialIndex\": 1, \
\"position\": {\"x\": 161891117336.0, \"y\": 21288951986.0, \"z\": -73529712226.0}, \
\"radius\": 5060000, \"solarSystemID\": 30000142, \
\"statistics\": {\"locked\": false}, \"typeID\": 11}\n",
),
(
"mapMoons.jsonl",
"{\"_key\": 40000004, \"solarSystemID\": 30000142, \"orbitIndex\": 1, \
\"orbitID\": 40000002, \"typeID\": 12, \"radius\": 100000, \
\"position\": {\"x\": 1.0, \"y\": 2.0, \"z\": 3.0}}\n",
),
(
"types.jsonl",
"{\"_key\": 3000, \"groupID\": 6, \"name\": {\"en\": \"Yellow G5 (ffcc00)\"}, \
\"iconID\": 100, \"published\": true, \"volume\": 0.0}\n\
{\"_key\": 16, \"groupID\": 7, \"name\": {\"en\": \"Stargate\"}, \"published\": true}\n\
{\"_key\": 11, \"groupID\": 7, \"name\": {\"en\": \"Planet (Barren)\"}, \"published\": true}\n\
{\"_key\": 12, \"groupID\": 7, \"name\": {\"en\": \"Moon\"}, \"published\": true}\n",
),
],
);
let mut connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let summary = parse_data(&mut connection, &dir.path, &config).unwrap();
assert_eq!(
summary,
ParseSummary {
categories: 1,
groups: 2,
types: 4,
races: 1,
npc_corporations: 1,
factions: 1,
star_types: 1,
regions: 1,
constellations: 1,
solar_systems: 2,
stargates: 2,
stars: 1,
planets: 1,
moons: 1,
connections: 1,
}
);
let total_faction_race: i64 = connection
.query_row("SELECT COUNT(*) FROM factionRace", [], |row| row.get(0))
.unwrap();
assert_eq!(total_faction_race, 1);
let (conn_system_a, conn_system_b): (i64, i64) = connection
.query_row(
"SELECT systemA, systemB FROM mapSystemConnections",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!((conn_system_a, conn_system_b), (30000142, 30002187));
let (dest_gate, dest_system): (i64, i64) = connection
.query_row(
"SELECT destinationGateId, destinationSystemId FROM mapSystemGates \
WHERE systemGateId = 50000001",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(dest_gate, 50000002);
assert_eq!(dest_system, 30002187);
}
#[test]
fn parse_data_rolls_back_everything_on_failure() {
let dir = TempSdeDir::new(
"parse_data_rollback",
&[
(
"categories.jsonl",
"{\"_key\": 6, \"name\": {\"en\": \"Celestial\"}, \"published\": true}\n",
),
(
"groups.jsonl",
"{\"_key\": 6, \"categoryID\": 6, \"name\": {\"en\": \"Sun\"}, \"anchorable\": false}\n",
),
(
"types.jsonl",
"{\"_key\": 3000, \"groupID\": 6, \"name\": {\"en\": \"Yellow G5 (ffcc00)\"}, \
\"iconID\": 100, \"published\": true, \"volume\": 0.0}\n",
),
(
"races.jsonl",
"{\"_key\": 1, \"name\": {\"en\": \"Caldari\"}}\n",
),
(
"npcCorporations.jsonl",
"{\"_key\": 1000004, \"name\": {\"en\": \"CBD Corporation\"}, \
\"tickerName\": \"CBD\", \"deleted\": false, \"iconID\": 500, \"raceID\": 1}\n",
),
(
"factions.jsonl",
"{\"_key\": 500001, \"name\": {\"en\": \"Caldari State\"}, \"iconID\": 600, \
\"uniqueName\": true, \"corporationID\": 1000004}\n",
),
],
);
let mut connection = Connection::open_in_memory().unwrap();
crate::builder::schema::create_schema(&connection).unwrap();
let config = ParserConfig::default();
let result = parse_data(&mut connection, &dir.path, &config);
assert!(result.is_err());
for table in [
"invCategories",
"invGroups",
"invTypes",
"races",
"npcCorporations",
"factions",
"factionRace",
"typeStar",
] {
let count: i64 = connection
.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(count, 0, "table {table} should be empty after the rollback");
}
}
}