feat(inventory-go): build EXISTS conditions for spell filter params

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-14 23:44:51 +02:00
parent b0a6b494bf
commit 4edf94f416
2 changed files with 108 additions and 0 deletions

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"net/url"
"sort" "sort"
"strings" "strings"
) )
@ -58,3 +59,48 @@ func exactSpellID(spells map[int]map[string]any, q string) (int, bool) {
} }
return best, best != -1 return best, best != -1
} }
// spellExists renders the EXISTS clause for a set of spell IDs, binding each
// ID as a positional arg. ids must be non-empty.
func spellExists(ids []int, ab *argBuilder) string {
ph := make([]string, len(ids))
for i, id := range ids {
ph[i] = ab.add(id)
}
return "EXISTS (SELECT 1 FROM item_spells sp WHERE sp.item_id = db_item_id AND sp.spell_id IN (" + strings.Join(ph, ", ") + "))"
}
// spellFilterConds builds the WHERE conditions for has_spell, spell_contains,
// and legendary_cantrips. Cantrips use AND semantics — one EXISTS per checked
// cantrip (NOT the legacy COUNT>=N trick, which can false-positive when one
// label matches two spells on an item while another matches none). Any
// no-match filter yields the impossible condition, matching legacy intent.
func spellFilterConds(q url.Values, spells map[int]map[string]any, ab *argBuilder) []string {
var conds []string
impossible := func() { conds = append(conds, "1 = 0") }
if v := q.Get("has_spell"); v != "" {
if id, ok := exactSpellID(spells, v); ok {
conds = append(conds, spellExists([]int{id}, ab))
} else {
impossible()
}
}
if v := q.Get("spell_contains"); v != "" {
if ids := spellIDsContaining(spells, v); len(ids) > 0 {
conds = append(conds, spellExists(ids, ab))
} else {
impossible()
}
}
if v := q.Get("legendary_cantrips"); v != "" {
for _, name := range splitNonEmpty(v) {
if ids := spellIDsForCantrip(spells, name); len(ids) > 0 {
conds = append(conds, spellExists(ids, ab))
} else {
impossible()
}
}
}
return conds
}

View file

@ -50,3 +50,65 @@ func TestExactSpellID(t *testing.T) {
t.Error("partial name must not exact-match") t.Error("partial name must not exact-match")
} }
} }
func TestSpellFilterConds_Cantrips(t *testing.T) {
ab := &argBuilder{}
q := map[string][]string{"legendary_cantrips": {"Legendary Invulnerability, Legendary Summoning Prowess"}}
conds := spellFilterConds(q, testSpells, ab)
want := []string{
"EXISTS (SELECT 1 FROM item_spells sp WHERE sp.item_id = db_item_id AND sp.spell_id IN ($1))",
"EXISTS (SELECT 1 FROM item_spells sp WHERE sp.item_id = db_item_id AND sp.spell_id IN ($2, $3))",
}
if !reflect.DeepEqual(conds, want) {
t.Errorf("conds = %v, want %v", conds, want)
}
// "Legendary Invulnerability" exact-contains only id 1; the Summoning
// label matches 3 and 5. AND semantics = one EXISTS per cantrip.
if wantArgs := []any{1, 3, 5}; !reflect.DeepEqual(ab.args, wantArgs) {
t.Errorf("args = %v, want %v", ab.args, wantArgs)
}
}
func TestSpellFilterConds_NoMatchIsImpossible(t *testing.T) {
ab := &argBuilder{}
q := map[string][]string{"legendary_cantrips": {"Legendary Frostbite"}}
if conds := spellFilterConds(q, testSpells, ab); !reflect.DeepEqual(conds, []string{"1 = 0"}) {
t.Errorf("unknown cantrip conds = %v, want [1 = 0]", conds)
}
ab = &argBuilder{}
q = map[string][]string{"spell_contains": {"frostbite"}}
if conds := spellFilterConds(q, testSpells, ab); !reflect.DeepEqual(conds, []string{"1 = 0"}) {
t.Errorf("unknown spell_contains conds = %v, want [1 = 0]", conds)
}
}
func TestSpellFilterConds_ContainsAndHasSpell(t *testing.T) {
ab := &argBuilder{}
q := map[string][]string{"spell_contains": {"invulnerability"}}
want := []string{"EXISTS (SELECT 1 FROM item_spells sp WHERE sp.item_id = db_item_id AND sp.spell_id IN ($1, $2))"}
if conds := spellFilterConds(q, testSpells, ab); !reflect.DeepEqual(conds, want) {
t.Errorf("spell_contains conds = %v, want %v", conds, want)
}
ab = &argBuilder{}
q = map[string][]string{"has_spell": {"Epic Invulnerability"}}
want = []string{"EXISTS (SELECT 1 FROM item_spells sp WHERE sp.item_id = db_item_id AND sp.spell_id IN ($1))"}
if conds := spellFilterConds(q, testSpells, ab); !reflect.DeepEqual(conds, want) {
t.Errorf("has_spell conds = %v, want %v", conds, want)
}
if _, ok := exactSpellID(testSpells, "No Such Spell"); ok {
t.Fatal("precondition")
}
ab = &argBuilder{}
q = map[string][]string{"has_spell": {"No Such Spell"}}
if conds := spellFilterConds(q, testSpells, ab); !reflect.DeepEqual(conds, []string{"1 = 0"}) {
t.Errorf("unknown has_spell conds = %v, want [1 = 0]", conds)
}
}
func TestSpellFilterConds_Empty(t *testing.T) {
ab := &argBuilder{}
if conds := spellFilterConds(map[string][]string{}, testSpells, ab); conds != nil {
t.Errorf("no params => nil conds, got %v", conds)
}
}