mapping.go
package imp
import (
"context"
"fmt"
"sort"
"skraak/db"
"skraak/mapping"
)
// Re-export mapping types for convenience within this package.
// External callers should use the mapping package directly.
type (
MappingFile = mapping.File
SpeciesMapping = mapping.SpeciesMapping
MappingValidationResult = mapping.ValidationResult
)
const (
MappingNegative = mapping.Negative
MappingIgnore = mapping.Ignore
)
var (
LoadMappingFile = mapping.Load
)
type (
MappingKind = mapping.Kind
)
const (
MappingReal = mapping.Real
MappingNeg = mapping.Neg
MappingIgn = mapping.Ign
)
// ValidateMappingAgainstDB validates that all mapped species and calltypes exist in the database
// Also validates that the mapping covers all species/calltypes found in .data files
func ValidateMappingAgainstDB(
q db.Querier,
m mapping.File,
dataSpeciesSet map[string]bool,
dataCalltypes map[string]map[string]bool, // species -> calltype -> true
) (mapping.ValidationResult, error) {
result := mapping.ValidationResult{
MissingSpecies: make([]string, 0),
MissingDBSpecies: make([]string, 0),
MissingCalltypes: make(map[string]string),
}
// Check all .data species are in mapping
for species := range dataSpeciesSet {
if _, exists := m[species]; !exists {
result.MissingSpecies = append(result.MissingSpecies, species)
}
}
sort.Strings(result.MissingSpecies)
// Collect all mapped species and calltypes
mappedSpeciesSet, mappedCalltypes := collectMappedLabels(m, dataCalltypes)
// Validate species exist in DB
if err := validateMappedSpecies(q, mappedSpeciesSet, &result); err != nil {
return result, err
}
// Validate calltypes exist in DB
if err := validateMappedCalltypes(q, mappedCalltypes, &result); err != nil {
return result, err
}
return result, nil
}
// collectUnmappedCalltypes adds calltypes from .data files that have no explicit
// mapping entry (dataCT == dbCT by convention) to the mappedCalltypes set.
func collectUnmappedCalltypes(m mapping.File, dataCalltypes map[string]map[string]bool, mappedCalltypes map[string]map[string]string) {
for dataSpecies, ctSet := range dataCalltypes {
sm, exists := m[dataSpecies]
if !exists {
continue
}
dbSpecies := sm.Species
for dataCT := range ctSet {
dbCT := dataCT
if sm.Calltypes != nil {
if mapped, ok := sm.Calltypes[dataCT]; ok {
dbCT = mapped
}
}
if mappedCalltypes[dbSpecies] == nil {
mappedCalltypes[dbSpecies] = make(map[string]string)
}
mappedCalltypes[dbSpecies][dbCT] = dataCT
}
}
}
func collectMappedLabels(m mapping.File, dataCalltypes map[string]map[string]bool) (map[string]bool, map[string]map[string]string) {
mappedSpeciesSet := make(map[string]bool)
mappedCalltypes := make(map[string]map[string]string)
for _, sm := range m {
if sm.Species == mapping.Negative || sm.Species == mapping.Ignore {
continue
}
mappedSpeciesSet[sm.Species] = true
if len(sm.Calltypes) > 0 {
if mappedCalltypes[sm.Species] == nil {
mappedCalltypes[sm.Species] = make(map[string]string)
}
for dataCT, dbCT := range sm.Calltypes {
mappedCalltypes[sm.Species][dbCT] = dataCT
}
}
}
collectUnmappedCalltypes(m, dataCalltypes, mappedCalltypes)
return mappedSpeciesSet, mappedCalltypes
}
// validateMappedSpecies checks that all mapped species exist in the database
func validateMappedSpecies(q db.Querier, mappedSpeciesSet map[string]bool, result *mapping.ValidationResult) error {
speciesLabels := make([]string, 0, len(mappedSpeciesSet))
for s := range mappedSpeciesSet {
speciesLabels = append(speciesLabels, s)
}
sort.Strings(speciesLabels)
if len(speciesLabels) == 0 {
return nil
}
query := `SELECT label FROM species WHERE label IN (` + db.Placeholders(len(speciesLabels)) + `) AND active = true`
args := make([]any, len(speciesLabels))
for i, s := range speciesLabels {
args[i] = s
}
rows, err := q.QueryContext(context.Background(), query, args...)
if err != nil {
return fmt.Errorf("failed to query species: %w", err)
}
defer rows.Close()
foundSpecies := make(map[string]bool)
for rows.Next() {
var label string
if err := rows.Scan(&label); err == nil {
foundSpecies[label] = true
}
}
for _, s := range speciesLabels {
if !foundSpecies[s] {
result.MissingDBSpecies = append(result.MissingDBSpecies, s)
}
}
return nil
}
// validateMappedCalltypes checks that all mapped calltypes exist in the database
func validateMappedCalltypes(q db.Querier, mappedCalltypes map[string]map[string]string, result *mapping.ValidationResult) error {
for dbSpecies, ctMap := range mappedCalltypes {
if len(ctMap) == 0 {
continue
}
ctLabels := make([]string, 0, len(ctMap))
for dbCT := range ctMap {
ctLabels = append(ctLabels, dbCT)
}
sort.Strings(ctLabels)
query := `
SELECT ct.label
FROM call_type ct
JOIN species s ON ct.species_id = s.id
WHERE s.label = ? AND ct.label IN (` + db.Placeholders(len(ctLabels)) + `) AND ct.active = true`
args := make([]any, 1+len(ctLabels))
args[0] = dbSpecies
for i, ct := range ctLabels {
args[1+i] = ct
}
rows, err := q.QueryContext(context.Background(), query, args...)
if err != nil {
return fmt.Errorf("failed to query calltypes for species %s: %w", dbSpecies, err)
}
defer rows.Close()
foundCT := make(map[string]bool)
for rows.Next() {
var label string
if err := rows.Scan(&label); err == nil {
foundCT[label] = true
}
}
for dbCT, dataCT := range ctMap {
if !foundCT[dbCT] {
key := fmt.Sprintf("%s/%s", dbSpecies, dataCT)
value := fmt.Sprintf("%s/%s", dbSpecies, dbCT)
result.MissingCalltypes[key] = value
}
}
}
return nil
}