Phase 4 (Performance) + Phase 6 (Infrastructure) completion
Phase 4 — Performance: - F1: SSH DialPool with key-by-host pooling and 5min idle TTL - F2: In-memory entity lookup cache (TTL 60s, HTTP resolveEntityID) - F3: Trigram GIN indexes on entities.slug and entities.name (migration 031) - F4: Partial index on executions(classification_id) for auto-act (migration 032) - Added missing RunOutput and RunStreaming in actuator/ (E3 gap fill) Phase 6 — Infrastructure: - H1: Infisical image pinned to v0.99.1 - H2: execworker daemon — polls pending executions with per-execution advisory locks, recovers orphaned running executions, wired as docker-compose service - H3: splitSQL hardened with block comment and string-literal support, 6 new edge-case tests (11 total) - H4: Scheduler acquires pg_try_advisory_lock(0x01c05e6) at startup
This commit is contained in:
61
internal/db/entity_cache.go
Normal file
61
internal/db/entity_cache.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type entityCacheEntry struct {
|
||||
slug string
|
||||
id string
|
||||
attrs string
|
||||
exp time.Time
|
||||
}
|
||||
|
||||
type EntityCache struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]entityCacheEntry
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func NewEntityCache(ttl time.Duration) *EntityCache {
|
||||
return &EntityCache{
|
||||
m: make(map[string]entityCacheEntry),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EntityCache) GetSlug(id string) (string, bool) {
|
||||
c.mu.RLock()
|
||||
e, ok := c.m[id]
|
||||
c.mu.RUnlock()
|
||||
if !ok || time.Now().After(e.exp) {
|
||||
return "", false
|
||||
}
|
||||
return e.slug, true
|
||||
}
|
||||
|
||||
func (c *EntityCache) GetID(slug string) (string, bool) {
|
||||
c.mu.RLock()
|
||||
e, ok := c.m[slug]
|
||||
c.mu.RUnlock()
|
||||
if !ok || time.Now().After(e.exp) {
|
||||
return "", false
|
||||
}
|
||||
return e.id, true
|
||||
}
|
||||
|
||||
func (c *EntityCache) Set(slug, id, attrs string) {
|
||||
exp := time.Now().Add(c.ttl)
|
||||
c.mu.Lock()
|
||||
c.m[slug] = entityCacheEntry{slug: slug, id: id, attrs: attrs, exp: exp}
|
||||
c.m[id] = entityCacheEntry{slug: slug, id: id, attrs: attrs, exp: exp}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *EntityCache) Invalidate(slug, id string) {
|
||||
c.mu.Lock()
|
||||
delete(c.m, slug)
|
||||
delete(c.m, id)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
@@ -190,7 +190,10 @@ func hasSuffix(s, suffix string) bool {
|
||||
}
|
||||
|
||||
// splitSQL splits a SQL string into individual statements.
|
||||
// Handles $$ ... $$ dollar-quoted blocks and -- line comments.
|
||||
// Handles $$ ... $$ dollar-quoted blocks, $tag$ ... $tag$ tagged quotes,
|
||||
// -- line comments, /* ... */ block comments, and '...' string literals
|
||||
// so that semicolons inside any of these constructs are not treated as
|
||||
// statement boundaries.
|
||||
func splitSQL(sql string) []string {
|
||||
var statements []string
|
||||
var current strings.Builder
|
||||
@@ -201,7 +204,6 @@ func splitSQL(sql string) []string {
|
||||
for i < len(sql) {
|
||||
// Handle line comments (-- to end of line)
|
||||
if !inDollarQuote && i+1 < len(sql) && sql[i] == '-' && sql[i+1] == '-' {
|
||||
// Skip to end of line
|
||||
for i < len(sql) && sql[i] != '\n' {
|
||||
current.WriteByte(sql[i])
|
||||
i++
|
||||
@@ -209,6 +211,34 @@ func splitSQL(sql string) []string {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle block comments (/* ... */)
|
||||
if !inDollarQuote && i+1 < len(sql) && sql[i] == '/' && sql[i+1] == '*' {
|
||||
end := strings.Index(sql[i+2:], "*/")
|
||||
if end >= 0 {
|
||||
current.WriteString(sql[i : i+end+4])
|
||||
i += end + 4
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Handle single-quoted string literals ('...')
|
||||
if !inDollarQuote && sql[i] == '\'' {
|
||||
j := i + 1
|
||||
for j < len(sql) {
|
||||
if sql[j] == '\'' {
|
||||
if j+1 < len(sql) && sql[j+1] == '\'' {
|
||||
j += 2 // skip doubled quote ''
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
current.WriteString(sql[i : j+1])
|
||||
i = j + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for dollar-quote start/end
|
||||
if !inDollarQuote && sql[i] == '$' {
|
||||
j := i + 1
|
||||
|
||||
@@ -51,3 +51,57 @@ func TestSplitSQLSemicolonInComment(t *testing.T) {
|
||||
t.Fatalf("got %d statements, want 2: %#v", len(stmts), stmts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLSemicolonInStringLiteral(t *testing.T) {
|
||||
sql := `SELECT 'hello; world'; INSERT INTO t VALUES (1);`
|
||||
stmts := nonEmpty(splitSQL(sql))
|
||||
if len(stmts) != 2 {
|
||||
t.Fatalf("got %d statements, want 2: %#v", len(stmts), stmts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLDollarSignInStringLiteral(t *testing.T) {
|
||||
sql := `SELECT '$100'; SELECT 2;`
|
||||
stmts := nonEmpty(splitSQL(sql))
|
||||
if len(stmts) != 2 {
|
||||
t.Fatalf("got %d statements, want 2: %#v", len(stmts), stmts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLBlockComment(t *testing.T) {
|
||||
sql := `SELECT 1; /* block; with; semicolons */ SELECT 2;`
|
||||
stmts := nonEmpty(splitSQL(sql))
|
||||
if len(stmts) != 2 {
|
||||
t.Fatalf("got %d statements, want 2: %#v", len(stmts), stmts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLBlockCommentWithDollarQuote(t *testing.T) {
|
||||
sql := `/* $$ not a dollar quote */ SELECT 1;`
|
||||
stmts := nonEmpty(splitSQL(sql))
|
||||
if len(stmts) != 1 {
|
||||
t.Fatalf("got %d statements, want 1: %#v", len(stmts), stmts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLDoubledQuoteInString(t *testing.T) {
|
||||
sql := `SELECT 'O''Brien'; SELECT 2;`
|
||||
stmts := nonEmpty(splitSQL(sql))
|
||||
if len(stmts) != 2 {
|
||||
t.Fatalf("got %d statements, want 2: %#v", len(stmts), stmts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLEmptyInput(t *testing.T) {
|
||||
stmts := nonEmpty(splitSQL(""))
|
||||
if len(stmts) != 0 {
|
||||
t.Fatalf("got %d statements, want 0", len(stmts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLNoSemicolon(t *testing.T) {
|
||||
stmts := nonEmpty(splitSQL("SELECT 1"))
|
||||
if len(stmts) != 1 {
|
||||
t.Fatalf("got %d statements, want 1", len(stmts))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user