feat(inventory-go): build EXISTS conditions for spell filter params
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b0a6b494bf
commit
4edf94f416
2 changed files with 108 additions and 0 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -58,3 +59,48 @@ func exactSpellID(spells map[int]map[string]any, q string) (int, bool) {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,3 +50,65 @@ func TestExactSpellID(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue