Files
s3/weed/query/engine/fast_path_predicate_validation_test.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

273 lines
9.0 KiB
Go

package engine
import (
"testing"
)
// TestFastPathPredicateValidation tests the critical fix for fast-path aggregation
// to ensure non-time predicates are properly detected and fast-path is blocked
func TestFastPathPredicateValidation(t *testing.T) {
engine := NewTestSQLEngine()
testCases := []struct {
name string
whereClause string
expectedTimeOnly bool
expectedStartTimeNs int64
expectedStopTimeNs int64
description string
}{
{
name: "No WHERE clause",
whereClause: "",
expectedTimeOnly: true, // No WHERE means time-only is true
description: "Queries without WHERE clause should allow fast path",
},
{
name: "Time-only predicate (greater than)",
whereClause: "_ts > 1640995200000000000",
expectedTimeOnly: true,
expectedStartTimeNs: 1640995200000000000,
expectedStopTimeNs: 0,
description: "Pure time predicates should allow fast path",
},
{
name: "Time-only predicate (less than)",
whereClause: "_ts < 1640995200000000000",
expectedTimeOnly: true,
expectedStartTimeNs: 0,
expectedStopTimeNs: 1640995200000000000,
description: "Pure time predicates should allow fast path",
},
{
name: "Time-only predicate (range with AND)",
whereClause: "_ts > 1640995200000000000 AND _ts < 1641081600000000000",
expectedTimeOnly: true,
expectedStartTimeNs: 1640995200000000000,
expectedStopTimeNs: 1641081600000000000,
description: "Time range predicates should allow fast path",
},
{
name: "Mixed predicate (time + non-time)",
whereClause: "_ts > 1640995200000000000 AND user_id = 'user123'",
expectedTimeOnly: false,
description: "CRITICAL: Mixed predicates must block fast path to prevent incorrect results",
},
{
name: "Non-time predicate only",
whereClause: "user_id = 'user123'",
expectedTimeOnly: false,
description: "Non-time predicates must block fast path",
},
{
name: "Multiple non-time predicates",
whereClause: "user_id = 'user123' AND status = 'active'",
expectedTimeOnly: false,
description: "Multiple non-time predicates must block fast path",
},
{
name: "OR with time predicate (unsafe)",
whereClause: "_ts > 1640995200000000000 OR user_id = 'user123'",
expectedTimeOnly: false,
description: "OR expressions are complex and must block fast path",
},
{
name: "OR with only time predicates (still unsafe)",
whereClause: "_ts > 1640995200000000000 OR _ts < 1640908800000000000",
expectedTimeOnly: false,
description: "Even time-only OR expressions must block fast path due to complexity",
},
// Note: Parenthesized expressions are not supported by the current parser
// These test cases are commented out until parser support is added
{
name: "String column comparison",
whereClause: "event_type = 'click'",
expectedTimeOnly: false,
description: "String column comparisons must block fast path",
},
{
name: "Numeric column comparison",
whereClause: "id > 1000",
expectedTimeOnly: false,
description: "Numeric column comparisons must block fast path",
},
{
name: "Internal timestamp column",
whereClause: "_ts_ns > 1640995200000000000",
expectedTimeOnly: true,
expectedStartTimeNs: 1640995200000000000,
description: "Internal timestamp column should allow fast path",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Parse the WHERE clause if present
var whereExpr ExprNode
if tc.whereClause != "" {
sql := "SELECT COUNT(*) FROM test WHERE " + tc.whereClause
stmt, err := ParseSQL(sql)
if err != nil {
t.Fatalf("Failed to parse SQL: %v", err)
}
selectStmt := stmt.(*SelectStatement)
whereExpr = selectStmt.Where.Expr
}
// Test the validation function
var startTimeNs, stopTimeNs int64
var onlyTimePredicates bool
if whereExpr == nil {
// No WHERE clause case
onlyTimePredicates = true
} else {
startTimeNs, stopTimeNs, onlyTimePredicates = engine.SQLEngine.extractTimeFiltersWithValidation(whereExpr)
}
// Verify the results
if onlyTimePredicates != tc.expectedTimeOnly {
t.Errorf("Expected onlyTimePredicates=%v, got %v. %s",
tc.expectedTimeOnly, onlyTimePredicates, tc.description)
}
// Check time filters if expected
if tc.expectedStartTimeNs != 0 && startTimeNs != tc.expectedStartTimeNs {
t.Errorf("Expected startTimeNs=%d, got %d", tc.expectedStartTimeNs, startTimeNs)
}
if tc.expectedStopTimeNs != 0 && stopTimeNs != tc.expectedStopTimeNs {
t.Errorf("Expected stopTimeNs=%d, got %d", tc.expectedStopTimeNs, stopTimeNs)
}
t.Logf("%s: onlyTimePredicates=%v, startTimeNs=%d, stopTimeNs=%d",
tc.name, onlyTimePredicates, startTimeNs, stopTimeNs)
})
}
}
// TestFastPathAggregationSafety tests that fast-path aggregation is only attempted
// when it's safe to do so (no non-time predicates)
func TestFastPathAggregationSafety(t *testing.T) {
engine := NewTestSQLEngine()
testCases := []struct {
name string
sql string
shouldUseFastPath bool
description string
}{
{
name: "No WHERE - should use fast path",
sql: "SELECT COUNT(*) FROM test",
shouldUseFastPath: true,
description: "Queries without WHERE should use fast path",
},
{
name: "Time-only WHERE - should use fast path",
sql: "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000",
shouldUseFastPath: true,
description: "Time-only predicates should use fast path",
},
{
name: "Mixed WHERE - should NOT use fast path",
sql: "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000 AND user_id = 'user123'",
shouldUseFastPath: false,
description: "CRITICAL: Mixed predicates must NOT use fast path to prevent wrong results",
},
{
name: "Non-time WHERE - should NOT use fast path",
sql: "SELECT COUNT(*) FROM test WHERE user_id = 'user123'",
shouldUseFastPath: false,
description: "Non-time predicates must NOT use fast path",
},
{
name: "OR expression - should NOT use fast path",
sql: "SELECT COUNT(*) FROM test WHERE _ts > 1640995200000000000 OR user_id = 'user123'",
shouldUseFastPath: false,
description: "OR expressions must NOT use fast path due to complexity",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Parse the SQL
stmt, err := ParseSQL(tc.sql)
if err != nil {
t.Fatalf("Failed to parse SQL: %v", err)
}
selectStmt := stmt.(*SelectStatement)
// Test the fast path decision logic
startTimeNs, stopTimeNs := int64(0), int64(0)
onlyTimePredicates := true
if selectStmt.Where != nil {
startTimeNs, stopTimeNs, onlyTimePredicates = engine.SQLEngine.extractTimeFiltersWithValidation(selectStmt.Where.Expr)
}
canAttemptFastPath := selectStmt.Where == nil || onlyTimePredicates
// Verify the decision
if canAttemptFastPath != tc.shouldUseFastPath {
t.Errorf("Expected canAttemptFastPath=%v, got %v. %s",
tc.shouldUseFastPath, canAttemptFastPath, tc.description)
}
t.Logf("%s: canAttemptFastPath=%v (onlyTimePredicates=%v, startTimeNs=%d, stopTimeNs=%d)",
tc.name, canAttemptFastPath, onlyTimePredicates, startTimeNs, stopTimeNs)
})
}
}
// TestTimestampColumnDetection tests that the engine correctly identifies timestamp columns
func TestTimestampColumnDetection(t *testing.T) {
engine := NewTestSQLEngine()
testCases := []struct {
columnName string
isTimestamp bool
description string
}{
{
columnName: "_ts",
isTimestamp: true,
description: "System timestamp display column should be detected",
},
{
columnName: "_ts_ns",
isTimestamp: true,
description: "Internal timestamp column should be detected",
},
{
columnName: "user_id",
isTimestamp: false,
description: "Non-timestamp column should not be detected as timestamp",
},
{
columnName: "id",
isTimestamp: false,
description: "ID column should not be detected as timestamp",
},
{
columnName: "status",
isTimestamp: false,
description: "Status column should not be detected as timestamp",
},
{
columnName: "event_type",
isTimestamp: false,
description: "Event type column should not be detected as timestamp",
},
}
for _, tc := range testCases {
t.Run(tc.columnName, func(t *testing.T) {
isTimestamp := engine.SQLEngine.isTimestampColumn(tc.columnName)
if isTimestamp != tc.isTimestamp {
t.Errorf("Expected isTimestampColumn(%s)=%v, got %v. %s",
tc.columnName, tc.isTimestamp, isTimestamp, tc.description)
}
t.Logf("Column '%s': isTimestamp=%v", tc.columnName, isTimestamp)
})
}
}