Add fulltext search on risks, documents, and measures
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -30,8 +30,8 @@ func NewControlFilter(query *string) *ControlFilter {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ControlFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
return pgx.StrictNamedArgs{
|
||||
func (f *ControlFilter) SQLArguments() pgx.NamedArgs {
|
||||
return pgx.NamedArgs{
|
||||
"query": f.query,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ func (p *Documents) LoadByOrganizationID(
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[DocumentOrderField],
|
||||
filter *DocumentFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -119,12 +120,14 @@ WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
args := pgx.NamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -249,6 +252,7 @@ func (p *Documents) LoadByControlID(
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor[DocumentOrderField],
|
||||
filter *DocumentFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH plcs AS (
|
||||
@@ -282,11 +286,13 @@ FROM
|
||||
plcs
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
args := pgx.NamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -310,6 +316,7 @@ func (p *Documents) LoadByRiskID(
|
||||
scope Scoper,
|
||||
riskID gid.GID,
|
||||
cursor *page.Cursor[DocumentOrderField],
|
||||
filter *DocumentFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH plcs AS (
|
||||
@@ -343,11 +350,13 @@ FROM
|
||||
plcs
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"risk_id": riskID}
|
||||
args := pgx.NamedArgs{"risk_id": riskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
|
||||
45
pkg/coredata/document_filter.go
Normal file
45
pkg/coredata/document_filter.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
DocumentFilter struct {
|
||||
query *string
|
||||
}
|
||||
)
|
||||
|
||||
func NewDocumentFilter(query *string) *DocumentFilter {
|
||||
return &DocumentFilter{
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *DocumentFilter) SQLArguments() pgx.NamedArgs {
|
||||
return pgx.NamedArgs{
|
||||
"query": f.query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *DocumentFilter) SQLFragment() string {
|
||||
if f.query == nil || *f.query == "" {
|
||||
return "TRUE"
|
||||
}
|
||||
|
||||
return "search_vector @@ websearch_to_tsquery('simple', @query)"
|
||||
}
|
||||
@@ -58,6 +58,7 @@ func (m *Measures) LoadByRiskID(
|
||||
scope Scoper,
|
||||
riskID gid.GID,
|
||||
cursor *page.Cursor[MeasureOrderField],
|
||||
filter *MeasureFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH msrs AS (
|
||||
@@ -93,11 +94,13 @@ FROM
|
||||
msrs
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"risk_id": riskID}
|
||||
args := pgx.NamedArgs{"risk_id": riskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -121,6 +124,7 @@ func (m *Measures) LoadByControlID(
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor[MeasureOrderField],
|
||||
filter *MeasureFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH mtgtns AS (
|
||||
@@ -156,11 +160,13 @@ FROM
|
||||
mtgtns
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
args := pgx.NamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -184,6 +190,7 @@ func (m *Measures) LoadByOrganizationID(
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[MeasureOrderField],
|
||||
filter *MeasureFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -202,11 +209,13 @@ WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
args := pgx.NamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
|
||||
45
pkg/coredata/measure_filter.go
Normal file
45
pkg/coredata/measure_filter.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
MeasureFilter struct {
|
||||
query *string
|
||||
}
|
||||
)
|
||||
|
||||
func NewMeasureFilter(query *string) *MeasureFilter {
|
||||
return &MeasureFilter{
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *MeasureFilter) SQLArguments() pgx.NamedArgs {
|
||||
return pgx.NamedArgs{
|
||||
"query": f.query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *MeasureFilter) SQLFragment() string {
|
||||
if f.query == nil || *f.query == "" {
|
||||
return "TRUE"
|
||||
}
|
||||
|
||||
return "search_vector @@ websearch_to_tsquery('simple', @query)"
|
||||
}
|
||||
26
pkg/coredata/migrations/20250609T212605Z.sql
Normal file
26
pkg/coredata/migrations/20250609T212605Z.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
ALTER TABLE documents ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('simple',
|
||||
COALESCE(title, '')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX documents_search_idx ON documents USING gin(search_vector);
|
||||
|
||||
ALTER TABLE measures ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('simple',
|
||||
COALESCE(name, '')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX measures_search_idx ON measures USING gin(search_vector);
|
||||
|
||||
ALTER TABLE risks ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('simple',
|
||||
COALESCE(name, '')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX risks_search_idx ON risks USING gin(search_vector);
|
||||
@@ -74,6 +74,7 @@ func (r *Risks) LoadByMeasureID(
|
||||
scope Scoper,
|
||||
measureID gid.GID,
|
||||
cursor *page.Cursor[RiskOrderField],
|
||||
filter *RiskFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH rsks AS (
|
||||
@@ -123,11 +124,14 @@ FROM
|
||||
rsks
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"measure_id": measureID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
@@ -150,6 +154,7 @@ func (r *Risks) LoadByOrganizationID(
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[RiskOrderField],
|
||||
filter *RiskFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -173,11 +178,13 @@ FROM risks
|
||||
WHERE %s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
args := pgx.NamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
|
||||
45
pkg/coredata/risk_filter.go
Normal file
45
pkg/coredata/risk_filter.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
RiskFilter struct {
|
||||
query *string
|
||||
}
|
||||
)
|
||||
|
||||
func NewRiskFilter(query *string) *RiskFilter {
|
||||
return &RiskFilter{
|
||||
query: query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *RiskFilter) SQLArguments() pgx.NamedArgs {
|
||||
return pgx.NamedArgs{
|
||||
"query": f.query,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *RiskFilter) SQLFragment() string {
|
||||
if f.query == nil || *f.query == "" {
|
||||
return "TRUE"
|
||||
}
|
||||
|
||||
return "search_vector @@ websearch_to_tsquery('simple', @query)"
|
||||
}
|
||||
Reference in New Issue
Block a user