Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

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
}