Skip to content

Commit

Permalink
fix #186: use args to set LIMIT and/or OFFSET
Browse files Browse the repository at this point in the history
  • Loading branch information
huandu committed Jan 26, 2025
1 parent b0b0b02 commit 5677f58
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 92 deletions.
99 changes: 57 additions & 42 deletions select.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package sqlbuilder

import (
"fmt"
"strconv"
"strings"
)

Expand Down Expand Up @@ -51,8 +50,6 @@ func newSelectBuilder() *SelectBuilder {
Cond: Cond{
Args: args,
},
limit: -1,
offset: -1,
args: args,
injection: newInjection(),
}
Expand All @@ -79,8 +76,8 @@ type SelectBuilder struct {
groupByCols []string
orderByCols []string
order string
limit int
offset int
limitVar string
offsetVar string
forWhat string

args *Args
Expand Down Expand Up @@ -249,14 +246,24 @@ func (sb *SelectBuilder) Desc() *SelectBuilder {

// Limit sets the LIMIT in SELECT.
func (sb *SelectBuilder) Limit(limit int) *SelectBuilder {
sb.limit = limit
if limit < 0 {
sb.limitVar = ""
return sb
}

sb.limitVar = sb.Var(limit)
sb.marker = selectMarkerAfterLimit
return sb
}

// Offset sets the LIMIT offset in SELECT.
func (sb *SelectBuilder) Offset(offset int) *SelectBuilder {
sb.offset = offset
if offset < 0 {
sb.offsetVar = ""
return sb
}

sb.offsetVar = sb.Var(offset)
sb.marker = selectMarkerAfterLimit
return sb
}
Expand Down Expand Up @@ -314,7 +321,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
buf := newStringBuilder()
sb.injection.WriteTo(buf, selectMarkerInit)

oraclePage := flavor == Oracle && (sb.limit >= 0 || sb.offset >= 0)
oraclePage := flavor == Oracle && (len(sb.limitVar) > 0 || len(sb.offsetVar) > 0)

if sb.cteBuilderVar != "" {
buf.WriteLeadingString(sb.cteBuilderVar)
Expand Down Expand Up @@ -349,7 +356,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{

if oraclePage {
if len(sb.selectCols) > 0 {
buf.WriteLeadingString("FROM ( SELECT ")
buf.WriteLeadingString("FROM (SELECT ")

if sb.distinct {
buf.WriteString("DISTINCT ")
Expand All @@ -368,7 +375,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
}

buf.WriteStrings(selectCols, ", ")
buf.WriteLeadingString("FROM ( SELECT ")
buf.WriteLeadingString("FROM (SELECT ")
buf.WriteStrings(sb.selectCols, ", ")
}
}
Expand Down Expand Up @@ -436,92 +443,100 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{

switch flavor {
case MySQL, SQLite, ClickHouse:
if sb.limit >= 0 {
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(strconv.Itoa(sb.limit))
buf.WriteString(sb.limitVar)

if sb.offset >= 0 {
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(strconv.Itoa(sb.offset))
buf.WriteString(sb.offsetVar)
}
}
case CQL:
if sb.limit >= 0 {
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(strconv.Itoa(sb.limit))
buf.WriteString(sb.limitVar)
}
case PostgreSQL, Presto:
if sb.limit >= 0 {
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(strconv.Itoa(sb.limit))
buf.WriteString(sb.limitVar)
}

if sb.offset >= 0 {
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(strconv.Itoa(sb.offset))
buf.WriteString(sb.offsetVar)
}

case SQLServer:
// If ORDER BY is not set, sort column #1 by default.
// It's required to make OFFSET...FETCH work.
if len(sb.orderByCols) == 0 && (sb.limit >= 0 || sb.offset >= 0) {
if len(sb.orderByCols) == 0 && (len(sb.limitVar) > 0 || len(sb.offsetVar) > 0) {
buf.WriteLeadingString("ORDER BY 1")
}

if sb.offset >= 0 {
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(strconv.Itoa(sb.offset))
buf.WriteString(sb.offsetVar)
buf.WriteString(" ROWS")
}

if sb.limit >= 0 {
if sb.offset < 0 {
if len(sb.limitVar) > 0 {
if len(sb.offsetVar) == 0 {
buf.WriteLeadingString("OFFSET 0 ROWS")
}

buf.WriteLeadingString("FETCH NEXT ")
buf.WriteString(strconv.Itoa(sb.limit))
buf.WriteString(sb.limitVar)
buf.WriteString(" ROWS ONLY")
}

case Oracle:
if oraclePage {
buf.WriteString(" ) ")
buf.WriteString(") ")

if len(sb.tables) > 0 {
buf.WriteStrings(sb.tables, ", ")
}

min := sb.offset
if min < 0 {
min = 0
}
buf.WriteString(") WHERE ")

buf.WriteString(" ) WHERE ")
if sb.limit >= 0 {
if len(sb.limitVar) > 0 {
buf.WriteString("r BETWEEN ")
buf.WriteString(strconv.Itoa(min + 1))
buf.WriteString(" AND ")
buf.WriteString(strconv.Itoa(sb.limit + min))

if len(sb.offsetVar) > 0 {
buf.WriteString(sb.offsetVar)
buf.WriteString(" + 1 AND ")
buf.WriteString(sb.limitVar)
buf.WriteString(" + ")
buf.WriteString(sb.offsetVar)
} else {
buf.WriteString("1 AND ")
buf.WriteString(sb.limitVar)
buf.WriteString(" + 1")
}
} else {
// As oraclePage is true, sb.offsetVar must not be empty.
buf.WriteString("r >= ")
buf.WriteString(strconv.Itoa(min + 1))
buf.WriteString(sb.offsetVar)
buf.WriteString(" + 1")
}
}
case Informix:
// [SKIP N] FIRST M
// M must be greater than 0
if sb.limit > 0 {
if sb.offset >= 0 {
if len(sb.limitVar) > 0 {
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("SKIP ")
buf.WriteString(strconv.Itoa(sb.offset))
buf.WriteString(sb.offsetVar)
}

buf.WriteLeadingString("FIRST ")
buf.WriteString(strconv.Itoa(sb.limit))
buf.WriteString(sb.limitVar)
}
}

if sb.limit >= 0 {
if len(sb.limitVar) > 0 {
sb.injection.WriteTo(buf, selectMarkerAfterLimit)
}

Expand Down
94 changes: 45 additions & 49 deletions select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,18 @@ import (
)

func ExampleSelect() {
// Build a SQL to create a HIVE table.
s := CreateTable("users").
SQL("PARTITION BY (year)").
SQL("AS").
SQL(
Select("columns[0] id", "columns[1] name", "columns[2] year").
From("`all-users.csv`").
Limit(100).
String(),
).
String()
// Build a SQL to create a HIVE table using MySQL-like SQL syntax.
sql, args := Select("columns[0] id", "columns[1] name", "columns[2] year").
From(MySQL.Quote("all-users.csv")).
Limit(100).
Build()

fmt.Println(s)
fmt.Println(sql)
fmt.Println(args)

// Output:
// CREATE TABLE users PARTITION BY (year) AS SELECT columns[0] id, columns[1] name, columns[2] year FROM `all-users.csv` LIMIT 100
// SELECT columns[0] id, columns[1] name, columns[2] year FROM `all-users.csv` LIMIT ?
// [100]
}

func ExampleSelectBuilder() {
Expand Down Expand Up @@ -56,8 +52,8 @@ func ExampleSelectBuilder() {
fmt.Println(args)

// Output:
// SELECT DISTINCT id, name, COUNT(*) AS t FROM demo.user WHERE id > ? AND name LIKE ? AND (id_card IS NULL OR status IN (?, ?, ?)) AND id NOT IN (SELECT id FROM banned) AND modified_at > created_at + ? GROUP BY status HAVING status NOT IN (?, ?) ORDER BY modified_at ASC LIMIT 10 OFFSET 5
// [1234 %Du 1 2 5 86400 4 5]
// SELECT DISTINCT id, name, COUNT(*) AS t FROM demo.user WHERE id > ? AND name LIKE ? AND (id_card IS NULL OR status IN (?, ?, ?)) AND id NOT IN (SELECT id FROM banned) AND modified_at > created_at + ? GROUP BY status HAVING status NOT IN (?, ?) ORDER BY modified_at ASC LIMIT ? OFFSET ?
// [1234 %Du 1 2 5 86400 4 5 10 5]
}

func ExampleSelectBuilder_advancedUsage() {
Expand Down Expand Up @@ -191,65 +187,65 @@ func ExampleSelectBuilder_limit_offset() {
// MySQL
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1 OFFSET 0
// #4: SELECT * FROM user LIMIT 1
// #5: SELECT * FROM user ORDER BY id LIMIT 1 OFFSET 1
// #3: SELECT * FROM user LIMIT ? OFFSET ?
// #4: SELECT * FROM user LIMIT ?
// #5: SELECT * FROM user ORDER BY id LIMIT ? OFFSET ?
//
// PostgreSQL
// #1: SELECT * FROM user
// #2: SELECT * FROM user OFFSET 0
// #3: SELECT * FROM user LIMIT 1 OFFSET 0
// #4: SELECT * FROM user LIMIT 1
// #5: SELECT * FROM user ORDER BY id LIMIT 1 OFFSET 1
// #2: SELECT * FROM user OFFSET $1
// #3: SELECT * FROM user LIMIT $1 OFFSET $2
// #4: SELECT * FROM user LIMIT $1
// #5: SELECT * FROM user ORDER BY id LIMIT $1 OFFSET $2
//
// SQLite
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1 OFFSET 0
// #4: SELECT * FROM user LIMIT 1
// #5: SELECT * FROM user ORDER BY id LIMIT 1 OFFSET 1
// #3: SELECT * FROM user LIMIT ? OFFSET ?
// #4: SELECT * FROM user LIMIT ?
// #5: SELECT * FROM user ORDER BY id LIMIT ? OFFSET ?
//
// SQLServer
// #1: SELECT * FROM user
// #2: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS
// #3: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
// #4: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
// #5: SELECT * FROM user ORDER BY id OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY
// #2: SELECT * FROM user ORDER BY 1 OFFSET @p1 ROWS
// #3: SELECT * FROM user ORDER BY 1 OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY
// #4: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS FETCH NEXT @p1 ROWS ONLY
// #5: SELECT * FROM user ORDER BY id OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY
//
// CQL
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1
// #4: SELECT * FROM user LIMIT 1
// #5: SELECT * FROM user ORDER BY id LIMIT 1
// #3: SELECT * FROM user LIMIT ?
// #4: SELECT * FROM user LIMIT ?
// #5: SELECT * FROM user ORDER BY id LIMIT ?
//
// ClickHouse
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1 OFFSET 0
// #4: SELECT * FROM user LIMIT 1
// #5: SELECT * FROM user ORDER BY id LIMIT 1 OFFSET 1
// #3: SELECT * FROM user LIMIT ? OFFSET ?
// #4: SELECT * FROM user LIMIT ?
// #5: SELECT * FROM user ORDER BY id LIMIT ? OFFSET ?
//
// Presto
// #1: SELECT * FROM user
// #2: SELECT * FROM user OFFSET 0
// #3: SELECT * FROM user LIMIT 1 OFFSET 0
// #4: SELECT * FROM user LIMIT 1
// #5: SELECT * FROM user ORDER BY id LIMIT 1 OFFSET 1
// #2: SELECT * FROM user OFFSET ?
// #3: SELECT * FROM user LIMIT ? OFFSET ?
// #4: SELECT * FROM user LIMIT ?
// #5: SELECT * FROM user ORDER BY id LIMIT ? OFFSET ?
//
// Oracle
// #1: SELECT * FROM user
// #2: SELECT * FROM ( SELECT ROWNUM r, * FROM ( SELECT * FROM user ) user ) WHERE r >= 1
// #3: SELECT * FROM ( SELECT ROWNUM r, * FROM ( SELECT * FROM user ) user ) WHERE r BETWEEN 1 AND 1
// #4: SELECT * FROM ( SELECT ROWNUM r, * FROM ( SELECT * FROM user ) user ) WHERE r BETWEEN 1 AND 1
// #5: SELECT * FROM ( SELECT ROWNUM r, * FROM ( SELECT * FROM user ORDER BY id ) user ) WHERE r BETWEEN 2 AND 2
// #2: SELECT * FROM (SELECT ROWNUM r, * FROM (SELECT * FROM user) user) WHERE r >= :1 + 1
// #3: SELECT * FROM (SELECT ROWNUM r, * FROM (SELECT * FROM user) user) WHERE r BETWEEN :1 + 1 AND :2 + :3
// #4: SELECT * FROM (SELECT ROWNUM r, * FROM (SELECT * FROM user) user) WHERE r BETWEEN 1 AND :1 + 1
// #5: SELECT * FROM (SELECT ROWNUM r, * FROM (SELECT * FROM user ORDER BY id) user) WHERE r BETWEEN :1 + 1 AND :2 + :3
//
// Informix
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user SKIP 0 FIRST 1
// #4: SELECT * FROM user FIRST 1
// #5: SELECT * FROM user ORDER BY id SKIP 1 FIRST 1
// #3: SELECT * FROM user SKIP ? FIRST ?
// #4: SELECT * FROM user FIRST ?
// #5: SELECT * FROM user ORDER BY id SKIP ? FIRST ?
}

func ExampleSelectBuilder_ForUpdate() {
Expand Down Expand Up @@ -314,7 +310,7 @@ func ExampleSelectBuilder_SQL() {
fmt.Println(s)

// Output:
// /* before */ SELECT u.id, u.name, c.type, p.nickname /* after select */ FROM user u /* after from */ JOIN contract c ON u.id = c.user_id RIGHT OUTER JOIN person p ON u.id = p.user_id /* after join */ WHERE u.modified_at > u.created_at /* after where */ ORDER BY id /* after order by */ LIMIT 10 /* after limit */ FOR SHARE /* after for */
// /* before */ SELECT u.id, u.name, c.type, p.nickname /* after select */ FROM user u /* after from */ JOIN contract c ON u.id = c.user_id RIGHT OUTER JOIN person p ON u.id = p.user_id /* after join */ WHERE u.modified_at > u.created_at /* after where */ ORDER BY id /* after order by */ LIMIT ? /* after limit */ FOR SHARE /* after for */
}

// Example for issue #115.
Expand Down Expand Up @@ -365,7 +361,7 @@ func ExampleSelectBuilder_With() {
fmt.Println(sql)

// Output:
// WITH users AS (SELECT id, name FROM users WHERE prime IS NOT NULL), orders AS (SELECT id, user_id FROM orders) SELECT orders.id FROM orders JOIN users ON orders.user_id = users.id LIMIT 10
// WITH users AS (SELECT id, name FROM users WHERE prime IS NOT NULL), orders AS (SELECT id, user_id FROM orders) SELECT orders.id FROM orders JOIN users ON orders.user_id = users.id LIMIT ?
}

func TestSelectBuilderSelectMore(t *testing.T) {
Expand Down Expand Up @@ -412,5 +408,5 @@ func ExampleSelectBuilder_LateralAs() {
fmt.Println(sb)

// Output:
// SELECT salesperson.name, max_sale.amount, max_sale.customer_name FROM salesperson, LATERAL (SELECT amount, customer_name FROM all_sales WHERE all_sales.salesperson_id = salesperson.id ORDER BY amount DESC LIMIT 1) AS max_sale
// SELECT salesperson.name, max_sale.amount, max_sale.customer_name FROM salesperson, LATERAL (SELECT amount, customer_name FROM all_sales WHERE all_sales.salesperson_id = salesperson.id ORDER BY amount DESC LIMIT ?) AS max_sale
}
2 changes: 1 addition & 1 deletion struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,6 @@ func ExampleFieldMapperFunc() {
fmt.Println(sql1 == sql2)

// Output:
// SELECT orders.id, orders.user_id, orders.product_name, orders.status, orders.user_addr_line1, orders.user_addr_line2, orders.created_at FROM orders LIMIT 10
// SELECT orders.id, orders.user_id, orders.product_name, orders.status, orders.user_addr_line1, orders.user_addr_line2, orders.created_at FROM orders LIMIT ?
// true
}

0 comments on commit 5677f58

Please sign in to comment.