sql

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 5 Imported by: 0

README

Sift SQL Adapter

SQL adapter for the Sift universal query filter library. Translates Sift filter expressions into SQL WHERE clause syntax.

Features

  • Multiple SQL Dialects: PostgreSQL, MySQL, SQLite, SQL Server
  • Parameterized Queries: Automatic parameter binding to prevent SQL injection
  • Flexible Configuration: Quote identifiers, case-sensitive/insensitive matching
  • Type Conversion: Automatic type detection for numbers, booleans, and strings
  • All Sift Operations: Full support for comparison, string, collection, and logical operations

Installation

go get github.com/nisimpson/sift/thru/sql

Quick Start

import (
    "context"
    "github.com/nisimpson/sift"
    siftsql "github.com/nisimpson/sift/thru/sql"
)

// Create a filter
filter := sift.Eq("status", "active").And(sift.Gt("age", 18))

// Create SQL adapter
adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Use with database/sql
query := fmt.Sprintf("SELECT * FROM users WHERE %s", adapter.Query())
rows, err := db.Query(query, adapter.Args()...)

Supported Dialects

PostgreSQL (Default)

Uses $1, $2, etc. for parameter placeholders:

adapter := siftsql.NewAdapter() // Defaults to PostgreSQL
// Query: status = $1 AND age > $2
MySQL

Uses ? for parameter placeholders:

config := &siftsql.Config{
    Dialect: siftsql.DialectMySQL,
}
adapter := siftsql.NewAdapterWithConfig(config)
// Query: status = ? AND age > ?
SQLite

Uses ? for parameter placeholders:

config := &siftsql.Config{
    Dialect: siftsql.DialectSQLite,
}
adapter := siftsql.NewAdapterWithConfig(config)
// Query: status = ? AND age > ?
SQL Server

Uses @p1, @p2, etc. for parameter placeholders:

config := &siftsql.Config{
    Dialect: siftsql.DialectSQLServer,
}
adapter := siftsql.NewAdapterWithConfig(config)
// Query: status = @p1 AND age > @p2

Configuration Options

type Config struct {
    // Dialect specifies the SQL dialect (PostgreSQL, MySQL, SQLite, SQLServer)
    Dialect Dialect

    // QuoteIdentifiers determines whether to quote column names
    // PostgreSQL/SQLite use double quotes, MySQL uses backticks
    QuoteIdentifiers bool

    // CaseSensitive determines whether string comparisons are case-sensitive
    // When false, uses LOWER() for case-insensitive comparisons
    CaseSensitive bool
}
Quote Identifiers

Useful for column names with special characters or reserved words:

config := &siftsql.Config{
    Dialect:          siftsql.DialectPostgreSQL,
    QuoteIdentifiers: true,
}
adapter := siftsql.NewAdapterWithConfig(config)

filter := sift.Eq("user_name", "john")
sift.Thru(context.Background(), adapter, filter)
// Query: "user_name" = $1

MySQL uses backticks:

config := &siftsql.Config{
    Dialect:          siftsql.DialectMySQL,
    QuoteIdentifiers: true,
}
// Query: `user_name` = ?
Case-Insensitive Matching

For case-insensitive string operations:

config := &siftsql.Config{
    CaseSensitive: false,
}
adapter := siftsql.NewAdapterWithConfig(config)

filter := sift.Contains("email", "@EXAMPLE.COM")
sift.Thru(context.Background(), adapter, filter)
// Query: LOWER(email) LIKE LOWER($1)

Supported Operations

