Skip to content
Merged
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
11 changes: 0 additions & 11 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ Fixes SC-XXXXX

### Acceptance criteria

This PR will be merged without review.

<!--
[Describe how reviewers can test this change to be sure that it works correctly or copy the requirements from the Shortcut story. Add a checklist below if possible.]
-->

### Author checklist

Expand All @@ -27,11 +23,4 @@ This PR will be merged without review.
- [ ] I have added new test fixtures as needed to support added tests
- [ ] I have added or updated the documentation
- [ ] I have run go generate to update generated code
<!--
- [ ] Check this box if a reviewer can merge this pull request after approval (leave it unchecked if you want to do it yourself)

### Reviewer(s) checklist

- [ ] Any new user-facing content that has been added for this PR has been QA'ed to ensure correct grammar, spelling, and understandability.
- [ ] To the best of my ability, I believe that this PR represents a good solution to the specified problem and that it should be merged into the main code base.
-->
116 changes: 70 additions & 46 deletions README.md

Large diffs are not rendered by default.

6 changes: 0 additions & 6 deletions filter/builder/builder.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
// Package builder provides SQL clause fragments for [filter.Filter] list queries.
//
// Types such as [Where], [Ordering], [Limit], and [Offset] render ANSI SQL fragments
// (WHERE, ORDER BY, LIMIT, OFFSET). [Where] supports Eq, Ne, comparisons, Like,
// ILike, In, Is, IsNot (with [Literal] or a bound value), and IS DISTINCT FROM.
// Deprecated WhereOp values IsNull and IsNotNull remain supported.
// [filter.Filter] composes them into a single [filter.ListFilter] clause.
package builder

import (
Expand Down
12 changes: 3 additions & 9 deletions filter/builder/in.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@ func inValues(value any) []any {
}
}

// Returns true if the condition should be added to the WHERE clause.
func shouldAddCondition(op WhereOp, value any) bool {
// All operators except In are always added.
if op != In {
return true
}

// An In condition with an empty slice is omitted.
return len(inValues(value)) > 0
// Reports whether a value is a nil or empty slice/array.
func isEmptySet(value any) bool {
return len(inValues(value)) == 0
}
123 changes: 109 additions & 14 deletions filter/builder/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,12 @@ const (
// Not all database providers support it.
ILike

// IsNull is the SQL 'IS NULL' operator for checking if a value is NULL.
// Deprecated: use [Is] with [Null] instead.
IsNull

// IsNotNull is the SQL 'IS NOT NULL' operator for checking if a value is not NULL.
// Deprecated: use [IsNot] with [Null] instead.
IsNotNull

// In represents the SQL 'IN' operator for matching any value in a set/list.
In

// NotIn represents the SQL 'NOT IN' operator for excluding values in a set/list.
NotIn

// Is is the SQL 'IS' operator. Use with a [Literal] for NULL and boolean predicates
// (for example, field IS NULL), or with any other value for null-safe equality
// (for example, field IS :param). Not all database providers support every form;
Expand Down Expand Up @@ -77,8 +72,112 @@ const (
BitNot
)

const (
quantifierAny WhereOp = 1 << 6
quantifierAll WhereOp = 1 << 7
quantifierMask = quantifierAny | quantifierAll

anyEq = quantifierAny
anyNe = quantifierAny | Ne
anyGt = quantifierAny | Gt
anyLt = quantifierAny | Lt
anyGte = quantifierAny | Gte
anyLte = quantifierAny | Lte
anyLike = quantifierAny | Like
anyILike = quantifierAny | ILike

allEq = quantifierAll
allNe = quantifierAll | Ne
allGt = quantifierAll | Gt
allLt = quantifierAll | Lt
allGte = quantifierAll | Gte
allLte = quantifierAll | Lte
allLike = quantifierAll | Like
allILike = quantifierAll | ILike
)

// Builds an ANY comparison from a comparison operator. Unsupported operators
// are rendered as provided and may fail when the database executes the query.
func Any(op WhereOp) WhereOp {
switch op {
case Eq:
return anyEq
case Ne:
return anyNe
case Gt:
return anyGt
case Lt:
return anyLt
case Gte:
return anyGte
case Lte:
return anyLte
case Like:
return anyLike
case ILike:
return anyILike
default:
return quantifierAny | op
}
}

// Builds an ALL comparison from a comparison operator. Unsupported operators
// are rendered as provided and may fail when the database executes the query.
func All(op WhereOp) WhereOp {
switch op {
case Eq:
return allEq
case Ne:
return allNe
case Gt:
return allGt
case Lt:
return allLt
case Gte:
return allGte
case Lte:
return allLte
case Like:
return allLike
case ILike:
return allILike
default:
return quantifierAll | op
}
}

// Splits a quantified operator into its comparison and quantifier components.
func quantifiedOperator(op WhereOp) (WhereOp, string, bool) {
var quantifier string
switch op & quantifierMask {
case quantifierAny:
quantifier = "ANY"
case quantifierAll:
quantifier = "ALL"
case quantifierMask:
quantifier = "ANY ALL"
default:
return op, "", false
}
return op &^ quantifierMask, quantifier, true
}

// Reports whether an operator is a standard scalar comparison operator.
func isComparisonOperator(op WhereOp) bool {
switch op {
case Eq, Ne, Gt, Lt, Gte, Lte, Like, ILike:
return true
default:
return false
}
}

// Returns the operator as a string for ANSI SQL.
func (op WhereOp) String() string {
if comparison, quantifier, ok := quantifiedOperator(op); ok {
return comparison.String() + " " + quantifier
}

switch op {
case Eq:
return "="
Expand All @@ -96,14 +195,10 @@ func (op WhereOp) String() string {
return "LIKE"
case ILike:
return "ILIKE"
case IsNull:
// Deprecated
return "IS NULL"
case IsNotNull:
// Deprecated
return "IS NOT NULL"
case In:
return "IN"
case NotIn:
return "NOT IN"
case Is:
return "IS"
case IsNot:
Expand Down
31 changes: 31 additions & 0 deletions filter/builder/subquery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package builder

import "strings"

// Subquery marks SQL that should be rendered as a subquery rather than bound as
// a string parameter. The SQL is inserted unchanged, so callers must ensure it
// is trusted and valid for the target database. Values passed directly as
// strings that begin with SELECT or WITH are recognized the same way.
type Subquery string

// Builds a trusted subquery value for use with [In], [NotIn], [Any], or [All].
func Subselect(query string) Subquery {
return Subquery(query)
}

// Returns the SQL text when value represents a trusted subquery.
func subqueryValue(value any) (string, bool) {
switch query := value.(type) {
case Subquery:
return string(query), true
case string:
// Accept the concise ticket/API form while preserving ordinary scalar
// strings as bound values for IN and NOT IN.
trimmed := strings.TrimSpace(query)
upper := strings.ToUpper(trimmed)
if strings.HasPrefix(upper, "SELECT ") || strings.HasPrefix(upper, "WITH ") {
return trimmed, true
}
}
return "", false
}
Loading
Loading