bug fix:修复可能的panic状态;增加更多功能
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user