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:
douxu 2026-07-23 14:30:49 +08:00
parent ec7d97d4d2
commit 0adfe022b2
8 changed files with 196 additions and 3 deletions

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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...,
)
}
}

View File

@ -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"])
}

View File

@ -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) {

View File

@ -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")

View File

@ -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 <> ''`
) )