Comparison Operations
Sift Operation SQL Output Example
OperationEQ = status = $1
OperationNEQ != status != $1
OperationLT < age < $1
OperationLTE <= age <= $1
OperationGT > age > $1
OperationGTE >= age >= $1
String Operations
Sift Operation SQL Output Example
OperationContains LIKE email LIKE $1 (with %value%)
OperationBeginsWith LIKE name LIKE $1 (with value%)
Collection Operations
Sift Operation SQL Output Example
OperationIn IN status IN ($1, $2, $3)
OperationBetween BETWEEN age BETWEEN $1 AND $2
Existence Operations
Sift Operation SQL Output Example
OperationExists IS NOT NULL email IS NOT NULL
OperationNotExists IS NULL deleted_at IS NULL
Logical Operations
Sift Operation SQL Output Example
AndOperation AND (status = $1) AND (age > $2)
OrOperation OR (role = $1) OR (role = $2)
NotOperation NOT NOT (deleted = $1)

Examples

Simple Condition
filter := sift.Eq("status", "active")

adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Query: status = $1
// Args: [active]
Complex Filter
// (status = "active" AND age > 18) OR role = "admin"
filter := sift.Eq("status", "active").
    And(sift.Gt("age", 18)).
    Or(sift.Eq("role", "admin"))

adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Query: ((status = $1) AND (age > $2)) OR (role = $3)
// Args: [active 18 admin]
String Operations
filter := sift.Contains("email", "@example.com").
    And(&sift.Condition{
        Name:      "name",
        Operation: sift.OperationBeginsWith,
        Value:     "John",
    })

adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Query: (email LIKE $1) AND (name LIKE $2)
// Args: [%@example.com% John%]
IN Operation
filter := &sift.Condition{
    Name:      "status",
    Operation: sift.OperationIn,
    Value:     "active,pending,approved",
}

adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Query: status IN ($1, $2, $3)
// Args: [active pending approved]
BETWEEN Operation
filter := sift.Between("age", 18, 65)

adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Query: age BETWEEN $1 AND $2
// Args: [18 65]
NOT Operation
filter := sift.Eq("deleted", "true").Not()

adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

// Query: NOT (deleted = $1)
// Args: [true]
With database/sql
import (
    "database/sql"
    _ "github.com/lib/pq" // PostgreSQL driver
)

func FindUsers(db *sql.DB, filter sift.Expression) ([]*User, error) {
    adapter := siftsql.NewAdapter()
    if err := sift.Thru(context.Background(), adapter, filter); err != nil {
        return nil, err
    }

    query := fmt.Sprintf("SELECT id, name, email FROM users WHERE %s", adapter.Query())
    rows, err := db.Query(query, adapter.Args()...)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []*User
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
            return nil, err
        }
        users = append(users, &user)
    }

    return users, rows.Err()
}
With GORM
import "gorm.io/gorm"

func FindUsers(db *gorm.DB, filter sift.Expression) ([]*User, error) {
    adapter := siftsql.NewAdapter()
    if err := sift.Thru(context.Background(), adapter, filter); err != nil {
        return nil, err
    }

    var users []*User
    result := db.Where(adapter.Query(), adapter.Args()...).Find(&users)
    return users, result.Error
}
With sqlx
import "github.com/jmoiron/sqlx"

func FindUsers(db *sqlx.DB, filter sift.Expression) ([]*User, error) {
    adapter := siftsql.NewAdapter()
    if err := sift.Thru(context.Background(), adapter, filter); err != nil {
        return nil, err
    }

    var users []*User
    query := fmt.Sprintf("SELECT * FROM users WHERE %s", adapter.Query())
    err := db.Select(&users, query, adapter.Args()...)
    return users, err
}

Type Conversion

The adapter automatically converts string values to appropriate types:

filter := sift.Gt("age", 18)
// Args: [int64(18)]

filter := sift.Eq("price", "99.99")
// Args: [float64(99.99)]

filter := sift.Eq("active", "true")
// Args: [bool(true)]

filter := sift.Eq("name", "John Doe")
// Args: ["John Doe"]

Security

The adapter uses parameterized queries to prevent SQL injection:

// Safe - uses parameters
filter := sift.Eq("name", "'; DROP TABLE users; --")
adapter := siftsql.NewAdapter()
sift.Thru(context.Background(), adapter, filter)
// Query: name = $1
// Args: ["'; DROP TABLE users; --"]

