Browse Source

support limit with offset

pull/7185/head
chrislu 1 month ago
parent
commit
20f61388bf
  1. 71
      weed/query/engine/aggregations.go
  2. 80
      weed/query/engine/engine.go
  3. 45
      weed/query/engine/hybrid_message_scanner.go
  4. 249
      weed/query/engine/offset_test.go
  5. 98
      weed/query/engine/query_parsing_test.go
  6. 86
      weed/query/engine/select_test.go

71
weed/query/engine/aggregations.go

@ -3,6 +3,7 @@ package engine
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/seaweedfs/seaweedfs/weed/mq/topic" "github.com/seaweedfs/seaweedfs/weed/mq/topic"
@ -348,6 +349,24 @@ func (e *SQLEngine) executeAggregationQuery(ctx context.Context, hybridScanner *
// executeAggregationQueryWithPlan handles SELECT queries with aggregation functions and populates execution plan // executeAggregationQueryWithPlan handles SELECT queries with aggregation functions and populates execution plan
func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridScanner *HybridMessageScanner, aggregations []AggregationSpec, stmt *SelectStatement, plan *QueryExecutionPlan) (*QueryResult, error) { func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridScanner *HybridMessageScanner, aggregations []AggregationSpec, stmt *SelectStatement, plan *QueryExecutionPlan) (*QueryResult, error) {
// Parse LIMIT and OFFSET for aggregation results (do this first)
limit := 0
offset := 0
if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
if limitExpr, ok := stmt.Limit.Rowcount.(*SQLVal); ok && limitExpr.Type == IntVal {
if limit64, err := strconv.ParseInt(string(limitExpr.Val), 10, 64); err == nil {
limit = int(limit64)
}
}
}
if stmt.Limit != nil && stmt.Limit.Offset != nil {
if offsetExpr, ok := stmt.Limit.Offset.(*SQLVal); ok && offsetExpr.Type == IntVal {
if offset64, err := strconv.ParseInt(string(offsetExpr.Val), 10, 64); err == nil {
offset = int(offset64)
}
}
}
// Parse WHERE clause for filtering // Parse WHERE clause for filtering
var predicate func(*schema_pb.RecordValue) bool var predicate func(*schema_pb.RecordValue) bool
var err error var err error
@ -372,6 +391,29 @@ func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridS
if isDebugMode(ctx) { if isDebugMode(ctx) {
fmt.Printf("Using fast hybrid statistics for aggregation (parquet stats + live log counts)\n") fmt.Printf("Using fast hybrid statistics for aggregation (parquet stats + live log counts)\n")
} }
// Apply OFFSET and LIMIT to fast path results too
if offset > 0 || limit > 0 {
rows := fastResult.Rows
// Apply OFFSET first
if offset > 0 {
if offset >= len(rows) {
rows = [][]sqltypes.Value{}
} else {
rows = rows[offset:]
}
}
// Apply LIMIT after OFFSET
if limit >= 0 { // Handle LIMIT 0 case
if limit == 0 {
rows = [][]sqltypes.Value{}
} else if len(rows) > limit {
rows = rows[:limit]
}
}
fastResult.Rows = rows
}
return fastResult, nil return fastResult, nil
} }
} }
@ -381,11 +423,12 @@ func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridS
fmt.Printf("Using full table scan for aggregation (parquet optimization not applicable)\n") fmt.Printf("Using full table scan for aggregation (parquet optimization not applicable)\n")
} }
// Build scan options for full table scan (aggregations need all data)
// Build scan options for full table scan (aggregations need all data during scanning)
hybridScanOptions := HybridScanOptions{ hybridScanOptions := HybridScanOptions{
StartTimeNs: startTimeNs, StartTimeNs: startTimeNs,
StopTimeNs: stopTimeNs, StopTimeNs: stopTimeNs,
Limit: 0, // No limit for aggregations - need all data
Limit: 0, // No limit during scanning - need all data for aggregation
Offset: 0, // No offset during scanning - OFFSET applies to final results
Predicate: predicate, Predicate: predicate,
} }
@ -440,9 +483,31 @@ func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridS
row[i] = e.formatAggregationResult(spec, aggResults[i]) row[i] = e.formatAggregationResult(spec, aggResults[i])
} }
// Apply OFFSET and LIMIT to aggregation results
rows := [][]sqltypes.Value{row}
if offset > 0 || limit > 0 {
// Apply OFFSET first
if offset > 0 {
if offset >= len(rows) {
rows = [][]sqltypes.Value{}
} else {
rows = rows[offset:]
}
}
// Apply LIMIT after OFFSET
if limit >= 0 { // Handle LIMIT 0 case
if limit == 0 {
rows = [][]sqltypes.Value{}
} else if len(rows) > limit {
rows = rows[:limit]
}
}
}
return &QueryResult{ return &QueryResult{
Columns: columns, Columns: columns,
Rows: [][]sqltypes.Value{row},
Rows: rows,
}, nil }, nil
} }

