From 02d36637de481051e9f3445210583f9d019a7cfa Mon Sep 17 00:00:00 2001 From: chrislu Date: Thu, 4 Sep 2025 09:13:54 -0700 Subject: [PATCH] limit with offset --- weed/query/engine/aggregations.go | 63 +++++++++++---------- weed/query/engine/engine.go | 6 +- weed/query/engine/hybrid_message_scanner.go | 29 +++++----- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/weed/query/engine/aggregations.go b/weed/query/engine/aggregations.go index 1bbeda456..44cf9fbcb 100644 --- a/weed/query/engine/aggregations.go +++ b/weed/query/engine/aggregations.go @@ -350,7 +350,8 @@ 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 + // Use -1 to distinguish "no LIMIT" from "LIMIT 0" + limit := -1 offset := 0 if stmt.Limit != nil && stmt.Limit.Rowcount != nil { if limitExpr, ok := stmt.Limit.Rowcount.(*SQLVal); ok && limitExpr.Type == IntVal { @@ -391,29 +392,31 @@ 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 { + // Limit semantics: -1 = no limit, 0 = LIMIT 0 (empty), >0 = limit to N rows + 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:] + // Handle LIMIT 0 first + if limit == 0 { + rows = [][]sqltypes.Value{} + } else { + // 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 { + // Apply LIMIT after OFFSET (only if limit > 0) + if limit > 0 && len(rows) > limit { rows = rows[:limit] } } fastResult.Rows = rows } - + return fastResult, nil } } @@ -484,22 +487,24 @@ func (e *SQLEngine) executeAggregationQueryWithPlan(ctx context.Context, hybridS } // Apply OFFSET and LIMIT to aggregation results + // Limit semantics: -1 = no limit, 0 = LIMIT 0 (empty), >0 = limit to N rows 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:] + if offset > 0 || limit >= 0 { + // Handle LIMIT 0 first + if limit == 0 { + rows = [][]sqltypes.Value{} + } else { + // 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 { + // Apply LIMIT after OFFSET (only if limit > 0) + if limit > 0 && len(rows) > limit { rows = rows[:limit] } } diff --git a/weed/query/engine/engine.go b/weed/query/engine/engine.go index cd8c7d57c..985bcd4fe 100644 --- a/weed/query/engine/engine.go +++ b/weed/query/engine/engine.go @@ -1416,7 +1416,8 @@ func (e *SQLEngine) executeSelectStatement(ctx context.Context, stmt *SelectStat } // Parse LIMIT and OFFSET clauses - limit := 0 + // Use -1 to distinguish "no LIMIT" from "LIMIT 0" + limit := -1 offset := 0 if stmt.Limit != nil && stmt.Limit.Rowcount != nil { switch limitExpr := stmt.Limit.Rowcount.(type) { @@ -1605,7 +1606,8 @@ func (e *SQLEngine) executeSelectStatementWithBrokerStats(ctx context.Context, s } // Parse LIMIT and OFFSET clauses - limit := 0 + // Use -1 to distinguish "no LIMIT" from "LIMIT 0" + limit := -1 offset := 0 if stmt.Limit != nil && stmt.Limit.Rowcount != nil { switch limitExpr := stmt.Limit.Rowcount.(type) { diff --git a/weed/query/engine/hybrid_message_scanner.go b/weed/query/engine/hybrid_message_scanner.go index 0f76de2fd..14c784615 100644 --- a/weed/query/engine/hybrid_message_scanner.go +++ b/weed/query/engine/hybrid_message_scanner.go @@ -233,25 +233,26 @@ func (hms *HybridMessageScanner) ScanWithStats(ctx context.Context, options Hybr } // Apply final OFFSET and LIMIT processing (done once at the end) + // Limit semantics: -1 = no limit, 0 = LIMIT 0 (empty), >0 = limit to N rows if options.Offset > 0 || options.Limit >= 0 { - // Handle LIMIT 0 special case - return empty result immediately + // Handle LIMIT 0 special case first 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:] - } - } + return []HybridScanResult{}, stats, nil + } - // Apply LIMIT after OFFSET - if options.Limit > 0 && len(results) > options.Limit { - results = results[:options.Limit] + // Apply OFFSET first + if options.Offset > 0 { + if options.Offset >= len(results) { + results = []HybridScanResult{} + } else { + results = results[options.Offset:] } } + + // Apply LIMIT after OFFSET (only if limit > 0) + if options.Limit > 0 && len(results) > options.Limit { + results = results[:options.Limit] + } } return results, stats, nil