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
13 changes: 13 additions & 0 deletions pkg/postgres/pgutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/stackrox/rox/pkg/logging"
"github.com/stackrox/rox/pkg/postgres"
"github.com/stackrox/rox/pkg/utils"
"github.com/stackrox/rox/pkg/uuid"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
Expand Down Expand Up @@ -62,6 +63,18 @@ func NilOrTime(t *types.Timestamp) *time.Time {
return &ts
}

// NilOrUUID allows for a proto string to be stored as a UUID type in Postgres
func NilOrUUID(value string) *uuid.UUID {
if value == "" {
return nil
}
id, err := uuid.FromString(value)
if err != nil {
return nil
}
return &id
}

// CreateTableFromModel executes input create statement using the input connection.
func CreateTableFromModel(ctx context.Context, db *gorm.DB, createStmt *postgres.CreateStmts) {
err := Retry(func() error {
Expand Down
1 change: 1 addition & 0 deletions pkg/postgres/walker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
Integer DataType = "integer"
IntArray DataType = "intarray"
BigInteger DataType = "biginteger"
UUID DataType = "uuid"
)

// DataTypeToSQLType converts the internal representation to SQL
Expand Down
38 changes: 28 additions & 10 deletions pkg/search/postgres/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (q *query) getPortionBeforeFromClause() string {
var primaryKeyPaths []string
// Always select the primary keys for count.
for _, pk := range q.Schema.PrimaryKeys() {
primaryKeyPaths = append(primaryKeyPaths, qualifyColumn(pk.Schema.Table, pk.ColumnName))
primaryKeyPaths = append(primaryKeyPaths, qualifyColumn(pk.Schema.Table, pk.ColumnName, ""))
}
countOn = fmt.Sprintf("distinct(%s)", strings.Join(primaryKeyPaths, ", "))
}
Expand All @@ -133,7 +133,11 @@ func (q *query) getPortionBeforeFromClause() string {
var primaryKeyPaths []string
// Always select the primary keys first.
for _, pk := range q.Schema.PrimaryKeys() {
primaryKeyPaths = append(primaryKeyPaths, qualifyColumn(pk.Schema.Table, pk.ColumnName))
var cast string
if pk.SQLType == "uuid" {
cast = "::text"
}
primaryKeyPaths = append(primaryKeyPaths, qualifyColumn(pk.Schema.Table, pk.ColumnName, cast))
}
primaryKeyPortion := strings.Join(primaryKeyPaths, ", ")

Expand Down Expand Up @@ -185,7 +189,7 @@ func (q *query) AsSQL() string {
primaryKeys := q.Schema.PrimaryKeys()
primaryKeyPaths := make([]string, 0, len(primaryKeys))
for _, pk := range primaryKeys {
primaryKeyPaths = append(primaryKeyPaths, qualifyColumn(pk.Schema.Table, pk.ColumnName))
primaryKeyPaths = append(primaryKeyPaths, qualifyColumn(pk.Schema.Table, pk.ColumnName, ""))
}
querySB.WriteString(" group by ")
querySB.WriteString(strings.Join(primaryKeyPaths, ", "))
Expand All @@ -201,8 +205,8 @@ func (q *query) AsSQL() string {
return querySB.String()
}

func qualifyColumn(table, column string) string {
return table + "." + column
func qualifyColumn(table, column, cast string) string {
return table + "." + column + cast
}

type parsedPaginationQuery struct {
Expand Down Expand Up @@ -245,9 +249,13 @@ func populatePagination(querySoFar *query, pagination *v1.QueryPagination, schem
return errors.New("search after for pagination must be defined for only the first sort option")
}
if so.GetField() == searchPkg.DocID.String() {
var cast string
if schema.ID().SQLType == "uuid" {
cast = "::text"
}
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.

nit:I might be wrong her, but I expect UUID columns to have an internal representation in postgres as unsigned int of size 128 bit. As a consequence I'd expect the ordering of values to be identical for the internal integer representation and the associated text representation (binary values 0000 to 1111 matching text representations from 0 to f - 0x30 to 0x39 and 0x61 to 0x66 - ).
I don't think the cast to text hurts that much, but if it's unnecessary, we might as well want to skip it for ordering.

I'd say it can stay as is for now. I don't have advanced enough knowledge on the topic to drive the eventual decision here.

querySoFar.Pagination.OrderBys = append(querySoFar.Pagination.OrderBys, orderByEntry{
Field: pgsearch.SelectQueryField{
SelectPath: qualifyColumn(schema.Table, schema.ID().ColumnName),
SelectPath: qualifyColumn(schema.Table, schema.ID().ColumnName, cast),
FieldType: walker.String,
},
Descending: so.GetReversed(),
Expand All @@ -261,10 +269,15 @@ func populatePagination(querySoFar *query, pagination *v1.QueryPagination, schem
return errors.Errorf("field %s does not exist in table %s or connected tables", so.GetField(), schema.Table)
}

var cast string
if dbField.SQLType == "uuid" {
cast = "::text"
}

if fieldMetadata.derivedMetadata == nil {
querySoFar.Pagination.OrderBys = append(querySoFar.Pagination.OrderBys, orderByEntry{
Field: pgsearch.SelectQueryField{
SelectPath: qualifyColumn(dbField.Schema.Table, dbField.ColumnName),
SelectPath: qualifyColumn(dbField.Schema.Table, dbField.ColumnName, cast),
FieldType: dbField.DataType,
},
Descending: so.GetReversed(),
Expand All @@ -275,7 +288,7 @@ func populatePagination(querySoFar *query, pagination *v1.QueryPagination, schem
case searchPkg.CountDerivationType:
querySoFar.Pagination.OrderBys = append(querySoFar.Pagination.OrderBys, orderByEntry{
Field: pgsearch.SelectQueryField{
SelectPath: fmt.Sprintf("count(%s)", qualifyColumn(dbField.Schema.Table, dbField.ColumnName)),
SelectPath: fmt.Sprintf("count(%s)", qualifyColumn(dbField.Schema.Table, dbField.ColumnName, "")),
FieldType: dbField.DataType,
},
Descending: so.GetReversed(),
Expand All @@ -285,7 +298,7 @@ func populatePagination(querySoFar *query, pagination *v1.QueryPagination, schem
case searchPkg.SimpleReverseSortDerivationType:
querySoFar.Pagination.OrderBys = append(querySoFar.Pagination.OrderBys, orderByEntry{
Field: pgsearch.SelectQueryField{
SelectPath: qualifyColumn(dbField.Schema.Table, dbField.ColumnName),
SelectPath: qualifyColumn(dbField.Schema.Table, dbField.ColumnName, cast),
FieldType: dbField.DataType,
},
Descending: !so.GetReversed(),
Expand Down Expand Up @@ -395,6 +408,7 @@ func combineQueryEntries(entries []*pgsearch.QueryEntry, separator string) *pgse
if newQE.Having != nil {
newQE.Having.Query = fmt.Sprintf("(%s)", strings.Join(havingQueryStrings, separator))
}

return newQE
}

Expand Down Expand Up @@ -423,8 +437,12 @@ func compileQueryToPostgres(schema *walker.Schema, q *v1.Query, queryFields map[
case *v1.Query_BaseQuery:
switch subBQ := q.GetBaseQuery().Query.(type) {
case *v1.BaseQuery_DocIdQuery:
cast := "::text[]"
if schema.ID().SQLType == "uuid" {
cast = "::uuid[]"
}
return &pgsearch.QueryEntry{Where: pgsearch.WhereClause{
Query: fmt.Sprintf("%s.%s = ANY($$::text[])", schema.Table, schema.ID().ColumnName),
Query: fmt.Sprintf("%s.%s = ANY($$%s)", schema.Table, schema.ID().ColumnName, cast),
Values: []interface{}{subBQ.DocIdQuery.GetIds()},
}}, nil
case *v1.BaseQuery_MatchFieldQuery:
Expand Down
3 changes: 3 additions & 0 deletions pkg/search/postgres/query/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ func MatchFieldQuery(dbField *walker.Field, derivedMetadata *walker.DerivedSearc

qualifiedColName := dbField.Schema.Table + "." + dbField.ColumnName
dataType := dbField.DataType
if dbField.SQLType == "uuid" {
dataType = walker.UUID
}
var goesIntoHavingClause bool
if derivedMetadata != nil {
switch derivedMetadata.DerivationType {
Expand Down
7 changes: 6 additions & 1 deletion pkg/search/postgres/query/query_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ type queryAndFieldContext struct {
func qeWithSelectFieldIfNeeded(ctx *queryAndFieldContext, whereClause *WhereClause, postTransformFunc func(interface{}) interface{}) *QueryEntry {
qe := &QueryEntry{Where: *whereClause}
if ctx.highlight {
var cast string
if ctx.sqlDataType == walker.UUID {
cast = "::text"
}
qe.SelectedFields = []SelectQueryField{{
SelectPath: ctx.qualifiedColumnName,
SelectPath: ctx.qualifiedColumnName + cast,
FieldType: ctx.sqlDataType,
FieldPath: ctx.field.FieldPath,
PostTransform: postTransformFunc,
Expand All @@ -47,6 +51,7 @@ var datatypeToQueryFunc = map[walker.DataType]queryFunction{
walker.Numeric: newNumericQuery,
walker.EnumArray: queryOnArray(newEnumQuery, getEnumArrayPostTransformFunc),
walker.IntArray: queryOnArray(newNumericQuery, getIntArrayPostTransformFunc),
walker.UUID: newUUIDQuery,
// Map is handled separately.
}

Expand Down
108 changes: 108 additions & 0 deletions pkg/search/postgres/query/uuid_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package pgsearch

import (
"fmt"
"regexp"
"strings"

"github.com/pkg/errors"
pkgSearch "github.com/stackrox/rox/pkg/search"
"github.com/stackrox/rox/pkg/utils"
"github.com/stackrox/rox/pkg/uuid"
)

func newUUIDQuery(ctx *queryAndFieldContext) (*QueryEntry, error) {
whereClause, err := newUUIDQueryWhereClause(ctx.qualifiedColumnName, ctx.value, ctx.queryModifiers...)
if err != nil {
return nil, err
}
return qeWithSelectFieldIfNeeded(ctx, &whereClause, nil), nil
}

func newUUIDQueryWhereClause(columnName string, value string, queryModifiers ...pkgSearch.QueryModifier) (WhereClause, error) {
if len(value) == 0 {
return WhereClause{}, errors.New("value in search query cannot be empty")
}

if value == pkgSearch.WildcardString {
return WhereClause{
Query: fmt.Sprintf("%s is not null", columnName),
Values: []interface{}{},
equivalentGoFunc: func(foundValue interface{}) bool {
foundVal := strings.ToLower(foundValue.(string))
return foundVal != ""
},
}, nil
}

if len(queryModifiers) == 0 {
uuidVal, err := uuid.FromString(value)
if err != nil {
return WhereClause{}, errors.Wrapf(err, "value %q in search query must be valid UUID", value)
}
return WhereClause{
Query: fmt.Sprintf("%s = $$", columnName),
Values: []interface{}{uuidVal},
equivalentGoFunc: func(foundValue interface{}) bool {
return strings.EqualFold(foundValue.(string), value)
},
}, nil
}
if queryModifiers[0] == pkgSearch.AtLeastOne {
panic("I dont think this is used")
}
var negationString string
negated := queryModifiers[0] == pkgSearch.Negation
if negated {
negationString = "!"
if len(queryModifiers) == 1 {
uuidVal, err := uuid.FromString(value)
if err != nil {
return WhereClause{}, errors.Wrapf(err, "value %q in search query must be valid UUID", value)
}
return WhereClause{
Query: fmt.Sprintf("%s != $$", columnName),
Values: []interface{}{uuidVal},
equivalentGoFunc: func(foundValue interface{}) bool {
return strings.EqualFold(foundValue.(string), value) != negated
},
}, nil
}
queryModifiers = queryModifiers[1:]
}

switch queryModifiers[0] {
case pkgSearch.Regex:
re, err := regexp.Compile(value)
if err != nil {
return WhereClause{}, fmt.Errorf("invalid regexp %s: %w", value, err)
}

if value != "*" {
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.

I might be missing some context but why only * is supported and not other regexes, for example, abc.*?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@md2119 I didn't think that made a lot of sense for a UUID. What would be the use case for that since UUIDs are random? There is no logical grouping of say all UUIDs that start with abc*. If we need to cover that, I can certainly add it.

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.

This is a breaking change given that we currently support any regex on the string fields which are planned to be converted uuid. At the very least we should call it out in the docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@md2119 got it. Probably easy enough to just support. I will take a look at that capability in with PR #3698

return WhereClause{}, fmt.Errorf("invalid regexp %s for UUID field", value)
}
return WhereClause{
Query: fmt.Sprintf("%s is not null", columnName),
Values: []interface{}{value},
equivalentGoFunc: func(foundValue interface{}) bool {
foundVal := strings.ToLower(foundValue.(string))
return re.MatchString(foundVal) != negated
},
}, nil
case pkgSearch.Equality:
uuidVal, err := uuid.FromString(value)
if err != nil {
return WhereClause{}, errors.Wrapf(err, "value %q in search query must be valid UUID", value)
}
return WhereClause{
Query: fmt.Sprintf("%s %s= $$", columnName, negationString),
Values: []interface{}{uuidVal},
equivalentGoFunc: func(foundValue interface{}) bool {
return strings.EqualFold(foundValue.(string), value) != negated
},
}, nil
}
err := fmt.Errorf("unknown query modifier: %s", queryModifiers[0])
utils.Should(err)
return WhereClause{}, err
}
8 changes: 8 additions & 0 deletions pkg/search/postgres/query_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ var (
return val.([]string)
},
},
walker.UUID: {
alloc: func() interface{} {
return pointers.String("")
},
printer: func(val interface{}) []string {
return []string{*(val.(*string))}
},
},
}
)

Expand Down