80
weed/query/engine/engine.go

@ -85,6 +85,7 @@ type WhereClause struct {
type LimitClause struct { type LimitClause struct {
Rowcount ExprNode Rowcount ExprNode
Offset ExprNode
} }
func (s *SelectStatement) isStatement() {} func (s *SelectStatement) isStatement() {}
@ -381,19 +382,46 @@ func parseSelectStatement(sql string) (*SelectStatement, error) {
} }
} }
// Parse LIMIT clause
// Parse LIMIT clause with optional OFFSET
limitIdx := strings.Index(strings.ToUpper(remaining), "LIMIT") limitIdx := strings.Index(strings.ToUpper(remaining), "LIMIT")
if limitIdx != -1 { if limitIdx != -1 {
limitClause := remaining[limitIdx+5:] // Skip "LIMIT" limitClause := remaining[limitIdx+5:] // Skip "LIMIT"
limitClause = strings.TrimSpace(limitClause) limitClause = strings.TrimSpace(limitClause)
if _, err := strconv.Atoi(limitClause); err == nil {
s.Limit = &LimitClause{
// Check for OFFSET keyword
limitClauseUpper := strings.ToUpper(limitClause)
offsetIdx := strings.Index(limitClauseUpper, "OFFSET")
var limitValue, offsetValue string
if offsetIdx != -1 {
// Parse LIMIT N OFFSET M syntax
limitValue = strings.TrimSpace(limitClause[:offsetIdx])
offsetValue = strings.TrimSpace(limitClause[offsetIdx+6:]) // Skip "OFFSET"
} else {
// Parse LIMIT N syntax only
limitValue = limitClause
}
// Create LIMIT clause
if _, err := strconv.Atoi(limitValue); err == nil {
limitClauseStruct := &LimitClause{
Rowcount: &SQLVal{ Rowcount: &SQLVal{
Type: IntVal, Type: IntVal,
Val: []byte(limitClause),
Val: []byte(limitValue),
}, },
} }
// Add OFFSET if present
if offsetValue != "" {
if _, err := strconv.Atoi(offsetValue); err == nil {
limitClauseStruct.Offset = &SQLVal{
Type: IntVal,
Val: []byte(offsetValue),
}
}
}
s.Limit = limitClauseStruct
} }
} }
} }
@ -1387,8 +1415,9 @@ func (e *SQLEngine) executeSelectStatement(ctx context.Context, stmt *SelectStat
} }
} }
// Parse LIMIT clause
// Parse LIMIT and OFFSET clauses
limit := 0 limit := 0
offset := 0
if stmt.Limit != nil && stmt.Limit.Rowcount != nil { if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
switch limitExpr := stmt.Limit.Rowcount.(type) { switch limitExpr := stmt.Limit.Rowcount.(type) {
case *SQLVal: case *SQLVal:
@ -1406,6 +1435,24 @@ func (e *SQLEngine) executeSelectStatement(ctx context.Context, stmt *SelectStat
} }
} }
// Parse OFFSET clause if present
if stmt.Limit != nil && stmt.Limit.Offset != nil {
switch offsetExpr := stmt.Limit.Offset.(type) {
case *SQLVal:
if offsetExpr.Type == IntVal {
var parseErr error
offset64, parseErr := strconv.ParseInt(string(offsetExpr.Val), 10, 64)
if parseErr != nil {
return &QueryResult{Error: parseErr}, parseErr
}
if offset64 > math.MaxInt32 || offset64 < 0 {
return &QueryResult{Error: fmt.Errorf("OFFSET value %d is out of valid range", offset64)}, fmt.Errorf("OFFSET value %d is out of valid range", offset64)
}
offset = int(offset64)
}
}
}
// Build hybrid scan options // Build hybrid scan options
// Extract time filters from WHERE clause to optimize scanning // Extract time filters from WHERE clause to optimize scanning
startTimeNs, stopTimeNs := int64(0), int64(0) startTimeNs, stopTimeNs := int64(0), int64(0)
@ -1417,6 +1464,7 @@ func (e *SQLEngine) executeSelectStatement(ctx context.Context, stmt *SelectStat
StartTimeNs: startTimeNs, // Extracted from WHERE clause time comparisons StartTimeNs: startTimeNs, // Extracted from WHERE clause time comparisons
StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons
Limit: limit, Limit: limit,
Offset: offset,
Predicate: predicate, Predicate: predicate,
} }
@ -1556,8 +1604,9 @@ func (e *SQLEngine) executeSelectStatementWithBrokerStats(ctx context.Context, s
} }
} }
// Parse LIMIT clause
// Parse LIMIT and OFFSET clauses
limit := 0 limit := 0
offset := 0
if stmt.Limit != nil && stmt.Limit.Rowcount != nil { if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
switch limitExpr := stmt.Limit.Rowcount.(type) { switch limitExpr := stmt.Limit.Rowcount.(type) {
case *SQLVal: case *SQLVal:
@ -1575,6 +1624,24 @@ func (e *SQLEngine) executeSelectStatementWithBrokerStats(ctx context.Context, s
} }
} }
// Parse OFFSET clause if present
if stmt.Limit != nil && stmt.Limit.Offset != nil {
switch offsetExpr := stmt.Limit.Offset.(type) {
case *SQLVal:
if offsetExpr.Type == IntVal {
var parseErr error
offset64, parseErr := strconv.ParseInt(string(offsetExpr.Val), 10, 64)
if parseErr != nil {
return &QueryResult{Error: parseErr}, parseErr
}
if offset64 > math.MaxInt32 || offset64 < 0 {
return &QueryResult{Error: fmt.Errorf("OFFSET value %d is out of valid range", offset64)}, fmt.Errorf("OFFSET value %d is out of valid range", offset64)
}
offset = int(offset64)
}
}
}
// Build hybrid scan options // Build hybrid scan options
// Extract time filters from WHERE clause to optimize scanning // Extract time filters from WHERE clause to optimize scanning
startTimeNs, stopTimeNs := int64(0), int64(0) startTimeNs, stopTimeNs := int64(0), int64(0)
@ -1586,6 +1653,7 @@ func (e *SQLEngine) executeSelectStatementWithBrokerStats(ctx context.Context, s
StartTimeNs: startTimeNs, // Extracted from WHERE clause time comparisons StartTimeNs: startTimeNs, // Extracted from WHERE clause time comparisons
StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons
Limit: limit, Limit: limit,
Offset: offset,
Predicate: predicate, Predicate: predicate,
} }