The malicious input is safely passed as a parameter value, not concatenated into the query string.

License

MIT License - see LICENSE file for details

Documentation

Overview

Package sql provides a SQL adapter for the sift filter library. It translates sift filter expressions into SQL WHERE clause syntax.

Example
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Create a simple filter: status = "active"
	filter := sift.Eq("status", "active")

	// Create SQL adapter
	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: status = $1
Args: [active]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter translates sift filter expressions into SQL WHERE clause syntax. It accumulates the SQL query string and parameter values during traversal.

Example (BetweenOperation)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// BETWEEN operation
	filter := sift.Between("age", 18, 65)

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: age BETWEEN $1 AND $2
Args: [18 65]
Example (CaseInsensitive)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Case-insensitive string matching
	config := &siftsql.Config{
		Dialect:       siftsql.DialectPostgreSQL,
		CaseSensitive: false,
	}

	filter := sift.Contains("email", "@EXAMPLE.COM")

	adapter := siftsql.NewAdapterWithConfig(config)
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())

}
Output:
Query: LOWER(email) LIKE LOWER($1)
Example (ComplexFilter)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Create a complex filter: (status = "active" AND age > 18) OR role = "admin"
	filter := sift.Eq("status", "active").
		And(sift.Gt("age", 18)).
		Or(sift.Eq("role", "admin"))

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: ((status = $1) AND (age > $2)) OR (role = $3)
Args: [active 18 admin]
Example (ExistenceChecks)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Existence checks: IS NULL and IS NOT NULL
	filter := sift.Exists("email").And(sift.NotExists("deleted_at"))

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())

}
Output:
Query: (email IS NOT NULL) AND (deleted_at IS NULL)
Example (InOperation)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// IN operation with multiple values
	filter := &sift.Condition{
		Name:      "status",
		Operation: sift.OperationIn,
		Value:     "active,pending,approved",
	}

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: status IN ($1, $2, $3)
Args: [active pending approved]
Example (NotOperation)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// NOT operation
	filter := sift.Eq("deleted", "true").Not()

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: NOT (deleted = $1)
Args: [true]
Example (StringOperations)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// String operations: contains and begins_with
	filter := sift.Contains("email", "@example.com").
		And(&sift.Condition{
			Name:      "name",
			Operation: sift.OperationBeginsWith,
			Value:     "John",
		})

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: (email LIKE $1) AND (name LIKE $2)
Args: [%@example.com% John%]
Example (WithDatabase)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Example showing usage with database/sql
	filter := sift.Eq("status", "active").And(sift.Gt("age", 18))

	adapter := siftsql.NewAdapter()
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	// Use with database/sql (pseudo-code)
	query := fmt.Sprintf("SELECT * FROM users WHERE %s", adapter.Query())
	fmt.Printf("SQL: %s\n", query)
	fmt.Printf("Args: %v\n", adapter.Args())

	// In real code:
	// rows, err := db.Query(query, adapter.Args()...)

}
Output:
SQL: SELECT * FROM users WHERE (status = $1) AND (age > $2)
Args: [active 18]

func NewAdapter

func NewAdapter() *Adapter

NewAdapter creates a new SQL adapter with default configuration (PostgreSQL dialect).

func NewAdapterWithConfig

func NewAdapterWithConfig(config *Config) *Adapter

NewAdapterWithConfig creates a new SQL adapter with custom configuration.

Example (Mysql)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Configure for MySQL dialect
	config := &siftsql.Config{
		Dialect:          siftsql.DialectMySQL,
		QuoteIdentifiers: true,
	}

	filter := sift.Eq("user_name", "john").And(sift.Gt("age", 18))

	adapter := siftsql.NewAdapterWithConfig(config)
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: (`user_name` = ?) AND (`age` > ?)
Args: [john 18]
Example (Sqlserver)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	siftsql "github.com/nisimpson/sift/thru/sql"
)

