bug fix:修复可能的panic状态;增加更多功能

This commit is contained in:
2026-03-20 13:36:59 +08:00
parent 8f1a9893bc
commit e0af498fa4
39 changed files with 5643 additions and 974 deletions
+323
View File
@@ -0,0 +1,323 @@
package sqlruntime
import "strings"
// FingerprintSQL creates a normalized SQL fingerprint.
// mode controls literal masking; keepComments controls whether comments are preserved.
func FingerprintSQL(query string, mode int, keepComments bool) string {
prepared := query
if !keepComments {
prepared = stripSQLComments(prepared)
}
normalized := normalizeSQL(prepared)
if normalized == "" {
return ""
}
if NormalizeFingerprintMode(mode) == fingerprintModeMaskLiterals {
return maskSQLLiterals(normalized, keepComments)
}
return normalized
}
func normalizeSQL(query string) string {
normalized := strings.ToLower(strings.TrimSpace(query))
if normalized == "" {
return ""
}
return strings.Join(strings.Fields(normalized), " ")
}
func stripSQLComments(query string) string {
if query == "" {
return ""
}
const (
stateNormal = iota
stateSingleQuote
stateDoubleQuote
stateBacktick
stateLineComment
stateBlockComment
)
var b strings.Builder
b.Grow(len(query))
state := stateNormal
for i := 0; i < len(query); i++ {
c := query[i]
switch state {
case stateNormal:
if c == '\'' {
state = stateSingleQuote
b.WriteByte(c)
continue
}
if c == '"' {
state = stateDoubleQuote
b.WriteByte(c)
continue
}
if c == '`' {
state = stateBacktick
b.WriteByte(c)
continue
}
if c == '-' && i+1 < len(query) && query[i+1] == '-' {
b.WriteByte(' ')
i++
state = stateLineComment
continue
}
if c == '/' && i+1 < len(query) && query[i+1] == '*' {
b.WriteByte(' ')
i++
state = stateBlockComment
continue
}
b.WriteByte(c)
case stateSingleQuote:
b.WriteByte(c)
if c == '\'' {
if i+1 < len(query) && query[i+1] == '\'' {
i++
b.WriteByte(query[i])
continue
}
state = stateNormal
}
case stateDoubleQuote:
b.WriteByte(c)
if c == '"' {
if i+1 < len(query) && query[i+1] == '"' {
i++
b.WriteByte(query[i])
continue
}
state = stateNormal
}
case stateBacktick:
b.WriteByte(c)
if c == '`' {
state = stateNormal
}
case stateLineComment:
if c == '\n' {
b.WriteByte(' ')
state = stateNormal
}
case stateBlockComment:
if c == '*' && i+1 < len(query) && query[i+1] == '/' {
b.WriteByte(' ')
i++
state = stateNormal
}
}
}
return b.String()
}
func maskSQLLiterals(query string, keepComments bool) string {
if query == "" {
return ""
}
const (
stateNormal = iota
stateSingleQuote
stateDoubleQuote
stateBacktick
stateLineComment
stateBlockComment
)
var b strings.Builder
b.Grow(len(query))
state := stateNormal
for i := 0; i < len(query); i++ {
c := query[i]
switch state {
case stateNormal:
if c == '\'' {
b.WriteByte('?')
state = stateSingleQuote
continue
}
if c == '"' {
b.WriteByte(c)
state = stateDoubleQuote
continue
}
if c == '`' {
b.WriteByte(c)
state = stateBacktick
continue
}
if c == '-' && i+1 < len(query) && query[i+1] == '-' {
if keepComments {
b.WriteByte(c)
i++
b.WriteByte(query[i])
} else {
b.WriteByte(' ')
i++
}
state = stateLineComment
continue
}
if c == '/' && i+1 < len(query) && query[i+1] == '*' {
if keepComments {
b.WriteByte(c)
i++
b.WriteByte(query[i])
} else {
b.WriteByte(' ')
i++
}
state = stateBlockComment
continue
}
if c == '$' {
j := i + 1
for j < len(query) && isDigit(query[j]) {
j++
}
if j > i+1 {
b.WriteByte('?')
i = j - 1
continue
}
}
if c == '-' && i+1 < len(query) && isDigit(query[i+1]) && isNumberBoundaryBefore(query, i) {
j := scanNumber(query, i+1)
if isNumberBoundaryAfter(query, j) {
b.WriteByte('?')
i = j - 1
continue
}
}
if isDigit(c) && isNumberBoundaryBefore(query, i) {
j := scanNumber(query, i)
if isNumberBoundaryAfter(query, j) {
b.WriteByte('?')
i = j - 1
continue
}
}
b.WriteByte(c)
case stateSingleQuote:
if c == '\'' {
if i+1 < len(query) && query[i+1] == '\'' {
i++
continue
}
state = stateNormal
}
case stateDoubleQuote:
b.WriteByte(c)
if c == '"' {
if i+1 < len(query) && query[i+1] == '"' {
i++
b.WriteByte(query[i])
continue
}
state = stateNormal
}
case stateBacktick:
b.WriteByte(c)
if c == '`' {
state = stateNormal
}
case stateLineComment:
if keepComments {
b.WriteByte(c)
}
if c == '\n' {
if !keepComments {
b.WriteByte(' ')
}
state = stateNormal
}
case stateBlockComment:
if keepComments {
b.WriteByte(c)
}
if c == '*' && i+1 < len(query) && query[i+1] == '/' {
if keepComments {
i++
b.WriteByte(query[i])
} else {
b.WriteByte(' ')
i++
}
state = stateNormal
}
}
}
return strings.Join(strings.Fields(b.String()), " ")
}
func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}
func isIdentifierChar(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'
}
func isNumberBoundaryBefore(query string, index int) bool {
if index <= 0 {
return true
}
prev := query[index-1]
return !isIdentifierChar(prev) && prev != '$' && prev != '.'
}
func isNumberBoundaryAfter(query string, index int) bool {
if index >= len(query) {
return true
}
next := query[index]
return !isIdentifierChar(next) && next != '.'
}
func scanNumber(query string, start int) int {
i := start
for i < len(query) && isDigit(query[i]) {
i++
}
if i < len(query) && query[i] == '.' {
i++
for i < len(query) && isDigit(query[i]) {
i++
}
}
if i < len(query) && (query[i] == 'e' || query[i] == 'E') {
i++
if i < len(query) && (query[i] == '+' || query[i] == '-') {
i++
}
for i < len(query) && isDigit(query[i]) {
i++
}
}
return i
}
+27
View File
@@ -0,0 +1,27 @@
package sqlruntime
import "time"
// CloneHookArgs creates a shallow copy for hook consumers to avoid mutation races.
func CloneHookArgs(args []interface{}) []interface{} {
if len(args) == 0 {
return nil
}
copied := make([]interface{}, len(args))
copy(copied, args)
return copied
}
// ShouldRunAfterHook decides whether after-hook should run.
func ShouldRunAfterHook(hasAfterHook bool, slowThreshold, duration time.Duration, err error) bool {
if !hasAfterHook {
return false
}
if err != nil {
return true
}
if slowThreshold <= 0 {
return true
}
return duration >= slowThreshold
}
+269
View File
@@ -0,0 +1,269 @@
package sqlruntime
import (
"sync"
"time"
)
const (
placeholderQuestion = 0
placeholderDollar = 1
fingerprintModeBasic = 0
fingerprintModeMaskLiterals = 1
)
// NormalizePlaceholderStyle converts unknown style values to default question style.
func NormalizePlaceholderStyle(style int) int {
switch style {
case placeholderDollar:
return placeholderDollar
default:
return placeholderQuestion
}
}
// NormalizeFingerprintMode converts unknown mode values to default basic mode.
func NormalizeFingerprintMode(mode int) int {
switch mode {
case fingerprintModeMaskLiterals:
return fingerprintModeMaskLiterals
default:
return fingerprintModeBasic
}
}
// State stores runtime SQL behavior toggles in a thread-safe manner.
type State struct {
mu sync.RWMutex
beforeHook interface{}
afterHook interface{}
placeholder int
slowThreshold time.Duration
fingerprintEnabled bool
fingerprintMode int
fingerprintKeepComments bool
fingerprintCounterEnabled bool
fingerprintCounts map[string]uint64
}
// Options returns snapshot of current runtime options.
func (s *State) Options() (before, after interface{}, placeholder int, slowThreshold time.Duration) {
if s == nil {
return nil, nil, placeholderQuestion, 0
}
s.mu.RLock()
before = s.beforeHook
after = s.afterHook
placeholder = NormalizePlaceholderStyle(s.placeholder)
slowThreshold = s.slowThreshold
s.mu.RUnlock()
return before, after, placeholder, slowThreshold
}
// Hooks returns before/after hooks and slow threshold.
func (s *State) Hooks() (before, after interface{}, slowThreshold time.Duration) {
before, after, _, slowThreshold = s.Options()
return before, after, slowThreshold
}
// SetHooks sets before/after hooks.
func (s *State) SetHooks(before, after interface{}) {
if s == nil {
return
}
s.mu.Lock()
s.beforeHook = before
s.afterHook = after
s.mu.Unlock()
}
// SetBeforeHook sets before hook.
func (s *State) SetBeforeHook(before interface{}) {
if s == nil {
return
}
s.mu.Lock()
s.beforeHook = before
s.mu.Unlock()
}
// SetAfterHook sets after hook.
func (s *State) SetAfterHook(after interface{}) {
if s == nil {
return
}
s.mu.Lock()
s.afterHook = after
s.mu.Unlock()
}
// SetPlaceholderStyle sets placeholder style.
func (s *State) SetPlaceholderStyle(style int) {
if s == nil {
return
}
s.mu.Lock()
s.placeholder = NormalizePlaceholderStyle(style)
s.mu.Unlock()
}
// PlaceholderStyle returns placeholder style.
func (s *State) PlaceholderStyle() int {
if s == nil {
return placeholderQuestion
}
s.mu.RLock()
style := NormalizePlaceholderStyle(s.placeholder)
s.mu.RUnlock()
return style
}
// SetSlowThreshold sets minimum duration for triggering after hook.
func (s *State) SetSlowThreshold(threshold time.Duration) {
if s == nil {
return
}
if threshold < 0 {
threshold = 0
}
s.mu.Lock()
s.slowThreshold = threshold
s.mu.Unlock()
}
// SlowThreshold returns current slow threshold.
func (s *State) SlowThreshold() time.Duration {
if s == nil {
return 0
}
s.mu.RLock()
threshold := s.slowThreshold
s.mu.RUnlock()
return threshold
}
// SetFingerprintEnabled toggles SQL fingerprint metadata generation for hooks.
func (s *State) SetFingerprintEnabled(enabled bool) {
if s == nil {
return
}
s.mu.Lock()
s.fingerprintEnabled = enabled
s.mu.Unlock()
}
// FingerprintEnabled reports whether SQL fingerprint metadata generation is enabled.
func (s *State) FingerprintEnabled() bool {
if s == nil {
return false
}
s.mu.RLock()
enabled := s.fingerprintEnabled
s.mu.RUnlock()
return enabled
}
// SetFingerprintMode sets SQL fingerprint mode.
func (s *State) SetFingerprintMode(mode int) {
if s == nil {
return
}
s.mu.Lock()
s.fingerprintMode = NormalizeFingerprintMode(mode)
s.mu.Unlock()
}
// FingerprintMode returns SQL fingerprint mode.
func (s *State) FingerprintMode() int {
if s == nil {
return fingerprintModeBasic
}
s.mu.RLock()
mode := NormalizeFingerprintMode(s.fingerprintMode)
s.mu.RUnlock()
return mode
}
// SetFingerprintKeepComments toggles comment preservation in generated SQL fingerprints.
func (s *State) SetFingerprintKeepComments(keep bool) {
if s == nil {
return
}
s.mu.Lock()
s.fingerprintKeepComments = keep
s.mu.Unlock()
}
// FingerprintKeepComments reports whether comments are kept in generated SQL fingerprints.
func (s *State) FingerprintKeepComments() bool {
if s == nil {
return false
}
s.mu.RLock()
keep := s.fingerprintKeepComments
s.mu.RUnlock()
return keep
}
// SetFingerprintCounterEnabled toggles in-memory SQL fingerprint hit counter.
func (s *State) SetFingerprintCounterEnabled(enabled bool) {
if s == nil {
return
}
s.mu.Lock()
s.fingerprintCounterEnabled = enabled
s.mu.Unlock()
}
// FingerprintCounterEnabled reports whether in-memory SQL fingerprint hit counter is enabled.
func (s *State) FingerprintCounterEnabled() bool {
if s == nil {
return false
}
s.mu.RLock()
enabled := s.fingerprintCounterEnabled
s.mu.RUnlock()
return enabled
}
// IncFingerprintCount increments hit count for a fingerprint.
func (s *State) IncFingerprintCount(fingerprint string) {
if s == nil || fingerprint == "" {
return
}
s.mu.Lock()
if s.fingerprintCounts == nil {
s.fingerprintCounts = make(map[string]uint64)
}
s.fingerprintCounts[fingerprint]++
s.mu.Unlock()
}
// FingerprintCountsSnapshot returns a snapshot copy of fingerprint counters.
func (s *State) FingerprintCountsSnapshot() map[string]uint64 {
if s == nil {
return map[string]uint64{}
}
s.mu.RLock()
if len(s.fingerprintCounts) == 0 {
s.mu.RUnlock()
return map[string]uint64{}
}
out := make(map[string]uint64, len(s.fingerprintCounts))
for k, v := range s.fingerprintCounts {
out[k] = v
}
s.mu.RUnlock()
return out
}
// ResetFingerprintCounts clears all fingerprint counters.
func (s *State) ResetFingerprintCounts() {
if s == nil {
return
}
s.mu.Lock()
s.fingerprintCounts = nil
s.mu.Unlock()
}