45
weed/query/engine/hybrid_message_scanner.go

@ -103,6 +103,9 @@ type HybridScanOptions struct {
// Row limit - 0 means no limit // Row limit - 0 means no limit
Limit int Limit int
// Row offset - 0 means no offset
Offset int
// Predicate for WHERE clause filtering // Predicate for WHERE clause filtering
Predicate func(*schema_pb.RecordValue) bool Predicate func(*schema_pb.RecordValue) bool
} }
@ -222,13 +225,35 @@ func (hms *HybridMessageScanner) ScanWithStats(ctx context.Context, options Hybr
} }
} }
// Apply global limit across all partitions
if options.Limit > 0 && len(results) >= options.Limit {
results = results[:options.Limit]
// Apply global limit (without offset) across all partitions
// Note: OFFSET will be applied at the end to avoid double-application
if options.Limit > 0 && len(results) >= options.Limit+options.Offset {
break break
} }
} }
// Apply final OFFSET and LIMIT processing (done once at the end)
if options.Offset > 0 || options.Limit >= 0 {
// Handle LIMIT 0 special case - return empty result immediately
if options.Limit == 0 {
results = []HybridScanResult{}
} else {
// Apply OFFSET first
if options.Offset > 0 {
if options.Offset >= len(results) {
results = []HybridScanResult{}
} else {
results = results[options.Offset:]
}
}
// Apply LIMIT after OFFSET
if options.Limit > 0 && len(results) > options.Limit {
results = results[:options.Limit]
}
}
}
return results, stats, nil return results, stats, nil
} }
@ -331,8 +356,8 @@ func (hms *HybridMessageScanner) scanUnflushedDataWithStats(ctx context.Context,
results = append(results, result) results = append(results, result)
// Apply limit
if options.Limit > 0 && len(results) >= options.Limit {
// Apply limit (accounting for offset) - collect more data than needed
if options.Limit > 0 && len(results) >= options.Offset+options.Limit {
break break
} }
} }
@ -497,10 +522,7 @@ func (hms *HybridMessageScanner) scanPartitionHybridWithStats(ctx context.Contex
if len(results) == 0 { if len(results) == 0 {
sampleResults := hms.generateSampleHybridData(options) sampleResults := hms.generateSampleHybridData(options)
results = append(results, sampleResults...) results = append(results, sampleResults...)
// Apply limit to sample data as well
if options.Limit > 0 && len(results) > options.Limit {
results = results[:options.Limit]
}
// Note: OFFSET and LIMIT will be applied at the end of the main scan function
} }
return results, stats, nil return results, stats, nil
@ -930,10 +952,7 @@ func (hms *HybridMessageScanner) generateSampleHybridData(options HybridScanOpti
sampleData = filtered sampleData = filtered
} }
// Apply limit
if options.Limit > 0 && len(sampleData) > options.Limit {
sampleData = sampleData[:options.Limit]
}
// Note: OFFSET and LIMIT will be applied at the end of the main scan function
return sampleData return sampleData
} }

249
weed/query/engine/offset_test.go

@ -0,0 +1,249 @@
package engine
import (
"context"
"strconv"
"strings"
"testing"
)
// TestParseSQL_OFFSET_EdgeCases tests edge cases for OFFSET parsing
func TestParseSQL_OFFSET_EdgeCases(t *testing.T) {
tests := []struct {
name string
sql string
wantErr bool
validate func(t *testing.T, stmt Statement, err error)
}{
{
name: "Valid LIMIT OFFSET with WHERE",
sql: "SELECT * FROM users WHERE age > 18 LIMIT 10 OFFSET 5",
wantErr: false,
validate: func(t *testing.T, stmt Statement, err error) {
selectStmt := stmt.(*SelectStatement)
if selectStmt.Limit == nil {
t.Fatal("Expected LIMIT clause, got nil")
}
if selectStmt.Limit.Offset == nil {
t.Fatal("Expected OFFSET clause, got nil")
}
if selectStmt.Where == nil {
t.Fatal("Expected WHERE clause, got nil")
}
},
},
{
name: "LIMIT OFFSET with mixed case",
sql: "select * from users limit 5 offset 3",
wantErr: false,
validate: func(t *testing.T, stmt Statement, err error) {
selectStmt := stmt.(*SelectStatement)
offsetVal := selectStmt.Limit.Offset.(*SQLVal)
if string(offsetVal.Val) != "3" {
t.Errorf("Expected offset value '3', got '%s'", string(offsetVal.Val))
}
},
},
{
name: "LIMIT OFFSET with extra spaces",
sql: "SELECT * FROM users LIMIT 10 OFFSET 20 ",
wantErr: false,
validate: func(t *testing.T, stmt Statement, err error) {
selectStmt := stmt.(*SelectStatement)
limitVal := selectStmt.Limit.Rowcount.(*SQLVal)
offsetVal := selectStmt.Limit.Offset.(*SQLVal)
if string(limitVal.Val) != "10" {
t.Errorf("Expected limit value '10', got '%s'", string(limitVal.Val))
}
if string(offsetVal.Val) != "20" {
t.Errorf("Expected offset value '20', got '%s'", string(offsetVal.Val))
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stmt, err := ParseSQL(tt.sql)
if tt.wantErr {
if err == nil {
t.Errorf("Expected error, but got none")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if tt.validate != nil {
tt.validate(t, stmt, err)
}
})
}
}
// TestSQLEngine_OFFSET_EdgeCases tests edge cases for OFFSET execution
func TestSQLEngine_OFFSET_EdgeCases(t *testing.T) {
engine := NewTestSQLEngine()
t.Run("OFFSET larger than result set", func(t *testing.T) {
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 5 OFFSET 100")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// Should return empty result set
if len(result.Rows) != 0 {
t.Errorf("Expected 0 rows when OFFSET > total rows, got %d", len(result.Rows))
}
})
t.Run("OFFSET with LIMIT 0", func(t *testing.T) {
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 0 OFFSET 2")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// LIMIT 0 should return no rows regardless of OFFSET
if len(result.Rows) != 0 {
t.Errorf("Expected 0 rows with LIMIT 0, got %d", len(result.Rows))
}
})
t.Run("High OFFSET with small LIMIT", func(t *testing.T) {
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 1 OFFSET 3")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// With 4 sample rows, OFFSET 3 LIMIT 1 should return 1 row (the last one)
if len(result.Rows) != 1 {
t.Errorf("Expected 1 row with LIMIT 1 OFFSET 3, got %d", len(result.Rows))
}
})
}
// TestSQLEngine_OFFSET_ErrorCases tests error conditions for OFFSET
func TestSQLEngine_OFFSET_ErrorCases(t *testing.T) {
engine := NewTestSQLEngine()
// Test negative OFFSET - should be caught during execution
t.Run("Negative OFFSET value", func(t *testing.T) {
// Note: This would need to be implemented as validation in the execution engine
// For now, we test that the parser accepts it but execution might handle it
_, err := ParseSQL("SELECT * FROM users LIMIT 10 OFFSET -5")
if err != nil {
t.Logf("Parser rejected negative OFFSET (this is expected): %v", err)
} else {
// Parser accepts it, execution should handle validation
t.Logf("Parser accepts negative OFFSET, execution should validate")
}
})
// Test very large OFFSET
t.Run("Very large OFFSET value", func(t *testing.T) {
largeOffset := "2147483647" // Max int32
sql := "SELECT * FROM user_events LIMIT 1 OFFSET " + largeOffset
result, err := engine.ExecuteSQL(context.Background(), sql)
if err != nil {
// Large OFFSET might cause parsing or execution errors
if strings.Contains(err.Error(), "out of valid range") {
t.Logf("Large OFFSET properly rejected: %v", err)
} else {
t.Errorf("Unexpected error for large OFFSET: %v", err)
}
} else if result.Error != nil {
if strings.Contains(result.Error.Error(), "out of valid range") {
t.Logf("Large OFFSET properly rejected during execution: %v", result.Error)
} else {
t.Errorf("Unexpected execution error for large OFFSET: %v", result.Error)
}
} else {
// Should return empty result for very large offset
if len(result.Rows) != 0 {
t.Errorf("Expected 0 rows for very large OFFSET, got %d", len(result.Rows))
}
}
})
}
// TestSQLEngine_OFFSET_Consistency tests that OFFSET produces consistent results
func TestSQLEngine_OFFSET_Consistency(t *testing.T) {
engine := NewTestSQLEngine()
// Get all rows first
allResult, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events")
if err != nil {
t.Fatalf("Failed to get all rows: %v", err)
}
if allResult.Error != nil {
t.Fatalf("Failed to get all rows: %v", allResult.Error)
}
totalRows := len(allResult.Rows)
if totalRows == 0 {
t.Skip("No data available for consistency test")
}
// Test that OFFSET + remaining rows = total rows
for offset := 0; offset < totalRows; offset++ {
t.Run("OFFSET_"+strconv.Itoa(offset), func(t *testing.T) {
sql := "SELECT * FROM user_events LIMIT 100 OFFSET " + strconv.Itoa(offset)
result, err := engine.ExecuteSQL(context.Background(), sql)
if err != nil {
t.Fatalf("Error with OFFSET %d: %v", offset, err)
}
if result.Error != nil {
t.Fatalf("Query error with OFFSET %d: %v", offset, result.Error)
}
expectedRows := totalRows - offset
if len(result.Rows) != expectedRows {
t.Errorf("OFFSET %d: expected %d rows, got %d", offset, expectedRows, len(result.Rows))
}
})
}
}
// TestSQLEngine_OFFSET_WithAggregation tests OFFSET with aggregation queries
func TestSQLEngine_OFFSET_WithAggregation(t *testing.T) {
engine := NewTestSQLEngine()
// Note: Aggregation queries typically return single rows, so OFFSET behavior is different
t.Run("COUNT with OFFSET", func(t *testing.T) {
result, err := engine.ExecuteSQL(context.Background(), "SELECT COUNT(*) FROM user_events LIMIT 1 OFFSET 0")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// COUNT typically returns 1 row, so OFFSET 0 should return that row
if len(result.Rows) != 1 {
t.Errorf("Expected 1 row for COUNT with OFFSET 0, got %d", len(result.Rows))
}
})
t.Run("COUNT with OFFSET 1", func(t *testing.T) {
result, err := engine.ExecuteSQL(context.Background(), "SELECT COUNT(*) FROM user_events LIMIT 1 OFFSET 1")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// COUNT returns 1 row, so OFFSET 1 should return 0 rows
if len(result.Rows) != 0 {
t.Errorf("Expected 0 rows for COUNT with OFFSET 1, got %d", len(result.Rows))
}
})
}

98
weed/query/engine/query_parsing_test.go

@ -345,6 +345,11 @@ func TestParseSQL_LIMIT_Clauses(t *testing.T) {
t.Error("Expected LIMIT rowcount, got nil") t.Error("Expected LIMIT rowcount, got nil")
} }
// Verify no OFFSET is set
if selectStmt.Limit.Offset != nil {
t.Error("Expected OFFSET to be nil for LIMIT-only query")
}
sqlVal, ok := selectStmt.Limit.Rowcount.(*SQLVal) sqlVal, ok := selectStmt.Limit.Rowcount.(*SQLVal)
if !ok { if !ok {
t.Errorf("Expected *SQLVal, got %T", selectStmt.Limit.Rowcount) t.Errorf("Expected *SQLVal, got %T", selectStmt.Limit.Rowcount)
@ -359,6 +364,99 @@ func TestParseSQL_LIMIT_Clauses(t *testing.T) {
} }
}, },
}, },
{
name: "LIMIT with OFFSET",
sql: "SELECT * FROM users LIMIT 10 OFFSET 5",
wantErr: false,
validate: func(t *testing.T, stmt Statement) {
selectStmt := stmt.(*SelectStatement)
if selectStmt.Limit == nil {
t.Fatal("Expected LIMIT clause, got nil")
}
// Verify LIMIT value
if selectStmt.Limit.Rowcount == nil {
t.Error("Expected LIMIT rowcount, got nil")
}
limitVal, ok := selectStmt.Limit.Rowcount.(*SQLVal)
if !ok {
t.Errorf("Expected *SQLVal for LIMIT, got %T", selectStmt.Limit.Rowcount)
}
if limitVal.Type != IntVal {
t.Errorf("Expected IntVal type for LIMIT, got %d", limitVal.Type)
}
if string(limitVal.Val) != "10" {
t.Errorf("Expected limit value '10', got '%s'", string(limitVal.Val))
}
// Verify OFFSET value
if selectStmt.Limit.Offset == nil {
t.Fatal("Expected OFFSET clause, got nil")
}
offsetVal, ok := selectStmt.Limit.Offset.(*SQLVal)
if !ok {
t.Errorf("Expected *SQLVal for OFFSET, got %T", selectStmt.Limit.Offset)
}
if offsetVal.Type != IntVal {
t.Errorf("Expected IntVal type for OFFSET, got %d", offsetVal.Type)
}
if string(offsetVal.Val) != "5" {
t.Errorf("Expected offset value '5', got '%s'", string(offsetVal.Val))
}
},
},
{
name: "LIMIT with OFFSET zero",
sql: "SELECT * FROM users LIMIT 5 OFFSET 0",
wantErr: false,
validate: func(t *testing.T, stmt Statement) {
selectStmt := stmt.(*SelectStatement)
if selectStmt.Limit == nil {
t.Fatal("Expected LIMIT clause, got nil")
}
// Verify OFFSET is 0
if selectStmt.Limit.Offset == nil {
t.Fatal("Expected OFFSET clause, got nil")
}
offsetVal, ok := selectStmt.Limit.Offset.(*SQLVal)
if !ok {
t.Errorf("Expected *SQLVal for OFFSET, got %T", selectStmt.Limit.Offset)
}
if string(offsetVal.Val) != "0" {
t.Errorf("Expected offset value '0', got '%s'", string(offsetVal.Val))
}
},
},
{
name: "LIMIT with large OFFSET",
sql: "SELECT * FROM users LIMIT 100 OFFSET 1000",
wantErr: false,
validate: func(t *testing.T, stmt Statement) {
selectStmt := stmt.(*SelectStatement)
if selectStmt.Limit == nil {
t.Fatal("Expected LIMIT clause, got nil")
}
// Verify large OFFSET value
offsetVal, ok := selectStmt.Limit.Offset.(*SQLVal)
if !ok {
t.Errorf("Expected *SQLVal for OFFSET, got %T", selectStmt.Limit.Offset)
}
if string(offsetVal.Val) != "1000" {
t.Errorf("Expected offset value '1000', got '%s'", string(offsetVal.Val))
}
},
},
} }
for _, tt := range tests { for _, tt := range tests {

86
weed/query/engine/select_test.go

@ -92,6 +92,92 @@ func TestSQLEngine_SelectFromNonExistentTable(t *testing.T) {
} }
} }
func TestSQLEngine_SelectWithOffset(t *testing.T) {
engine := NewTestSQLEngine()
// Test SELECT with OFFSET only
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 10 OFFSET 1")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// Should have fewer rows than total since we skip 1 row
// Sample data has 4 rows, so OFFSET 1 should give us 3 rows
if len(result.Rows) != 3 {
t.Errorf("Expected 3 rows with OFFSET 1 (4 total - 1 offset), got %d", len(result.Rows))
}
}
func TestSQLEngine_SelectWithLimitAndOffset(t *testing.T) {
engine := NewTestSQLEngine()
// Test SELECT with both LIMIT and OFFSET
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 2 OFFSET 1")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// Should have exactly 2 rows (skip 1, take 2)
if len(result.Rows) != 2 {
t.Errorf("Expected 2 rows with LIMIT 2 OFFSET 1, got %d", len(result.Rows))
}
}
func TestSQLEngine_SelectWithOffsetExceedsRows(t *testing.T) {
engine := NewTestSQLEngine()
// Test OFFSET that exceeds available rows
result, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 10 OFFSET 10")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result.Error != nil {
t.Fatalf("Expected no query error, got %v", result.Error)
}
// Should have 0 rows since offset exceeds available data
if len(result.Rows) != 0 {
t.Errorf("Expected 0 rows with large OFFSET, got %d", len(result.Rows))
}
}
func TestSQLEngine_SelectWithOffsetZero(t *testing.T) {
engine := NewTestSQLEngine()
// Test OFFSET 0 (should be same as no offset)
result1, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 3")
if err != nil {
t.Fatalf("Expected no error for LIMIT query, got %v", err)
}
result2, err := engine.ExecuteSQL(context.Background(), "SELECT * FROM user_events LIMIT 3 OFFSET 0")
if err != nil {
t.Fatalf("Expected no error for LIMIT OFFSET query, got %v", err)
}
if result1.Error != nil {
t.Fatalf("Expected no query error for LIMIT, got %v", result1.Error)
}
if result2.Error != nil {
t.Fatalf("Expected no query error for LIMIT OFFSET, got %v", result2.Error)
}
// Both should return the same number of rows
if len(result1.Rows) != len(result2.Rows) {
t.Errorf("LIMIT 3 and LIMIT 3 OFFSET 0 should return same number of rows. Got %d vs %d", len(result1.Rows), len(result2.Rows))
}
}
func TestSQLEngine_SelectDifferentTables(t *testing.T) { func TestSQLEngine_SelectDifferentTables(t *testing.T) {
engine := NewTestSQLEngine() engine := NewTestSQLEngine()

Loading…
Cancel
Save