Files
zkill-susser/analytics_queries.go

742 lines
23 KiB
Go

package main
import (
"context"
"fmt"
"strings"
logger "git.site.quack-lab.dev/dave/cylogger"
)
// Time aggregation queries
func (db *DBWrapper) QueryTimeByHour(ctx context.Context, filters AnalyticsFilters) ([]TimeAggregationByHour, error) {
flog := logger.Default.WithPrefix("QueryTimeByHour")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
kill_hour,
count() as kill_count,
count(DISTINCT victim_character_id) as unique_victims,
count(DISTINCT solar_system_id) as systems_hit
FROM zkill.killmails
%s
GROUP BY kill_hour
ORDER BY kill_hour
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query time by hour: %w", err)
}
defer rows.Close()
var results []TimeAggregationByHour
for rows.Next() {
var r TimeAggregationByHour
if err := rows.Scan(&r.KillHour, &r.KillCount, &r.UniqueVictims, &r.SystemsHit); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryTimeByDay(ctx context.Context, filters AnalyticsFilters) ([]TimeAggregationByDay, error) {
flog := logger.Default.WithPrefix("QueryTimeByDay")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
kill_day_of_week,
CASE kill_day_of_week
WHEN 1 THEN 'Monday'
WHEN 2 THEN 'Tuesday'
WHEN 3 THEN 'Wednesday'
WHEN 4 THEN 'Thursday'
WHEN 5 THEN 'Friday'
WHEN 6 THEN 'Saturday'
WHEN 7 THEN 'Sunday'
END as day_name,
count() as kill_count,
count(DISTINCT victim_character_id) as unique_victims
FROM zkill.killmails
%s
GROUP BY kill_day_of_week
ORDER BY kill_day_of_week
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query time by day: %w", err)
}
defer rows.Close()
var results []TimeAggregationByDay
for rows.Next() {
var r TimeAggregationByDay
if err := rows.Scan(&r.KillDayOfWeek, &r.DayName, &r.KillCount, &r.UniqueVictims); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryTimeByDate(ctx context.Context, filters AnalyticsFilters) ([]TimeAggregationByDate, error) {
flog := logger.Default.WithPrefix("QueryTimeByDate")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
toString(kill_date) as kill_date,
count() as kill_count,
count(DISTINCT victim_character_id) as unique_victims,
count(DISTINCT solar_system_id) as systems_hit
FROM zkill.killmails
%s
GROUP BY kill_date
ORDER BY kill_date DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query time by date: %w", err)
}
defer rows.Close()
var results []TimeAggregationByDate
for rows.Next() {
var r TimeAggregationByDate
if err := rows.Scan(&r.KillDate, &r.KillCount, &r.UniqueVictims, &r.SystemsHit); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryTimeByMonth(ctx context.Context, filters AnalyticsFilters) ([]TimeAggregationByMonth, error) {
flog := logger.Default.WithPrefix("QueryTimeByMonth")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
toString(toYYYYMM(killmail_time)) as month,
count() as kill_count,
count(DISTINCT victim_character_id) as unique_victims
FROM zkill.killmails
%s
GROUP BY month
ORDER BY month DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query time by month: %w", err)
}
defer rows.Close()
var results []TimeAggregationByMonth
for rows.Next() {
var r TimeAggregationByMonth
if err := rows.Scan(&r.Month, &r.KillCount, &r.UniqueVictims); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
// Location aggregation queries
func (db *DBWrapper) QueryLocationBySystem(ctx context.Context, filters AnalyticsFilters) ([]LocationAggregationBySystem, error) {
flog := logger.Default.WithPrefix("QueryLocationBySystem")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
solar_system_id,
solar_system_name,
region_name,
security,
count() as kill_count,
count(DISTINCT victim_character_id) as unique_victims,
count(DISTINCT victim_ship_type_name) as ship_variety
FROM zkill.killmails
%s
GROUP BY solar_system_id, solar_system_name, region_name, security
ORDER BY kill_count DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query location by system: %w", err)
}
defer rows.Close()
var results []LocationAggregationBySystem
for rows.Next() {
var r LocationAggregationBySystem
if err := rows.Scan(&r.SolarSystemID, &r.SolarSystemName, &r.RegionName, &r.Security, &r.KillCount, &r.UniqueVictims, &r.ShipVariety); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryLocationByRegion(ctx context.Context, filters AnalyticsFilters) ([]LocationAggregationByRegion, error) {
flog := logger.Default.WithPrefix("QueryLocationByRegion")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
region_name,
count() as kill_count,
count(DISTINCT solar_system_name) as systems_with_kills,
count(DISTINCT victim_character_id) as unique_victims
FROM zkill.killmails
%s
GROUP BY region_name
ORDER BY kill_count DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query location by region: %w", err)
}
defer rows.Close()
var results []LocationAggregationByRegion
for rows.Next() {
var r LocationAggregationByRegion
if err := rows.Scan(&r.RegionName, &r.KillCount, &r.SystemsWithKills, &r.UniqueVictims); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryLocationByConstellation(ctx context.Context, filters AnalyticsFilters) ([]LocationAggregationByConstellation, error) {
flog := logger.Default.WithPrefix("QueryLocationByConstellation")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
constellation_name,
region_name,
count() as kill_count,
count(DISTINCT solar_system_name) as systems_in_constellation
FROM zkill.killmails
%s
GROUP BY constellation_name, region_name
ORDER BY kill_count DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query location by constellation: %w", err)
}
defer rows.Close()
var results []LocationAggregationByConstellation
for rows.Next() {
var r LocationAggregationByConstellation
if err := rows.Scan(&r.ConstellationName, &r.RegionName, &r.KillCount, &r.SystemsInConstellation); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryLocationBySecurity(ctx context.Context, filters AnalyticsFilters) ([]LocationAggregationBySecurity, error) {
flog := logger.Default.WithPrefix("QueryLocationBySecurity")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
CASE
WHEN security >= 0.5 THEN 'High Sec'
WHEN security > 0.0 THEN 'Low Sec'
ELSE 'Null Sec'
END as sec_status,
count() as kill_count,
count(DISTINCT solar_system_name) as systems
FROM zkill.killmails
%s
GROUP BY sec_status
ORDER BY kill_count DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query location by security: %w", err)
}
defer rows.Close()
var results []LocationAggregationBySecurity
for rows.Next() {
var r LocationAggregationBySecurity
if err := rows.Scan(&r.SecStatus, &r.KillCount, &r.Systems); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
// Ship aggregation queries
func (db *DBWrapper) QueryShipByVictim(ctx context.Context, filters AnalyticsFilters) ([]ShipAggregationByVictimShip, error) {
flog := logger.Default.WithPrefix("QueryShipByVictim")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
victim_ship_type_id,
victim_ship_type_name,
victim_ship_group_name,
victim_ship_category_name,
count() as kill_count,
count(DISTINCT victim_character_id) as unique_pilots_killed
FROM zkill.killmails
%s
GROUP BY victim_ship_type_id, victim_ship_type_name, victim_ship_group_name, victim_ship_category_name
ORDER BY kill_count DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query ship by victim: %w", err)
}
defer rows.Close()
var results []ShipAggregationByVictimShip
for rows.Next() {
var r ShipAggregationByVictimShip
if err := rows.Scan(&r.ShipTypeID, &r.ShipTypeName, &r.ShipGroupName, &r.ShipCategoryName, &r.KillCount, &r.UniquePilots); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryShipByAttacker(ctx context.Context, filters AnalyticsFilters) ([]ShipAggregationByAttackerShip, error) {
flog := logger.Default.WithPrefix("QueryShipByAttacker")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
whereClause, whereArgs := buildAttackerWhereClause(filters)
args := append(killmailArgs, whereArgs...)
query := fmt.Sprintf(`
SELECT
ship_type_name,
ship_group_name,
count() as times_used,
count(DISTINCT killmail_id) as kills_participated,
sum(damage_done) as total_damage,
countIf(final_blow) as final_blows
FROM zkill.killmail_attackers
WHERE killmail_id IN %s
%s
GROUP BY ship_type_name, ship_group_name
ORDER BY kills_participated DESC
`, killmailSubquery, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query ship by attacker: %w", err)
}
defer rows.Close()
var results []ShipAggregationByAttackerShip
for rows.Next() {
var r ShipAggregationByAttackerShip
if err := rows.Scan(&r.ShipTypeName, &r.ShipGroupName, &r.TimesUsed, &r.KillsParticipated, &r.TotalDamage, &r.FinalBlows); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
// Player aggregation queries
func (db *DBWrapper) QueryPlayerByVictimCharacter(ctx context.Context, filters AnalyticsFilters) ([]PlayerAggregationByVictimCharacter, error) {
flog := logger.Default.WithPrefix("QueryPlayerByVictimCharacter")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
victim_character_name,
victim_corporation_name,
victim_alliance_name,
count() as deaths,
count(DISTINCT victim_ship_type_name) as ships_lost
FROM zkill.killmails
%s
GROUP BY victim_character_name, victim_corporation_name, victim_alliance_name
ORDER BY deaths DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query player by victim character: %w", err)
}
defer rows.Close()
var results []PlayerAggregationByVictimCharacter
for rows.Next() {
var r PlayerAggregationByVictimCharacter
if err := rows.Scan(&r.CharacterName, &r.Corporation, &r.Alliance, &r.Deaths, &r.ShipsLost); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryPlayerByVictimCorporation(ctx context.Context, filters AnalyticsFilters) ([]PlayerAggregationByVictimCorporation, error) {
flog := logger.Default.WithPrefix("QueryPlayerByVictimCorporation")
whereClause, args := buildWhereClause(filters)
query := fmt.Sprintf(`
SELECT
victim_corporation_name,
victim_alliance_name,
count() as losses,
count(DISTINCT victim_character_name) as members_killed
FROM zkill.killmails
%s
GROUP BY victim_corporation_name, victim_alliance_name
ORDER BY losses DESC
`, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query player by victim corporation: %w", err)
}
defer rows.Close()
var results []PlayerAggregationByVictimCorporation
for rows.Next() {
var r PlayerAggregationByVictimCorporation
if err := rows.Scan(&r.Corporation, &r.Alliance, &r.Losses, &r.MembersKilled); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryPlayerByVictimAlliance(ctx context.Context, filters AnalyticsFilters) ([]PlayerAggregationByVictimAlliance, error) {
flog := logger.Default.WithPrefix("QueryPlayerByVictimAlliance")
whereClause, args := buildWhereClause(filters)
whereWithAlliance := whereClause
if whereClause == "" {
whereWithAlliance = "WHERE victim_alliance_name != ''"
} else {
whereWithAlliance = whereClause + " AND victim_alliance_name != ''"
}
query := fmt.Sprintf(`
SELECT
victim_alliance_name,
count() as losses,
count(DISTINCT victim_corporation_name) as corps_hit,
count(DISTINCT victim_character_name) as members_killed
FROM zkill.killmails
%s
GROUP BY victim_alliance_name
ORDER BY losses DESC
`, whereWithAlliance)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query player by victim alliance: %w", err)
}
defer rows.Close()
var results []PlayerAggregationByVictimAlliance
for rows.Next() {
var r PlayerAggregationByVictimAlliance
if err := rows.Scan(&r.Alliance, &r.Losses, &r.CorpsHit, &r.MembersKilled); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryPlayerByAttackerCharacter(ctx context.Context, filters AnalyticsFilters) ([]PlayerAggregationByAttackerCharacter, error) {
flog := logger.Default.WithPrefix("QueryPlayerByAttackerCharacter")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
whereClause, whereArgs := buildAttackerWhereClause(filters)
args := append(killmailArgs, whereArgs...)
query := fmt.Sprintf(`
SELECT
character_name,
corporation_name,
alliance_name,
count(DISTINCT killmail_id) as kills_participated,
countIf(final_blow) as final_blows,
sum(damage_done) as total_damage
FROM zkill.killmail_attackers
WHERE killmail_id IN %s
%s
GROUP BY character_name, corporation_name, alliance_name
ORDER BY kills_participated DESC
`, killmailSubquery, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query player by attacker character: %w", err)
}
defer rows.Close()
var results []PlayerAggregationByAttackerCharacter
for rows.Next() {
var r PlayerAggregationByAttackerCharacter
if err := rows.Scan(&r.CharacterName, &r.Corporation, &r.Alliance, &r.KillsParticipated, &r.FinalBlows, &r.TotalDamage); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryPlayerByAttackerCorporation(ctx context.Context, filters AnalyticsFilters) ([]PlayerAggregationByAttackerCorporation, error) {
flog := logger.Default.WithPrefix("QueryPlayerByAttackerCorporation")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
whereClause, whereArgs := buildAttackerWhereClause(filters)
args := append(killmailArgs, whereArgs...)
query := fmt.Sprintf(`
SELECT
corporation_name,
alliance_name,
count(DISTINCT killmail_id) as kills_participated,
count(DISTINCT character_name) as members_involved
FROM zkill.killmail_attackers
WHERE killmail_id IN %s
%s
GROUP BY corporation_name, alliance_name
ORDER BY kills_participated DESC
`, killmailSubquery, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query player by attacker corporation: %w", err)
}
defer rows.Close()
var results []PlayerAggregationByAttackerCorporation
for rows.Next() {
var r PlayerAggregationByAttackerCorporation
if err := rows.Scan(&r.Corporation, &r.Alliance, &r.KillsParticipated, &r.MembersInvolved); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryPlayerByAttackerAlliance(ctx context.Context, filters AnalyticsFilters) ([]PlayerAggregationByAttackerAlliance, error) {
flog := logger.Default.WithPrefix("QueryPlayerByAttackerAlliance")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
whereClause, whereArgs := buildAttackerWhereClause(filters)
args := append(killmailArgs, whereArgs...)
query := fmt.Sprintf(`
SELECT
alliance_name,
count(DISTINCT killmail_id) as kills_participated,
count(DISTINCT corporation_name) as corps_involved,
count(DISTINCT character_name) as members_involved
FROM zkill.killmail_attackers
WHERE killmail_id IN %s
%s AND alliance_name != ''
GROUP BY alliance_name
ORDER BY kills_participated DESC
`, killmailSubquery, whereClause)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query player by attacker alliance: %w", err)
}
defer rows.Close()
var results []PlayerAggregationByAttackerAlliance
for rows.Next() {
var r PlayerAggregationByAttackerAlliance
if err := rows.Scan(&r.Alliance, &r.KillsParticipated, &r.CorpsInvolved, &r.MembersInvolved); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
// Module aggregation queries
func (db *DBWrapper) QueryModuleBySlotType(ctx context.Context, filters AnalyticsFilters) ([]ModuleAggregationBySlotType, error) {
flog := logger.Default.WithPrefix("QueryModuleBySlotType")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
var slotTypeFilter string
var slotArgs []interface{}
if len(filters.SlotType) > 0 {
placeholders := make([]string, len(filters.SlotType))
for i := range placeholders {
placeholders[i] = "?"
}
slotTypeFilter = "AND slot_type IN (" + strings.Join(placeholders, ",") + ")"
for _, s := range filters.SlotType {
slotArgs = append(slotArgs, s)
}
}
args := append(killmailArgs, slotArgs...)
query := fmt.Sprintf(`
SELECT
slot_type,
count() as items_fitted,
count(DISTINCT killmail_id) as ships_with_slot,
count(DISTINCT item_type_name) as module_variety
FROM zkill.killmail_items
WHERE killmail_id IN %s
%s
GROUP BY slot_type
ORDER BY items_fitted DESC
`, killmailSubquery, slotTypeFilter)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query module by slot type: %w", err)
}
defer rows.Close()
var results []ModuleAggregationBySlotType
for rows.Next() {
var r ModuleAggregationBySlotType
if err := rows.Scan(&r.SlotType, &r.ItemsFitted, &r.ShipsWithSlot, &r.ModuleVariety); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryModuleByModule(ctx context.Context, filters AnalyticsFilters) ([]ModuleAggregationByModule, error) {
flog := logger.Default.WithPrefix("QueryModuleByModule")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
var slotTypeFilter string
var slotArgs []interface{}
if len(filters.SlotType) > 0 {
placeholders := make([]string, len(filters.SlotType))
for i := range placeholders {
placeholders[i] = "?"
}
slotTypeFilter = "AND slot_type IN (" + strings.Join(placeholders, ",") + ")"
for _, s := range filters.SlotType {
slotArgs = append(slotArgs, s)
}
}
args := append(killmailArgs, slotArgs...)
query := fmt.Sprintf(`
SELECT
item_type_id,
item_type_name,
item_group_name,
item_category_name,
count() as times_fitted,
count(DISTINCT killmail_id) as ships_with_module
FROM zkill.killmail_items
WHERE killmail_id IN %s
%s
GROUP BY item_type_id, item_type_name, item_group_name, item_category_name
ORDER BY times_fitted DESC
`, killmailSubquery, slotTypeFilter)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query module by module: %w", err)
}
defer rows.Close()
var results []ModuleAggregationByModule
for rows.Next() {
var r ModuleAggregationByModule
if err := rows.Scan(&r.ItemTypeID, &r.ItemTypeName, &r.ItemGroupName, &r.ItemCategoryName, &r.TimesFitted, &r.ShipsWithModule); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}
func (db *DBWrapper) QueryModuleCoOccurrence(ctx context.Context, filters AnalyticsFilters, selectedModule, selectedSlot string) ([]ModuleCoOccurrence, error) {
flog := logger.Default.WithPrefix("QueryModuleCoOccurrence")
killmailSubquery, killmailArgs := getKillmailIDSubquery(filters)
args := append(killmailArgs, selectedModule, selectedSlot)
query := fmt.Sprintf(`
WITH selected_ships AS (
SELECT DISTINCT killmail_id
FROM zkill.killmail_items
WHERE item_type_name = ?
AND slot_type = ?
AND killmail_id IN %s
)
SELECT
item_type_name,
slot_type,
count() as times_fitted_together,
count() * 100.0 / (SELECT count() FROM selected_ships) as percentage
FROM zkill.killmail_items
WHERE killmail_id IN selected_ships
AND NOT (item_type_name = ? AND slot_type = ?)
GROUP BY item_type_name, slot_type
ORDER BY times_fitted_together DESC
`, killmailSubquery)
args = append(args, selectedModule, selectedSlot)
flog.Debug("Executing query: %s", query)
rows, err := db.ch.Query(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to query module co-occurrence: %w", err)
}
defer rows.Close()
var results []ModuleCoOccurrence
for rows.Next() {
var r ModuleCoOccurrence
if err := rows.Scan(&r.ItemTypeName, &r.SlotType, &r.TimesTogether, &r.Percentage); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
results = append(results, r)
}
return results, nil
}