mirror of
https://github.com/ddworken/hishtory.git
synced 2025-08-09 23:17:55 +02:00
Add support for searching for custom columns as part of default searches (#286)
* Refactor code for searching for custom columns * Add support for searching custom columns in default searches * Add integration test for default searches for custom columns * Add memoization to avoid repeated work for queries * update comment * Update golden * Update goldens
This commit is contained in:
@ -784,11 +784,11 @@ func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*
|
||||
}
|
||||
tx = where(tx, "NOT "+query, v1, v2)
|
||||
} else {
|
||||
query, v1, v2, v3, err := parseNonAtomizedToken(ctx, token[1:])
|
||||
query, args, err := parseNonAtomizedToken(ctx, token[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx = where(tx, "NOT "+query, v1, v2, v3)
|
||||
tx = where(tx, "NOT "+query, args...)
|
||||
}
|
||||
} else if containsUnescaped(token, ":") {
|
||||
query, v1, v2, err := parseAtomizedToken(ctx, token)
|
||||
@ -797,11 +797,11 @@ func MakeWhereQueryFromSearch(ctx context.Context, db *gorm.DB, query string) (*
|
||||
}
|
||||
tx = where(tx, query, v1, v2)
|
||||
} else {
|
||||
query, v1, v2, v3, err := parseNonAtomizedToken(ctx, token)
|
||||
query, args, err := parseNonAtomizedToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx = where(tx, query, v1, v2, v3)
|
||||
tx = where(tx, query, args...)
|
||||
}
|
||||
}
|
||||
return tx, nil
|
||||
@ -851,36 +851,27 @@ func retryingSearch(ctx context.Context, db *gorm.DB, query string, limit, offse
|
||||
return historyEntries, nil
|
||||
}
|
||||
|
||||
func parseNonAtomizedToken(ctx context.Context, token string) (string, any, any, any, error) {
|
||||
var SUPPORTED_DEFAULT_COLUMNS = []string{"command", "hostname", "current_working_directory"}
|
||||
|
||||
func parseNonAtomizedToken(ctx context.Context, token string) (string, []any, error) {
|
||||
wildcardedToken := "%" + unescape(token) + "%"
|
||||
query := "(false "
|
||||
numFilters := 0
|
||||
if slices.Contains(hctx.GetConf(ctx).DefaultSearchColumns, "command") {
|
||||
query += "OR command LIKE ? "
|
||||
numFilters += 1
|
||||
}
|
||||
if slices.Contains(hctx.GetConf(ctx).DefaultSearchColumns, "hostname") {
|
||||
query += "OR hostname LIKE ? "
|
||||
numFilters += 1
|
||||
}
|
||||
if slices.Contains(hctx.GetConf(ctx).DefaultSearchColumns, "current_working_directory") {
|
||||
query += "OR current_working_directory LIKE ? "
|
||||
numFilters += 1
|
||||
args := make([]any, 0)
|
||||
for _, column := range hctx.GetConf(ctx).DefaultSearchColumns {
|
||||
if slices.Contains(SUPPORTED_DEFAULT_COLUMNS, column) {
|
||||
query += "OR " + column + " LIKE ? "
|
||||
args = append(args, wildcardedToken)
|
||||
} else {
|
||||
q, a, err := buildCustomColumnSearchQuery(ctx, column, unescape(token))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
query += "OR " + q + " "
|
||||
args = append(args, a...)
|
||||
}
|
||||
}
|
||||
query += ")"
|
||||
var t1 any = nil
|
||||
var t2 any = nil
|
||||
var t3 any = nil
|
||||
if numFilters >= 1 {
|
||||
t1 = wildcardedToken
|
||||
}
|
||||
if numFilters >= 2 {
|
||||
t2 = wildcardedToken
|
||||
}
|
||||
if numFilters >= 3 {
|
||||
t3 = wildcardedToken
|
||||
}
|
||||
return query, t1, t2, t3, nil
|
||||
return query, args, nil
|
||||
}
|
||||
|
||||
func parseAtomizedToken(ctx context.Context, token string) (string, any, any, error) {
|
||||
@ -932,34 +923,54 @@ func parseAtomizedToken(ctx context.Context, token string) (string, any, any, er
|
||||
case "command":
|
||||
return "(instr(command, ?) > 0)", val, nil, nil
|
||||
default:
|
||||
knownCustomColumns := make([]string, 0)
|
||||
// Get custom columns that are defined on this machine
|
||||
conf := hctx.GetConf(ctx)
|
||||
for _, c := range conf.CustomColumns {
|
||||
knownCustomColumns = append(knownCustomColumns, c.ColumnName)
|
||||
}
|
||||
// Also get all ones that are in the DB
|
||||
names, err := getAllCustomColumnNames(ctx)
|
||||
q, args, err := buildCustomColumnSearchQuery(ctx, field, val)
|
||||
if err != nil {
|
||||
return "", nil, nil, fmt.Errorf("failed to get custom column names from the DB: %w", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
knownCustomColumns = append(knownCustomColumns, names...)
|
||||
// Check if the atom is for a custom column that exists and if it isn't, return an error
|
||||
isCustomColumn := false
|
||||
for _, ccName := range knownCustomColumns {
|
||||
if ccName == field {
|
||||
isCustomColumn = true
|
||||
}
|
||||
if len(args) != 2 {
|
||||
return "", nil, nil, fmt.Errorf("custom column search query returned an unexpected number of args: %d", len(args))
|
||||
}
|
||||
if !isCustomColumn {
|
||||
return "", nil, nil, fmt.Errorf("search query contains unknown search atom '%s' that doesn't match any column names", field)
|
||||
}
|
||||
// Build the where clause for the custom column
|
||||
return "EXISTS (SELECT 1 FROM json_each(custom_columns) WHERE json_extract(value, '$.name') = ? and instr(json_extract(value, '$.value'), ?) > 0)", field, val, nil
|
||||
return q, args[0], args[1], nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAllCustomColumnNames(ctx context.Context) ([]string, error) {
|
||||
func buildCustomColumnSearchQuery(ctx context.Context, columnName, columnVal string) (string, []any, error) {
|
||||
knownCustomColumns, err := GetAllCustomColumnNames(ctx)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get list of known custom columns: %w", err)
|
||||
}
|
||||
if !slices.Contains(knownCustomColumns, columnName) {
|
||||
return "", nil, fmt.Errorf("search query contains unknown search atom '%s' that doesn't match any column names", columnName)
|
||||
}
|
||||
// Build the where clause for the custom column
|
||||
return "EXISTS (SELECT 1 FROM json_each(custom_columns) WHERE json_extract(value, '$.name') = ? and instr(json_extract(value, '$.value'), ?) > 0)", []any{columnName, columnVal}, nil
|
||||
}
|
||||
|
||||
func GetAllCustomColumnNames(ctx context.Context) ([]string, error) {
|
||||
knownCustomColumns := make([]string, 0)
|
||||
// Get custom columns that are defined on this machine
|
||||
conf := hctx.GetConf(ctx)
|
||||
for _, c := range conf.CustomColumns {
|
||||
knownCustomColumns = append(knownCustomColumns, c.ColumnName)
|
||||
}
|
||||
// Also get all ones that are in the DB
|
||||
names, err := getAllCustomColumnNamesFromDb(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get custom column names from the DB: %w", err)
|
||||
}
|
||||
knownCustomColumns = append(knownCustomColumns, names...)
|
||||
return knownCustomColumns, nil
|
||||
}
|
||||
|
||||
var cachedCustomColumnNames []string
|
||||
|
||||
func getAllCustomColumnNamesFromDb(ctx context.Context) ([]string, error) {
|
||||
if len(cachedCustomColumnNames) > 0 {
|
||||
// Note: We memoize this function since it is called repeatedly in the TUI and querying the
|
||||
// entire DB for every updated search is quite inefficient. This is reasonable since the set
|
||||
// of custom columns shouldn't ever change within the lifetime of one hishtory process.
|
||||
return cachedCustomColumnNames, nil
|
||||
}
|
||||
db := hctx.GetDb(ctx)
|
||||
rows, err := RetryingDbFunctionWithResult(func() (*sql.Rows, error) {
|
||||
query := `
|
||||
@ -981,6 +992,7 @@ func getAllCustomColumnNames(ctx context.Context) ([]string, error) {
|
||||
}
|
||||
ccNames = append(ccNames, ccName)
|
||||
}
|
||||
cachedCustomColumnNames = ccNames
|
||||
return ccNames, nil
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user