Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions pkg/rain/compile_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ func BenchmarkSelectToSQL(b *testing.B) {
ToSQL()
}
})

b.Run("InClause1000", func(b *testing.B) {
vals := make([]int64, 1000)
for i := range 1000 {
vals[i] = int64(i)
}

b.ReportAllocs()
b.ResetTimer()
for range b.N {
_, _, _ = db.Select().
Table(users).
Where(users.ID.In(vals...)).
ToSQL()
}
})
}

func BenchmarkInsertToSQL(b *testing.B) {
Expand Down
31 changes: 27 additions & 4 deletions pkg/rain/query_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ func (c *compileContext) writeJoinedPredicates(predicates []schema.Predicate, wr
return c.writePredicate(predicates[0])
}

c.ensureArgsCapacity(len(predicates))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Capacity hint treats each predicate as contributing exactly one arg

ensureArgsCapacity(len(predicates)) assumes one arg per predicate, but a single predicate can easily contribute many more β€” an IN(1000) predicate contributes 1000, a BETWEEN contributes 2, a nested AND/OR contributes the sum of its children. When these compound predicates are involved, the outer hint is immediately exhausted by the very first child, leaving all subsequent children's ensureArgsCapacity calls to each allocate exactly their own minimum. This doesn't affect correctness, but for WHERE clauses mixing simple and complex predicates the allocation savings can fall well short of what was intended.

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/rain/query_compile.go
Line: 311

Comment:
**Capacity hint treats each predicate as contributing exactly one arg**

`ensureArgsCapacity(len(predicates))` assumes one arg per predicate, but a single predicate can easily contribute many more β€” an `IN(1000)` predicate contributes 1000, a `BETWEEN` contributes 2, a nested `AND`/`OR` contributes the sum of its children. When these compound predicates are involved, the outer hint is immediately exhausted by the very first child, leaving all subsequent children's `ensureArgsCapacity` calls to each allocate exactly their own minimum. This doesn't affect correctness, but for WHERE clauses mixing simple and complex predicates the allocation savings can fall well short of what was intended.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex


if wrap {
c.writeByte('(')
}
Expand Down Expand Up @@ -398,6 +400,7 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
default:
return fmt.Errorf("rain: invalid binary operator %q", value.Operator)
}
c.ensureArgsCapacity(2)
c.writeByte('(')
if err := c.writeExpression(value.Left); err != nil {
return err
Expand All @@ -413,6 +416,7 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
if len(value.Exprs) < 2 {
return errors.New("rain: CONCAT requires at least two expressions")
}
c.ensureArgsCapacity(len(value.Exprs))
switch c.dialect.Name() {
case "postgres", "sqlite":
c.writeByte('(')
Expand Down Expand Up @@ -443,6 +447,7 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
if len(value.Values) == 0 {
return errors.New("rain: IN predicate requires at least one value")
}
c.ensureArgsCapacity(len(value.Values) + 1)
if err := c.writeExpression(value.Left); err != nil {
return err
}
Expand All @@ -451,20 +456,23 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
} else {
c.writeString(" IN (")
}

// OPTIMIZATION: Hoist expressionContext outside the loop and reuse it.
ctx := expressionContext{}
if len(value.Values) == 1 {
ctx.noParens = true
}
for idx, item := range value.Values {
if idx > 0 {
c.writeString(", ")
}
ctx := expressionContext{}
if len(value.Values) == 1 {
ctx.noParens = true
}
if err := c.writeExpressionInContext(item, ctx); err != nil {
return err
}
}
c.writeByte(')')
case schema.BetweenExpr:
c.ensureArgsCapacity(3)
if err := c.writeExpression(value.Left); err != nil {
return err
}
Expand Down Expand Up @@ -515,6 +523,7 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
c.writeString(" IS NULL")
}
case schema.LogicalExpr:
c.ensureArgsCapacity(len(value.Exprs))
c.writeByte('(')
for idx, part := range value.Exprs {
if idx > 0 {
Expand All @@ -531,6 +540,16 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
if len(value.WhenThenPairs) == 0 {
return errors.New("rain: CASE expression requires at least one WHEN clause")
}
// OPTIMIZATION: Hint capacity for WHEN/THEN pairs plus optional value and else.
n := len(value.WhenThenPairs) * 2
if value.ValueExpression != nil {
n++
}
if value.ElseExpression != nil {
n++
}
c.ensureArgsCapacity(n)

c.writeByte('(')
c.writeString("CASE")
if value.ValueExpression != nil {
Expand Down Expand Up @@ -584,6 +603,7 @@ func (c *compileContext) writeExpressionInContext(expr schema.Expression, contex
if len(value.Exprs) < 2 {
return errors.New("rain: COALESCE requires at least two expressions")
}
c.ensureArgsCapacity(len(value.Exprs))
c.writeString("COALESCE(")
for idx, part := range value.Exprs {
if part == nil {
Expand Down Expand Up @@ -631,6 +651,9 @@ func (c *compileContext) writeColumnName(column schema.ColumnReference) {
}

func (c *compileContext) writeRaw(raw schema.RawExpr) error {
if len(raw.Args) > 0 {
c.ensureArgsCapacity(len(raw.Args))
}
argIndex := 0
for idx := range len(raw.SQL) {
if raw.SQL[idx] != '?' {
Expand Down