feat(data-object): support bay device measurement candidates
- expose bay dev_* columns for components linked to bay measurements - restrict parameter queries and traversal to supported attribute-group tables - add regression tests for bay candidates and parameter table validation
This commit is contained in:
parent
ec7d97d4d2
commit
0adfe022b2
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Package constants define constant variable
|
||||||
|
package constants
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
var supportedParameterTableSuffixes = [...]string{
|
||||||
|
"base_extend",
|
||||||
|
"rated",
|
||||||
|
"setup",
|
||||||
|
"model",
|
||||||
|
"stable",
|
||||||
|
"craft",
|
||||||
|
"integrity",
|
||||||
|
"behavior",
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSupportedParameterTableName reports whether a dynamic parameter table has
|
||||||
|
// one of the supported attribute-group suffixes.
|
||||||
|
func IsSupportedParameterTableName(tableName string) bool {
|
||||||
|
for _, suffix := range supportedParameterTableSuffixes {
|
||||||
|
if strings.HasSuffix(tableName, "_"+suffix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package constants
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestIsSupportedParameterTableName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tableName string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "bay table is excluded", tableName: "ct_ct_demo_bay", want: false},
|
||||||
|
{name: "model table is included", tableName: "cable_cable_demo_model", want: true},
|
||||||
|
{name: "base extend table is included", tableName: "cable_cable_demo_base_extend", want: true},
|
||||||
|
{name: "suffix must start at separator", tableName: "cable_cable_demomodel", want: false},
|
||||||
|
{name: "empty table name", tableName: "", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := IsSupportedParameterTableName(tt.tableName); got != tt.want {
|
||||||
|
t.Fatalf("IsSupportedParameterTableName(%q) = %v, want %v", tt.tableName, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryBayDevColumnNames returns the bay table columns exposed as token7
|
||||||
|
// candidates under token6=bay.
|
||||||
|
func QueryBayDevColumnNames(ctx context.Context, db *gorm.DB) ([]string, error) {
|
||||||
|
columnTypes, err := db.WithContext(ctx).Migrator().ColumnTypes((&orm.Bay{}).TableName())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
columnNames := make([]string, 0, len(columnTypes))
|
||||||
|
for _, columnType := range columnTypes {
|
||||||
|
columnName := columnType.Name()
|
||||||
|
if strings.HasPrefix(columnName, "dev_") {
|
||||||
|
columnNames = append(columnNames, columnName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return columnNames, nil
|
||||||
|
}
|
||||||
|
|
@ -279,6 +279,31 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
|
|
||||||
g, gctx := errgroup.WithContext(ctx)
|
g, gctx := errgroup.WithContext(ctx)
|
||||||
db = db.WithContext(gctx)
|
db = db.WithContext(gctx)
|
||||||
|
var bayLinkedCompTags []string
|
||||||
|
var bayDevColumnNames []string
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
var linkedComponents []struct {
|
||||||
|
CompTag string `gorm:"column:comp_tag"`
|
||||||
|
}
|
||||||
|
if err := db.Raw(compactMeasurementSQL(sql.MeasurementBayLinkedComponentTags)).Scan(&linkedComponents).Error; err != nil {
|
||||||
|
return fmt.Errorf("query bay-linked components: %w", err)
|
||||||
|
}
|
||||||
|
bayLinkedCompTags = make([]string, 0, len(linkedComponents))
|
||||||
|
for _, component := range linkedComponents {
|
||||||
|
bayLinkedCompTags = append(bayLinkedCompTags, component.CompTag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
g.Go(func() error {
|
||||||
|
var err error
|
||||||
|
bayDevColumnNames, err = QueryBayDevColumnNames(gctx, db)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("query bay dev columns: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var grids []orm.Grid
|
var grids []orm.Grid
|
||||||
|
|
@ -383,6 +408,22 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendBayDevCandidates(mSet, bayLinkedCompTags, bayDevColumnNames)
|
||||||
|
|
||||||
mSet.AllConfigTags = append(mSet.AllConfigTags, "bay")
|
mSet.AllConfigTags = append(mSet.AllConfigTags, "bay")
|
||||||
return mSet, nil
|
return mSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendBayDevCandidates(mSet *orm.MeasurementSet, bayLinkedCompTags, bayDevColumnNames []string) {
|
||||||
|
if mSet == nil || len(bayLinkedCompTags) == 0 || len(bayDevColumnNames) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mSet.AllMeasTags = append(mSet.AllMeasTags, bayDevColumnNames...)
|
||||||
|
for _, compTag := range bayLinkedCompTags {
|
||||||
|
mSet.CompTagToMeasTags[compTag] = append(
|
||||||
|
mSet.CompTagToMeasTags[compTag],
|
||||||
|
bayDevColumnNames...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppendBayDevCandidatesRequiresBayLinkedComponent(t *testing.T) {
|
||||||
|
measurementSet := &orm.MeasurementSet{
|
||||||
|
AllMeasTags: []string{"current"},
|
||||||
|
CompTagToMeasTags: map[string][]string{
|
||||||
|
"linked-component": {"current"},
|
||||||
|
"unlinked-component": {"voltage"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
appendBayDevCandidates(
|
||||||
|
measurementSet,
|
||||||
|
[]string{"linked-component"},
|
||||||
|
[]string{"dev_instruct", "dev_dyn_sense", "dev_fault_record"},
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Equal(t,
|
||||||
|
[]string{"current", "dev_instruct", "dev_dyn_sense", "dev_fault_record"},
|
||||||
|
measurementSet.AllMeasTags,
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
[]string{"current", "dev_instruct", "dev_dyn_sense", "dev_fault_record"},
|
||||||
|
measurementSet.CompTagToMeasTags["linked-component"],
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
[]string{"voltage"},
|
||||||
|
measurementSet.CompTagToMeasTags["unlinked-component"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendBayDevCandidatesWithoutBayLinkDoesNothing(t *testing.T) {
|
||||||
|
measurementSet := &orm.MeasurementSet{
|
||||||
|
AllMeasTags: []string{"current"},
|
||||||
|
CompTagToMeasTags: map[string][]string{
|
||||||
|
"component": {"current"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
appendBayDevCandidates(measurementSet, nil, []string{"dev_instruct"})
|
||||||
|
|
||||||
|
require.Equal(t, []string{"current"}, measurementSet.AllMeasTags)
|
||||||
|
require.Equal(t, []string{"current"}, measurementSet.CompTagToMeasTags["component"])
|
||||||
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ func QueryParameterByDataObjectToken(ctx context.Context, db *gorm.DB, token str
|
||||||
}
|
}
|
||||||
|
|
||||||
project := projects[0]
|
project := projects[0]
|
||||||
if !parameterTableNamePattern.MatchString(project.Name) {
|
if !validParameterTableName(project.Name) {
|
||||||
return nil, fmt.Errorf("project mapping for parameter token %q contains invalid table name %q", token, project.Name)
|
return nil, fmt.Errorf("project mapping for parameter token %q contains invalid table name %q", token, project.Name)
|
||||||
}
|
}
|
||||||
attributeType, err := queryParameterAttributeType(ctx, db, project.Name, attributeName, token)
|
attributeType, err := queryParameterAttributeType(ctx, db, project.Name, attributeName, token)
|
||||||
|
|
@ -155,7 +155,7 @@ func UpdateParameterDataObjectValue(ctx context.Context, db *gorm.DB, parameter
|
||||||
if parameter.AttributeGroup == "component" {
|
if parameter.AttributeGroup == "component" {
|
||||||
return fmt.Errorf("component data-object updates are not supported")
|
return fmt.Errorf("component data-object updates are not supported")
|
||||||
}
|
}
|
||||||
if !parameterTableNamePattern.MatchString(parameter.TableName) {
|
if !validParameterTableName(parameter.TableName) {
|
||||||
return fmt.Errorf("invalid parameter table name %q", parameter.TableName)
|
return fmt.Errorf("invalid parameter table name %q", parameter.TableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,6 +172,10 @@ func UpdateParameterDataObjectValue(ctx context.Context, db *gorm.DB, parameter
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validParameterTableName(tableName string) bool {
|
||||||
|
return parameterTableNamePattern.MatchString(tableName) && constants.IsSupportedParameterTableName(tableName)
|
||||||
|
}
|
||||||
|
|
||||||
// QueryParameterAttributeDescription returns the display name registered for
|
// QueryParameterAttributeDescription returns the display name registered for
|
||||||
// token7 in basic.attribute.
|
// token7 in basic.attribute.
|
||||||
func QueryParameterAttributeDescription(ctx context.Context, db *gorm.DB, attributeName string) (string, error) {
|
func QueryParameterAttributeDescription(ctx context.Context, db *gorm.DB, attributeName string) (string, error) {
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,19 @@ func TraverseAttributeGroupTables(ctx context.Context, db *gorm.DB, compTagToFul
|
||||||
var tableNames []string
|
var tableNames []string
|
||||||
|
|
||||||
excludedTables := []string{"component", ""}
|
excludedTables := []string{"component", ""}
|
||||||
|
var projectTableNames []string
|
||||||
result := db.Model(&orm.ProjectManager{}).
|
result := db.Model(&orm.ProjectManager{}).
|
||||||
Where("name NOT IN ?", excludedTables).
|
Where("name NOT IN ?", excludedTables).
|
||||||
Pluck("name", &tableNames)
|
Pluck("name", &projectTableNames)
|
||||||
if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
|
if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
|
||||||
logger.Error(ctx, "query name column data from postgres table failed", "err", result.Error)
|
logger.Error(ctx, "query name column data from postgres table failed", "err", result.Error)
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
|
for _, tableName := range projectTableNames {
|
||||||
|
if constants.IsSupportedParameterTableName(tableName) {
|
||||||
|
tableNames = append(tableNames, tableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(tableNames) == 0 {
|
if len(tableNames) == 0 {
|
||||||
logger.Info(ctx, "query from postgres successed, but no records found")
|
logger.Info(ctx, "query from postgres successed, but no records found")
|
||||||
|
|
|
||||||
|
|
@ -86,4 +86,15 @@ const (
|
||||||
ON measurement.component_uuid = component.global_uuid
|
ON measurement.component_uuid = component.global_uuid
|
||||||
LEFT JOIN bay
|
LEFT JOIN bay
|
||||||
ON measurement.bay_uuid = bay.bay_uuid`
|
ON measurement.bay_uuid = bay.bay_uuid`
|
||||||
|
|
||||||
|
// MeasurementBayLinkedComponentTags returns components that have at least
|
||||||
|
// one measurement whose bay_uuid resolves to an existing bay record.
|
||||||
|
MeasurementBayLinkedComponentTags = `
|
||||||
|
SELECT DISTINCT component.tag AS comp_tag
|
||||||
|
FROM component
|
||||||
|
INNER JOIN measurement
|
||||||
|
ON component.global_uuid = measurement.component_uuid
|
||||||
|
INNER JOIN bay
|
||||||
|
ON measurement.bay_uuid = bay.bay_uuid
|
||||||
|
WHERE component.tag <> ''`
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue