mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-05-24 18:51:31 +00:00
support limit with offset
This commit is contained in:
@@ -3,6 +3,7 @@ package engine
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
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
|
||||
var predicate func(*schema_pb.RecordValue) bool
|
||||
var err error
|
||||
@@ -372,6 +391,29 @@ func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridS
|
||||
if isDebugMode(ctx) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
// 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{
|
||||
StartTimeNs: startTimeNs,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -440,9 +483,31 @@ func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridS
|
||||
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{
|
||||
Columns: columns,
|
||||
Rows: [][]sqltypes.Value{row},
|
||||
Rows: rows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ type WhereClause struct {
|
||||
|
||||
type LimitClause struct {
|
||||
Rowcount ExprNode
|
||||
Offset ExprNode
|
||||
}
|
||||
|
||||
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")
|
||||
if limitIdx != -1 {
|
||||
limitClause := remaining[limitIdx+5:] // Skip "LIMIT"
|
||||
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{
|
||||
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
|
||||
offset := 0
|
||||
if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
|
||||
switch limitExpr := stmt.Limit.Rowcount.(type) {
|
||||
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
|
||||
// Extract time filters from WHERE clause to optimize scanning
|
||||
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
|
||||
StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Predicate: predicate,
|
||||
}
|
||||
|
||||
@@ -1556,8 +1604,9 @@ func (e *SQLEngine) executeSelectStatementWithBrokerStats(ctx context.Context, s
|
||||
}
|
||||
}
|
||||
|
||||
// Parse LIMIT clause
|
||||
// Parse LIMIT and OFFSET clauses
|
||||
limit := 0
|
||||
offset := 0
|
||||
if stmt.Limit != nil && stmt.Limit.Rowcount != nil {
|
||||
switch limitExpr := stmt.Limit.Rowcount.(type) {
|
||||
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
|
||||
// Extract time filters from WHERE clause to optimize scanning
|
||||
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
|
||||
StopTimeNs: stopTimeNs, // Extracted from WHERE clause time comparisons
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Predicate: predicate,
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ type HybridScanOptions struct {
|
||||
// Row limit - 0 means no limit
|
||||
Limit int
|
||||
|
||||
// Row offset - 0 means no offset
|
||||
Offset int
|
||||
|
||||
// Predicate for WHERE clause filtering
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -331,8 +356,8 @@ func (hms *HybridMessageScanner) scanUnflushedDataWithStats(ctx context.Context,
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -497,10 +522,7 @@ func (hms *HybridMessageScanner) scanPartitionHybridWithStats(ctx context.Contex
|
||||
if len(results) == 0 {
|
||||
sampleResults := hms.generateSampleHybridData(options)
|
||||
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
|
||||
@@ -930,10 +952,7 @@ func (hms *HybridMessageScanner) generateSampleHybridData(options HybridScanOpti
|
||||
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
|
||||
}
|
||||
|
||||
249
weed/query/engine/offset_test.go
Normal file
249
weed/query/engine/offset_test.go
Normal file
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -345,6 +345,11 @@ func TestParseSQL_LIMIT_Clauses(t *testing.T) {
|
||||
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)
|
||||
if !ok {
|
||||
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 {
|
||||
|
||||
@@ -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) {
|
||||
engine := NewTestSQLEngine()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user