Add stub database selection, and fix differences between pekoe and c-redis

- PUBSUB CHANNELS glob pattern matching
- Implement SELECT + max DB validation (0-15) (default db max is 16), for perl clients
- CLIENT KILL ID (single/range) + MAXAGE
- CLIENT INFO events + resp fields
- PUBSUB SHARDCHANNELS/SHARDNUMSUB stubs
This commit is contained in:
eleksir
2026-04-03 01:02:03 +03:00
parent 9e30243533
commit 92cd652b0d
5 changed files with 266 additions and 10 deletions

58
internal/glob/glob.go Normal file
View File

@@ -0,0 +1,58 @@
// @tag glob
package glob
// Match checks if a string matches a glob-style pattern.
// Supports:
// - * (asterisk): matches any sequence of characters (including empty)
// - ? (question): matches any single character
//
// This matches Redis PUBSUB CHANNELS pattern behavior.
func Match(pattern, s string) bool {
if pattern == "" {
return s == ""
}
if pattern == "*" {
return true
}
// Simple glob matching using dynamic programming
// Convert pattern to regex-like matching
patternLen := len(pattern)
sLen := len(s)
// dp[i][j] = true if pattern[0:i] matches s[0:j]
dp := make([][]bool, patternLen+1)
for i := range dp {
dp[i] = make([]bool, sLen+1)
}
// Empty pattern matches empty string
dp[0][0] = true
// Handle leading * in pattern
for i := 1; i <= patternLen; i++ {
if pattern[i-1] == '*' {
dp[i][0] = dp[i-1][0]
}
}
// Fill the DP table
for i := 1; i <= patternLen; i++ {
for j := 1; j <= sLen; j++ {
switch pattern[i-1] {
case '*':
// * matches: either consume nothing (stay at same position in pattern)
// or consume one character from string
dp[i][j] = dp[i-1][j] || dp[i][j-1]
case '?':
// ? matches any single character
dp[i][j] = dp[i-1][j-1]
default:
// Literal character must match exactly
dp[i][j] = dp[i-1][j-1] && pattern[i-1] == s[j-1]
}
}
}
return dp[patternLen][sLen]
}

View File

@@ -0,0 +1,69 @@
// @tag glob
// @tag test
package glob
import "testing"
func TestMatch(t *testing.T) {
tests := []struct {
pattern string
s string
want bool
}{
{"", "", true},
{"", "foo", false},
{"*", "", true},
{"*", "foo", true},
{"*", "foo/bar", true},
{"foo", "foo", true},
{"foo", "bar", false},
{"foo*", "foo", true},
{"foo*", "foobar", true},
{"foo*", "fo", false},
{"*foo", "foo", true},
{"*foo", "barfoo", true},
{"*foo", "bar", false},
{"foo*bar", "foobar", true},
{"foo*bar", "foobazbar", true},
{"foo*bar", "foobaz", false},
{"foo?bar", "fooXbar", true},
{"foo?bar", "fooXXbar", false},
{"foo??bar", "fooXXbar", true},
{"?", "a", true},
{"?", "", false},
{"a?c", "abc", true},
{"a?c", "ac", false},
{"foo?*bar", "fooabcbar", true},
{"foo?*bar", "foobar", false},
{"*foo*bar*", "ahellofoocherrybarb", true},
{"ch*", "channel", true},
{"ch*", "ch", true},
{"ch*", "c", false},
{"*", "*", true},
{"**", "*", true},
{"*a*b", "aXb", true},
{"a*b*c", "aXbYc", true},
}
for _, tt := range tests {
t.Run(tt.pattern+"/"+tt.s, func(t *testing.T) {
if got := Match(tt.pattern, tt.s); got != tt.want {
t.Errorf("Match(%q, %q) = %v, want %v", tt.pattern, tt.s, got, tt.want)
}
})
}
}
func BenchmarkMatch(b *testing.B) {
patterns := []string{"*", "ch*", "foo*bar", "???", "*channel*"}
strings := []string{"channel", "channel123", "foobar", "abc", "somechannelname"}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, p := range patterns {
for _, s := range strings {
Match(p, s)
}
}
}
}