func main() {
	// Configure for SQL Server dialect
	config := &siftsql.Config{
		Dialect: siftsql.DialectSQLServer,
	}

	filter := sift.Eq("status", "active").And(sift.Gt("age", 18))

	adapter := siftsql.NewAdapterWithConfig(config)
	sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Printf("Query: %s\n", adapter.Query())
	fmt.Printf("Args: %v\n", adapter.Args())

}
Output:
Query: (status = @p1) AND (age > @p2)
Args: [active 18]

func (*Adapter) Args

func (a *Adapter) Args() []interface{}

Args returns the parameter values for the query.

func (*Adapter) EvaluateAnd

func (a *Adapter) EvaluateAnd(ctx context.Context, node *sift.AndOperation) error

EvaluateAnd combines two conditions with logical AND.

func (*Adapter) EvaluateCondition

func (a *Adapter) EvaluateCondition(ctx context.Context, node *sift.Condition) error

EvaluateCondition translates a sift condition into a SQL WHERE clause fragment.

func (*Adapter) EvaluateNot

func (a *Adapter) EvaluateNot(ctx context.Context, node *sift.NotOperation) error

EvaluateNot negates a condition.

func (*Adapter) EvaluateOffsetPagination

func (a *Adapter) EvaluateOffsetPagination(ctx context.Context, page *sift.OffsetPagination) error

EvaluateOffsetPagination sets the LIMIT and OFFSET values.

func (*Adapter) EvaluateOr

func (a *Adapter) EvaluateOr(ctx context.Context, node *sift.OrOperation) error

EvaluateOr combines two conditions with logical OR.

func (*Adapter) EvaluateSortList

func (a *Adapter) EvaluateSortList(ctx context.Context, list *sift.SortList) error

EvaluateSortList translates a sort list into SQL ORDER BY clause.

func (*Adapter) Evaluator

func (a *Adapter) Evaluator(ctx context.Context) *sift.Evaluator

Evaluator returns a sift evaluator configured for SQL.

func (*Adapter) Limit

func (a *Adapter) Limit() int

Limit returns the LIMIT value. Returns the configured DefaultLimit if no pagination was specified. Returns 0 if no limit should be applied.

func (*Adapter) Offset

func (a *Adapter) Offset() int

Offset returns the OFFSET value. Returns 0 if no offset was specified.

func (*Adapter) OrderBy

func (a *Adapter) OrderBy() string

OrderBy returns the ORDER BY clause (without the ORDER BY keyword). Returns empty string if no sorting was specified.

func (*Adapter) Query

func (a *Adapter) Query() string

Query returns the generated SQL WHERE clause (without the WHERE keyword).

type Config

type Config struct {
	// Dialect specifies the SQL dialect to use for query generation.
	// Defaults to DialectPostgreSQL.
	Dialect Dialect

	// QuoteIdentifiers determines whether to quote column names.
	// PostgreSQL/SQLite use double quotes, MySQL uses backticks.
	QuoteIdentifiers bool

	// CaseSensitive determines whether string comparisons are case-sensitive.
	// When false, uses LOWER() for case-insensitive comparisons.
	CaseSensitive bool

	// DefaultLimit is applied when no pagination is specified.
	// Set to 0 to disable default limit (not recommended for production).
	DefaultLimit int
}

Config holds configuration options for the SQL adapter.

type Dialect

type Dialect string

Dialect represents a SQL dialect for query generation.

const (
	// DialectPostgreSQL generates PostgreSQL-compatible queries with $1, $2, etc. placeholders
	DialectPostgreSQL Dialect = "postgresql"
	// DialectMySQL generates MySQL-compatible queries with ? placeholders
	DialectMySQL Dialect = "mysql"
	// DialectSQLite generates SQLite-compatible queries with ? placeholders
	DialectSQLite Dialect = "sqlite"
	// DialectSQLServer generates SQL Server-compatible queries with @p1, @p2, etc. placeholders
	DialectSQLServer Dialect = "sqlserver"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL