equal ignore case
diff --git a/spanner/key_recipe.go b/spanner/key_recipe.go index a9e2d44..431abb7 100644 --- a/spanner/key_recipe.go +++ b/spanner/key_recipe.go
@@ -24,6 +24,7 @@ "math" "math/rand" "strconv" + "strings" "time" sppb "cloud.google.com/go/spanner/apiv1/spannerpb" @@ -554,11 +555,34 @@ if in != nil { fields = in.GetFields() } + var foldIndex map[string]string + foldIndexBuilt := false return r.encodeKeyInternal(func(index int, identifier string) (*structpb.Value, valueLookupStatus) { if identifier == "" { return nil, valueLookupMissing } value, ok := fields[identifier] + if ok { + return value, valueLookupFound + } + if !foldIndexBuilt { + foldIndex = make(map[string]string, len(fields)) + for fieldName := range fields { + foldedName := strings.ToLower(fieldName) + if existing, exists := foldIndex[foldedName]; !exists { + foldIndex[foldedName] = fieldName + } else if existing != fieldName { + // Multiple parameters differ only in case; treat as ambiguous. + foldIndex[foldedName] = "" + } + } + foldIndexBuilt = true + } + fallbackName, exists := foldIndex[strings.ToLower(identifier)] + if !exists || fallbackName == "" { + return nil, valueLookupMissing + } + value, ok = fields[fallbackName] if !ok { return nil, valueLookupMissing }
diff --git a/spanner/key_recipe_test.go b/spanner/key_recipe_test.go index 2ab82e9..c80311c 100644 --- a/spanner/key_recipe_test.go +++ b/spanner/key_recipe_test.go
@@ -172,6 +172,73 @@ } } +func TestKeyRecipe_QueryParamsCaseInsensitiveFallback(t *testing.T) { + recipeProto := parseKeyRecipeTextProto(t, + "part { tag: 1 }\n"+ + "part {\n"+ + " order: ASCENDING\n"+ + " null_order: NULLS_FIRST\n"+ + " type { code: STRING }\n"+ + " identifier: \"id\"\n"+ + "}\n") + params := parseStructTextProto(t, + "fields {\n"+ + " key: \"Id\"\n"+ + " value { string_value: \"foo\" }\n"+ + "}\n") + + recipe, err := newKeyRecipe(recipeProto) + if err != nil { + t.Fatalf("newKeyRecipe() failed: %v", err) + } + target := recipe.queryParamsToTargetRange(params) + + wantStart := expectedKeyForStringValue(t, "foo") + if !bytes.Equal(target.start, wantStart) { + t.Fatalf("unexpected start with case-insensitive fallback: got %q want %q", target.start, wantStart) + } + if target.approximate { + t.Fatal("expected exact routing key with case-insensitive fallback") + } +} + +func TestKeyRecipe_QueryParamsCaseInsensitiveFallbackAmbiguous(t *testing.T) { + recipeProto := parseKeyRecipeTextProto(t, + "part { tag: 1 }\n"+ + "part {\n"+ + " order: ASCENDING\n"+ + " null_order: NULLS_FIRST\n"+ + " type { code: STRING }\n"+ + " identifier: \"ID\"\n"+ + "}\n") + params := parseStructTextProto(t, + "fields {\n"+ + " key: \"Id\"\n"+ + " value { string_value: \"foo\" }\n"+ + "}\n"+ + "fields {\n"+ + " key: \"id\"\n"+ + " value { string_value: \"bar\" }\n"+ + "}\n") + + recipe, err := newKeyRecipe(recipeProto) + if err != nil { + t.Fatalf("newKeyRecipe() failed: %v", err) + } + target := recipe.queryParamsToTargetRange(params) + + wantStart, err := appendCompositeTag(nil, 1) + if err != nil { + t.Fatalf("appendCompositeTag() failed: %v", err) + } + if !bytes.Equal(target.start, wantStart) { + t.Fatalf("unexpected start for ambiguous case-insensitive lookup: got %q want %q", target.start, wantStart) + } + if !target.approximate { + t.Fatal("expected approximate=true for ambiguous case-insensitive lookup") + } +} + func parseKeyRecipeTextProto(t *testing.T, text string) *sppb.KeyRecipe { t.Helper() recipe := &sppb.KeyRecipe{}