Compare commits
No commits in common. "refactor/optimize-parameter-measurement-value-api" and "develop" have entirely different histories.
refactor/o
...
develop
|
|
@ -1,23 +0,0 @@
|
||||||
// Package common define common error variables
|
|
||||||
package common
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrUnsupportedParameterField indicates that a requested parameter field is not supported.
|
|
||||||
ErrUnsupportedParameterField = errors.New("unsupported parameter field")
|
|
||||||
// ErrInvalidParameterToken indicates that a token cannot represent a parameter.
|
|
||||||
ErrInvalidParameterToken = errors.New("invalid parameter token")
|
|
||||||
// ErrParameterTokenNotFound indicates that no parameter matches the token hierarchy.
|
|
||||||
ErrParameterTokenNotFound = errors.New("parameter token not found")
|
|
||||||
// ErrAmbiguousParameterToken indicates that a token matches more than one parameter.
|
|
||||||
ErrAmbiguousParameterToken = errors.New("ambiguous parameter token")
|
|
||||||
// ErrUnsupportedMeasurementField define error of unsupport measurement field
|
|
||||||
ErrUnsupportedMeasurementField = errors.New("unsupported measurement field")
|
|
||||||
// ErrInvalidMeasurementToken indicates that a token cannot represent a measurement.
|
|
||||||
ErrInvalidMeasurementToken = errors.New("invalid measurement token")
|
|
||||||
// ErrMeasurementTokenNotFound indicates that no measurement matches the token hierarchy.
|
|
||||||
ErrMeasurementTokenNotFound = errors.New("measurement token not found")
|
|
||||||
// ErrAmbiguousMeasurementToken indicates that a token matches more than one measurement.
|
|
||||||
ErrAmbiguousMeasurementToken = errors.New("ambiguous measurement token")
|
|
||||||
)
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// Package constants define constant variable
|
|
||||||
package constants
|
|
||||||
|
|
||||||
// DataObjectType identifies the kind of object represented by a data object token.
|
|
||||||
type DataObjectType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DataObjectTypeParameter represents a component parameter.
|
|
||||||
DataObjectTypeParameter DataObjectType = "parameter"
|
|
||||||
// DataObjectTypeMeasurement represents a component measurement.
|
|
||||||
DataObjectTypeMeasurement DataObjectType = "measurement"
|
|
||||||
)
|
|
||||||
|
|
@ -4,174 +4,23 @@ package database
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"modelRT/common"
|
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
"modelRT/sql"
|
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// QueryMeasurementByID return the result of query circuit diagram component measurement info by id from postgresDB
|
type ZoneWithParent struct {
|
||||||
func QueryMeasurementByID(ctx context.Context, tx *gorm.DB, id int64) (orm.Measurement, error) {
|
orm.Zone
|
||||||
var measurement orm.Measurement
|
GridTag string `gorm:"column:grid_tag"`
|
||||||
// ctx超时判断
|
|
||||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
result := tx.WithContext(cancelCtx).
|
|
||||||
Where(sql.MeasurementIDWhere, id).
|
|
||||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
First(&measurement)
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
return orm.Measurement{}, result.Error
|
|
||||||
}
|
|
||||||
return measurement, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryMeasurementByToken define function query circuit diagram component measurement info by token from postgresDB
|
type StationWithParent struct {
|
||||||
func QueryMeasurementByToken(ctx context.Context, tx *gorm.DB, token string) (orm.Measurement, error) {
|
orm.Zone
|
||||||
measurement, _, err := QueryMeasurementByDataObjectToken(ctx, tx, token)
|
ZoneTag string `gorm:"column:zone_tag"`
|
||||||
if err != nil {
|
|
||||||
return orm.Measurement{}, err
|
|
||||||
}
|
|
||||||
return *measurement, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateMeasurementToken checks whether token uniquely identifies an existing
|
|
||||||
// measurement through the measurement, component, bay, station, zone, and grid
|
|
||||||
// relationships. Supported formats are token1-token7, token4-token7, and
|
|
||||||
// token4.token7.
|
|
||||||
func ValidateMeasurementToken(ctx context.Context, db *gorm.DB, token string) error {
|
|
||||||
query, args, err := buildMeasurementTokenValidationQuery(token)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if err := db.WithContext(ctx).Raw(query, args...).Scan(&count).Error; err != nil {
|
|
||||||
return fmt.Errorf("query measurement token %q: %w", token, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case count == 0:
|
|
||||||
return fmt.Errorf("%w: %q", common.ErrMeasurementTokenNotFound, token)
|
|
||||||
case count > 1:
|
|
||||||
return fmt.Errorf("%w: %q matched %d records", common.ErrAmbiguousMeasurementToken, token, count)
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryMeasurementByDataObjectToken validates token and returns the existing
|
|
||||||
// measurement and its owning component for attribute response construction.
|
|
||||||
func QueryMeasurementByDataObjectToken(ctx context.Context, db *gorm.DB, token string) (*orm.Measurement, *orm.Component, error) {
|
|
||||||
validationQuery, args, err := buildMeasurementTokenValidationQuery(token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
query := buildMeasurementRowsQuery(validationQuery)
|
|
||||||
var rows []orm.Measurement
|
|
||||||
if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("query measurement token %q: %w", token, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(rows) {
|
|
||||||
case 0:
|
|
||||||
return nil, nil, fmt.Errorf("%w: %q", common.ErrMeasurementTokenNotFound, token)
|
|
||||||
case 1:
|
|
||||||
// Continue by loading the owning component.
|
|
||||||
default:
|
|
||||||
return nil, nil, fmt.Errorf("%w: %q matched more than one record", common.ErrAmbiguousMeasurementToken, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
var component orm.Component
|
|
||||||
result := db.WithContext(ctx).
|
|
||||||
Raw(compactMeasurementSQL(sql.MeasurementComponentByUUID), rows[0].ComponentUUID).
|
|
||||||
Scan(&component)
|
|
||||||
if result.Error != nil {
|
|
||||||
return nil, nil, fmt.Errorf("query component for measurement token %q: %w", token, result.Error)
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return nil, nil, fmt.Errorf("%w: component for %q", common.ErrMeasurementTokenNotFound, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &rows[0], &component, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMeasurementRowsQuery(validationQuery string) string {
|
|
||||||
measurementQuery := strings.Replace(
|
|
||||||
validationQuery,
|
|
||||||
sql.MeasurementCountSelect,
|
|
||||||
sql.MeasurementRowsSelect,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
return compactMeasurementSQL(strings.Join([]string{measurementQuery, sql.MeasurementLimitTwo}, "\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactMeasurementSQL(statement string) string {
|
|
||||||
return strings.Join(strings.Fields(statement), " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMeasurementTokenValidationQuery(token string) (string, []any, error) {
|
|
||||||
parts := strings.Split(token, ".")
|
|
||||||
for _, part := range parts {
|
|
||||||
if part == "" {
|
|
||||||
return "", nil, fmt.Errorf("%w %q: token segment cannot be empty", common.ErrInvalidMeasurementToken, token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(parts) {
|
|
||||||
case 7:
|
|
||||||
if parts[5] != "bay" {
|
|
||||||
return "", nil, fmt.Errorf("%w %q: token6 must be bay", common.ErrInvalidMeasurementToken, token)
|
|
||||||
}
|
|
||||||
query := compactMeasurementSQL(strings.Join([]string{
|
|
||||||
sql.MeasurementTokenValidationQueryBase,
|
|
||||||
sql.MeasurementSevenPartTokenWhere,
|
|
||||||
}, "\n"))
|
|
||||||
return query, []any{parts[0], parts[1], parts[2], parts[3], parts[4], parts[6]}, nil
|
|
||||||
case 4:
|
|
||||||
if parts[2] != "bay" {
|
|
||||||
return "", nil, fmt.Errorf("%w %q: token6 must be bay", common.ErrInvalidMeasurementToken, token)
|
|
||||||
}
|
|
||||||
query := compactMeasurementSQL(strings.Join([]string{
|
|
||||||
sql.MeasurementTokenValidationQueryBase,
|
|
||||||
sql.MeasurementFourPartTokenWhere,
|
|
||||||
}, "\n"))
|
|
||||||
return query, []any{parts[0], parts[1], parts[3]}, nil
|
|
||||||
case 2:
|
|
||||||
query := compactMeasurementSQL(strings.Join([]string{
|
|
||||||
sql.MeasurementTokenValidationQueryBase,
|
|
||||||
sql.MeasurementTwoPartTokenWhere,
|
|
||||||
}, "\n"))
|
|
||||||
return query, []any{parts[0], parts[1]}, nil
|
|
||||||
default:
|
|
||||||
return "", nil, fmt.Errorf("%w %q: expected 2, 4, or 7 segments, got %d", common.ErrInvalidMeasurementToken, token, len(parts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllMeasurements define func to query all measurement info from postgresDB
|
|
||||||
func GetAllMeasurements(ctx context.Context, tx *gorm.DB) ([]orm.Measurement, error) {
|
|
||||||
var measurements []orm.Measurement
|
|
||||||
|
|
||||||
// ctx超时判断
|
|
||||||
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&measurements)
|
|
||||||
if result.Error != nil {
|
|
||||||
return nil, result.Error
|
|
||||||
}
|
|
||||||
return measurements, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFullMeasurementSet queries all hierarchy tags required to build
|
|
||||||
// measurement recommendations.
|
|
||||||
func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSet, error) {
|
func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSet, error) {
|
||||||
mSet := &orm.MeasurementSet{
|
mSet := &orm.MeasurementSet{
|
||||||
GridToZoneTags: make(map[string][]string),
|
GridToZoneTags: make(map[string][]string),
|
||||||
|
|
@ -187,7 +36,7 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var grids []orm.Grid
|
var grids []orm.Grid
|
||||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementGridTags)).Scan(&grids).Error; err != nil {
|
if err := db.Table("grid").Select("tagname").Scan(&grids).Error; err != nil {
|
||||||
return fmt.Errorf("query grids: %w", err)
|
return fmt.Errorf("query grids: %w", err)
|
||||||
}
|
}
|
||||||
for _, grid := range grids {
|
for _, grid := range grids {
|
||||||
|
|
@ -203,13 +52,16 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
orm.Zone
|
orm.Zone
|
||||||
GridTag string `gorm:"column:grid_tag"`
|
GridTag string `gorm:"column:grid_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementZoneHierarchy)).Scan(&zones).Error; err != nil {
|
if err := db.Table("zone").
|
||||||
|
Select("zone.*, grid.tagname as grid_tag").
|
||||||
|
Joins("left join grid on zone.grid_id = grid.id").
|
||||||
|
Scan(&zones).Error; err != nil {
|
||||||
return fmt.Errorf("query zones: %w", err)
|
return fmt.Errorf("query zones: %w", err)
|
||||||
}
|
}
|
||||||
for _, zone := range zones {
|
for _, z := range zones {
|
||||||
mSet.AllZoneTags = append(mSet.AllZoneTags, zone.TAGNAME)
|
mSet.AllZoneTags = append(mSet.AllZoneTags, z.TAGNAME)
|
||||||
if zone.GridTag != "" {
|
if z.GridTag != "" {
|
||||||
mSet.GridToZoneTags[zone.GridTag] = append(mSet.GridToZoneTags[zone.GridTag], zone.TAGNAME)
|
mSet.GridToZoneTags[z.GridTag] = append(mSet.GridToZoneTags[z.GridTag], z.TAGNAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -220,40 +72,40 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
orm.Station
|
orm.Station
|
||||||
ZoneTag string `gorm:"column:zone_tag"`
|
ZoneTag string `gorm:"column:zone_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementStationHierarchy)).Scan(&stations).Error; err != nil {
|
if err := db.Table("station").
|
||||||
|
Select("station.*, zone.tagname as zone_tag").
|
||||||
|
Joins("left join zone on station.zone_id = zone.id").
|
||||||
|
Scan(&stations).Error; err != nil {
|
||||||
return fmt.Errorf("query stations: %w", err)
|
return fmt.Errorf("query stations: %w", err)
|
||||||
}
|
}
|
||||||
for _, station := range stations {
|
for _, s := range stations {
|
||||||
mSet.AllStationTags = append(mSet.AllStationTags, station.TAGNAME)
|
mSet.AllStationTags = append(mSet.AllStationTags, s.TAGNAME)
|
||||||
if station.ZoneTag != "" {
|
if s.ZoneTag != "" {
|
||||||
mSet.ZoneToStationTags[station.ZoneTag] = append(mSet.ZoneToStationTags[station.ZoneTag], station.TAGNAME)
|
mSet.ZoneToStationTags[s.ZoneTag] = append(mSet.ZoneToStationTags[s.ZoneTag], s.TAGNAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
var components []struct {
|
var comps []struct {
|
||||||
orm.Component
|
orm.Component
|
||||||
StationTag string `gorm:"column:station_tag"`
|
StationTag string `gorm:"column:station_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementComponentHierarchy)).Scan(&components).Error; err != nil {
|
if err := db.Table("component").
|
||||||
|
Select("component.*, station.tagname as station_tag").
|
||||||
|
Joins("left join station on component.station_id = station.id").
|
||||||
|
Scan(&comps).Error; err != nil {
|
||||||
return fmt.Errorf("query components: %w", err)
|
return fmt.Errorf("query components: %w", err)
|
||||||
}
|
}
|
||||||
for _, component := range components {
|
for _, c := range comps {
|
||||||
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, component.NSPath)
|
mSet.AllCompNSPaths = append(mSet.AllCompNSPaths, c.NSPath)
|
||||||
mSet.AllCompTags = append(mSet.AllCompTags, component.Tag)
|
mSet.AllCompTags = append(mSet.AllCompTags, c.Tag)
|
||||||
if component.StationTag != "" {
|
if c.StationTag != "" {
|
||||||
mSet.StationToCompNSPaths[component.StationTag] = append(
|
mSet.StationToCompNSPaths[c.StationTag] = append(mSet.StationToCompNSPaths[c.StationTag], c.NSPath)
|
||||||
mSet.StationToCompNSPaths[component.StationTag],
|
|
||||||
component.NSPath,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if component.NSPath != "" {
|
if c.NSPath != "" {
|
||||||
mSet.CompNSPathToCompTags[component.NSPath] = append(
|
mSet.CompNSPathToCompTags[c.NSPath] = append(mSet.CompNSPathToCompTags[c.NSPath], c.Tag)
|
||||||
mSet.CompNSPathToCompTags[component.NSPath],
|
|
||||||
component.Tag,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -266,19 +118,20 @@ func GetFullMeasurementSet(ctx context.Context, db *gorm.DB) (*orm.MeasurementSe
|
||||||
CompNSPath string `gorm:"column:comp_nspath"`
|
CompNSPath string `gorm:"column:comp_nspath"`
|
||||||
BayTag string `gorm:"column:bay_tag"`
|
BayTag string `gorm:"column:bay_tag"`
|
||||||
}
|
}
|
||||||
if err := db.Raw(compactMeasurementSQL(sql.MeasurementTagHierarchy)).Scan(&measurements).Error; err != nil {
|
if err := db.Table("measurement").
|
||||||
|
Select("measurement.*, component.tag as comp_tag, component.nspath as comp_nspath, bay.tag as bay_tag").
|
||||||
|
Joins("left join component on measurement.component_uuid = component.global_uuid").
|
||||||
|
Joins("left join bay on measurement.bay_uuid = bay.bay_uuid").
|
||||||
|
Scan(&measurements).Error; err != nil {
|
||||||
return fmt.Errorf("query measurements: %w", err)
|
return fmt.Errorf("query measurements: %w", err)
|
||||||
}
|
}
|
||||||
for _, measurement := range measurements {
|
for _, m := range measurements {
|
||||||
mSet.AllMeasTags = append(mSet.AllMeasTags, measurement.Tag)
|
mSet.AllMeasTags = append(mSet.AllMeasTags, m.Tag)
|
||||||
if measurement.CompTag != "" {
|
if m.CompTag != "" {
|
||||||
mSet.CompTagToMeasTags[measurement.CompTag] = append(
|
mSet.CompTagToMeasTags[m.CompTag] = append(mSet.CompTagToMeasTags[m.CompTag], m.Tag)
|
||||||
mSet.CompTagToMeasTags[measurement.CompTag],
|
|
||||||
measurement.Tag,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if measurement.CompNSPath != "" && measurement.CompNSPath == measurement.BayTag {
|
if m.CompNSPath != "" && m.CompNSPath == m.BayTag {
|
||||||
mSet.CompNSPathToMeasTags[measurement.CompNSPath] = append(mSet.CompNSPathToMeasTags[measurement.CompNSPath], measurement.Tag)
|
mSet.CompNSPathToMeasTags[m.CompNSPath] = append(mSet.CompNSPathToMeasTags[m.CompNSPath], m.Tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,199 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"modelRT/common"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuildMeasurementTokenValidationQuery(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
token string
|
|
||||||
wantArgs []any
|
|
||||||
wantWhere string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "seven-part token",
|
|
||||||
token: "grid.zone.station.nspath.component.bay.measurement",
|
|
||||||
wantArgs: []any{"grid", "zone", "station", "nspath", "component", "measurement"},
|
|
||||||
wantWhere: "WHERE g.tagname = ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "four-part token",
|
|
||||||
token: "nspath.component.bay.measurement",
|
|
||||||
wantArgs: []any{"nspath", "component", "measurement"},
|
|
||||||
wantWhere: "WHERE c.nspath = ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two-part token",
|
|
||||||
token: "nspath.measurement",
|
|
||||||
wantArgs: []any{"nspath", "measurement"},
|
|
||||||
wantWhere: "WHERE c.nspath = ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-bay group",
|
|
||||||
token: "nspath.component.rated.attribute",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty segment",
|
|
||||||
token: "nspath..bay.measurement",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid segment count",
|
|
||||||
token: "grid.zone.station",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
query, args, err := buildMeasurementTokenValidationQuery(tt.token)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.ErrorIs(t, err, common.ErrInvalidMeasurementToken)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, query, "INNER JOIN component AS c ON c.global_uuid = m.component_uuid")
|
|
||||||
assert.Contains(t, query, "INNER JOIN bay AS b ON b.bay_uuid = m.bay_uuid")
|
|
||||||
assert.Contains(t, query, tt.wantWhere)
|
|
||||||
assert.NotContains(t, query, "grid_idWHERE")
|
|
||||||
assert.Regexp(t, `grid_id\s+WHERE`, query)
|
|
||||||
assert.NotContains(t, query, "\n")
|
|
||||||
assert.NotContains(t, query, "\t")
|
|
||||||
assert.Equal(t, tt.wantArgs, args)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateMeasurementToken(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
count int64
|
|
||||||
queryErr error
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{name: "exists", count: 1},
|
|
||||||
{name: "not found", count: 0, wantErr: common.ErrMeasurementTokenNotFound},
|
|
||||||
{name: "ambiguous", count: 2, wantErr: common.ErrAmbiguousMeasurementToken},
|
|
||||||
{name: "query failure", queryErr: errors.New("database unavailable")},
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = "nspath.measurement"
|
|
||||||
query, _, err := buildMeasurementTokenValidationQuery(token)
|
|
||||||
require.NoError(t, err)
|
|
||||||
expectedQuery := query
|
|
||||||
for i := 1; strings.Contains(expectedQuery, "?"); i++ {
|
|
||||||
expectedQuery = strings.Replace(expectedQuery, "?", fmt.Sprintf("$%d", i), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
expectation := mock.ExpectQuery(regexp.QuoteMeta(expectedQuery)).
|
|
||||||
WithArgs("nspath", "measurement")
|
|
||||||
if tt.queryErr != nil {
|
|
||||||
expectation.WillReturnError(tt.queryErr)
|
|
||||||
} else {
|
|
||||||
expectation.WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.count))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ValidateMeasurementToken(context.Background(), db, token)
|
|
||||||
if tt.wantErr != nil {
|
|
||||||
assert.ErrorIs(t, err, tt.wantErr)
|
|
||||||
} else if tt.queryErr != nil {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), tt.queryErr.Error())
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMeasurementRowsQuerySeparatesLimitClause(t *testing.T) {
|
|
||||||
validationQuery, _, err := buildMeasurementTokenValidationQuery("nspath.measurement")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
query := buildMeasurementRowsQuery(validationQuery)
|
|
||||||
assert.NotContains(t, query, "?LIMIT")
|
|
||||||
assert.Regexp(t, `m\.tag = \?\s+LIMIT 2$`, query)
|
|
||||||
assert.NotContains(t, query, "\n")
|
|
||||||
assert.NotContains(t, query, "\t")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryMeasurementByDataObjectToken(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
|
||||||
mock.ExpectQuery(`(?s)SELECT m\.\*.*WHERE c\.nspath = \$1.*AND m\.tag = \$2.*LIMIT 2`).
|
|
||||||
WithArgs("nspath", "measurement").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
|
||||||
"id",
|
|
||||||
"tag",
|
|
||||||
"name",
|
|
||||||
"mode",
|
|
||||||
"size",
|
|
||||||
"data_source",
|
|
||||||
"event_plan",
|
|
||||||
"binding",
|
|
||||||
"component_uuid",
|
|
||||||
}).AddRow(
|
|
||||||
int64(10),
|
|
||||||
"measurement",
|
|
||||||
"A phase current",
|
|
||||||
int16(1),
|
|
||||||
10,
|
|
||||||
`{"type":1,"io_address":{"channel":"tm1"}}`,
|
|
||||||
`{"enabled":true}`,
|
|
||||||
`{"ct":{"ratio":1}}`,
|
|
||||||
componentUUID,
|
|
||||||
))
|
|
||||||
mock.ExpectQuery(`(?s)SELECT global_uuid, nspath, tag, grid, zone, station.*WHERE global_uuid = \$1.*LIMIT 1`).
|
|
||||||
WithArgs(componentUUID).
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
|
||||||
"global_uuid",
|
|
||||||
"nspath",
|
|
||||||
"tag",
|
|
||||||
"grid",
|
|
||||||
"zone",
|
|
||||||
"station",
|
|
||||||
}).AddRow(componentUUID, "nspath", "component", "grid", "zone", "station"))
|
|
||||||
|
|
||||||
measurement, component, err := QueryMeasurementByDataObjectToken(context.Background(), db, "nspath.measurement")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, int64(10), measurement.ID)
|
|
||||||
assert.Equal(t, int16(1), measurement.Mode)
|
|
||||||
assert.Equal(t, float64(1), measurement.DataSource["type"])
|
|
||||||
assert.Equal(t, "grid", component.GridName)
|
|
||||||
assert.Equal(t, "component", component.Tag)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/common"
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/model"
|
|
||||||
"modelRT/orm"
|
|
||||||
modelsql "modelRT/sql"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var parameterTableNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
|
||||||
|
|
||||||
// ParameterDataObject contains the resolved metadata needed to query a
|
|
||||||
// parameter attribute after its token has passed hierarchy validation.
|
|
||||||
type ParameterDataObject struct {
|
|
||||||
Component orm.Component
|
|
||||||
Project orm.ProjectManager
|
|
||||||
TableName string
|
|
||||||
AttributeGroup string
|
|
||||||
AttributeName string
|
|
||||||
AttributeType string
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryParameterByDataObjectToken validates a four-part or seven-part
|
|
||||||
// parameter token. The component group resolves directly to the component
|
|
||||||
// table; other groups resolve through project_manager and a dynamic table.
|
|
||||||
func QueryParameterByDataObjectToken(ctx context.Context, db *gorm.DB, token string) (*ParameterDataObject, error) {
|
|
||||||
componentQuery, componentArgs, parts, err := buildParameterComponentQuery(token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var components []orm.Component
|
|
||||||
if err := db.WithContext(ctx).Raw(componentQuery, componentArgs...).Scan(&components).Error; err != nil {
|
|
||||||
return nil, fmt.Errorf("query component for parameter token %q: %w", token, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(components) {
|
|
||||||
case 0:
|
|
||||||
return nil, fmt.Errorf("%w: component hierarchy for %q", common.ErrParameterTokenNotFound, token)
|
|
||||||
case 1:
|
|
||||||
// Continue by resolving the component model and attribute group.
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: component hierarchy for %q matched more than one record", common.ErrAmbiguousParameterToken, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeGroup := parts[len(parts)-2]
|
|
||||||
attributeName := parts[len(parts)-1]
|
|
||||||
component := components[0]
|
|
||||||
if attributeGroup == "component" {
|
|
||||||
attributeType, err := queryParameterAttributeType(ctx, db, "component", attributeName, token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ParameterDataObject{
|
|
||||||
Component: component,
|
|
||||||
TableName: "component",
|
|
||||||
AttributeGroup: attributeGroup,
|
|
||||||
AttributeName: attributeName,
|
|
||||||
AttributeType: attributeType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var projects []orm.ProjectManager
|
|
||||||
if err := db.WithContext(ctx).
|
|
||||||
Where("tag = ? AND group_name = ?", component.ModelName, attributeGroup).
|
|
||||||
Limit(2).
|
|
||||||
Find(&projects).Error; err != nil {
|
|
||||||
return nil, fmt.Errorf("query project mapping for parameter token %q: %w", token, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(projects) {
|
|
||||||
case 0:
|
|
||||||
return nil, fmt.Errorf("%w: model %q does not define attribute group %q", common.ErrParameterTokenNotFound, component.ModelName, attributeGroup)
|
|
||||||
case 1:
|
|
||||||
// Continue by validating the dynamic table and attribute.
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: model %q and attribute group %q matched more than one project", common.ErrAmbiguousParameterToken, component.ModelName, attributeGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
project := projects[0]
|
|
||||||
if !parameterTableNamePattern.MatchString(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)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var recordCount int64
|
|
||||||
if err := db.WithContext(ctx).
|
|
||||||
Table(project.Name).
|
|
||||||
Where("global_uuid = ? AND attribute_group = ?", component.GlobalUUID, attributeGroup).
|
|
||||||
Count(&recordCount).Error; err != nil {
|
|
||||||
return nil, fmt.Errorf("query dynamic record for parameter token %q: %w", token, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case recordCount == 0:
|
|
||||||
return nil, fmt.Errorf("%w: component %q has no %q parameter record", common.ErrParameterTokenNotFound, component.Tag, attributeGroup)
|
|
||||||
case recordCount > 1:
|
|
||||||
return nil, fmt.Errorf("%w: component %q has %d %q parameter records", common.ErrAmbiguousParameterToken, component.Tag, recordCount, attributeGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ParameterDataObject{
|
|
||||||
Component: component,
|
|
||||||
Project: project,
|
|
||||||
TableName: project.Name,
|
|
||||||
AttributeGroup: attributeGroup,
|
|
||||||
AttributeName: attributeName,
|
|
||||||
AttributeType: attributeType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryParameterDataObjectValue returns token7 from the component row or from
|
|
||||||
// a dynamic parameter row identified during token validation.
|
|
||||||
func QueryParameterDataObjectValue(ctx context.Context, db *gorm.DB, parameter *ParameterDataObject) (any, error) {
|
|
||||||
if parameter == nil {
|
|
||||||
return nil, fmt.Errorf("parameter data object is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
var record map[string]any
|
|
||||||
query := db.WithContext(ctx).Table(parameter.TableName)
|
|
||||||
if parameter.AttributeGroup == "component" {
|
|
||||||
query = query.Where("tag = ?", parameter.Component.Tag)
|
|
||||||
} else {
|
|
||||||
query = query.Where("global_uuid = ? AND attribute_group = ?", parameter.Component.GlobalUUID, parameter.AttributeGroup)
|
|
||||||
}
|
|
||||||
result := query.Take(&record)
|
|
||||||
if result.Error != nil {
|
|
||||||
return nil, fmt.Errorf("query parameter value from table %q: %w", parameter.TableName, result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, ok := record[parameter.AttributeName]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("parameter column %q is missing from table %q result", parameter.AttributeName, parameter.TableName)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryParameterAttributeDescription returns the display name registered for
|
|
||||||
// token7 in basic.attribute.
|
|
||||||
func QueryParameterAttributeDescription(ctx context.Context, db *gorm.DB, attributeName string) (string, error) {
|
|
||||||
var rows []struct {
|
|
||||||
Description string `gorm:"column:attribute_name"`
|
|
||||||
}
|
|
||||||
if err := db.WithContext(ctx).
|
|
||||||
Raw(modelsql.ParameterAttributeDescription, attributeName).
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return "", fmt.Errorf("query parameter description for attribute %q: %w", attributeName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(rows) {
|
|
||||||
case 0:
|
|
||||||
return "", fmt.Errorf("parameter description not found for attribute %q", attributeName)
|
|
||||||
case 1:
|
|
||||||
return rows[0].Description, nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("ambiguous parameter description for attribute %q", attributeName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryParameterAttributeType(ctx context.Context, db *gorm.DB, tableName, attributeName, token string) (string, error) {
|
|
||||||
var attributeType string
|
|
||||||
result := db.WithContext(ctx).
|
|
||||||
Raw(modelsql.ParameterAttributeColumnType, tableName, attributeName).
|
|
||||||
Scan(&attributeType)
|
|
||||||
if result.Error != nil {
|
|
||||||
return "", fmt.Errorf("validate attribute column for parameter token %q: %w", token, result.Error)
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 || attributeType == "" {
|
|
||||||
return "", fmt.Errorf("%w: column %q does not exist in parameter table %q", common.ErrParameterTokenNotFound, attributeName, tableName)
|
|
||||||
}
|
|
||||||
return strings.ToUpper(attributeType), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildParameterComponentQuery(token string) (string, []any, []string, error) {
|
|
||||||
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, nil, fmt.Errorf("%w %q: %v", common.ErrInvalidParameterToken, token, err)
|
|
||||||
}
|
|
||||||
if dataObjectType != constants.DataObjectTypeParameter {
|
|
||||||
return "", nil, nil, fmt.Errorf("%w %q: token does not identify a parameter", common.ErrInvalidParameterToken, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(token, ".")
|
|
||||||
var where string
|
|
||||||
var args []any
|
|
||||||
switch len(parts) {
|
|
||||||
case 7:
|
|
||||||
where = modelsql.ParameterSevenPartTokenWhere
|
|
||||||
args = []any{parts[0], parts[1], parts[2], parts[3], parts[4]}
|
|
||||||
case 4:
|
|
||||||
where = modelsql.ParameterFourPartTokenWhere
|
|
||||||
args = []any{parts[0], parts[1]}
|
|
||||||
default:
|
|
||||||
return "", nil, nil, fmt.Errorf("%w %q: expected 4 or 7 segments", common.ErrInvalidParameterToken, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := compactParameterSQL(strings.Join([]string{
|
|
||||||
modelsql.ParameterComponentQueryBase,
|
|
||||||
where,
|
|
||||||
modelsql.ParameterLimitTwo,
|
|
||||||
}, "\n"))
|
|
||||||
return query, args, parts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func compactParameterSQL(statement string) string {
|
|
||||||
return strings.Join(strings.Fields(statement), " ")
|
|
||||||
}
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"modelRT/common"
|
|
||||||
"modelRT/orm"
|
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuildParameterComponentQuery(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
token string
|
|
||||||
wantArgs []any
|
|
||||||
wantWhere string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "seven-part parameter",
|
|
||||||
token: "grid.zone.station.nspath.component.stable.attribute",
|
|
||||||
wantArgs: []any{"grid", "zone", "station", "nspath", "component"},
|
|
||||||
wantWhere: "WHERE g.tagname = ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "four-part local parameter",
|
|
||||||
token: "nspath.component.rated.attribute",
|
|
||||||
wantArgs: []any{"nspath", "component"},
|
|
||||||
wantWhere: "s.is_local = TRUE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "measurement token",
|
|
||||||
token: "nspath.component.bay.measurement",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty segment",
|
|
||||||
token: "nspath..stable.attribute",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
query, args, _, err := buildParameterComponentQuery(tt.token)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.ErrorIs(t, err, common.ErrInvalidParameterToken)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Contains(t, query, "INNER JOIN station AS s ON s.id = c.station_id")
|
|
||||||
assert.Contains(t, query, tt.wantWhere)
|
|
||||||
assert.Equal(t, tt.wantArgs, args)
|
|
||||||
assert.NotContains(t, query, "\n")
|
|
||||||
assert.NotContains(t, query, "\t")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryParameterByDataObjectToken(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
const (
|
|
||||||
token = "grid.zone.station.nspath.component.stable.rated_voltage"
|
|
||||||
componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
|
||||||
)
|
|
||||||
|
|
||||||
mock.ExpectQuery(`(?s)SELECT c\.\*.*WHERE g\.tagname = \$1.*AND c\.tag = \$5.*LIMIT 2`).
|
|
||||||
WithArgs("grid", "zone", "station", "nspath", "component").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
|
||||||
"global_uuid", "nspath", "tag", "model_name", "station_id",
|
|
||||||
}).AddRow(componentUUID, "nspath", "component", "bus_1", int64(10)))
|
|
||||||
|
|
||||||
mock.ExpectQuery(`SELECT \* FROM "project_manager" WHERE tag = \$1 AND group_name = \$2 LIMIT \$3`).
|
|
||||||
WithArgs("bus_1", "stable", 2).
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
|
||||||
"id", "name", "tag", "meta_model", "group_name", "link_type", "check_state", "ispublic",
|
|
||||||
}).AddRow(
|
|
||||||
int32(1),
|
|
||||||
"bus_bus_1_stable",
|
|
||||||
"bus_1",
|
|
||||||
"bus",
|
|
||||||
"stable",
|
|
||||||
int32(0),
|
|
||||||
`{"checkState":[{"name":"rated_voltage","checked":1}]}`,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
|
|
||||||
mock.ExpectQuery(`(?s)SELECT pg_catalog\.format_type.*pg_catalog\.pg_attribute.*c\.relname = \$1.*a\.attname = \$2.*LIMIT 1`).
|
|
||||||
WithArgs("bus_bus_1_stable", "rated_voltage").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"format_type"}).AddRow("double precision"))
|
|
||||||
|
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "bus_bus_1_stable" WHERE global_uuid = $1 AND attribute_group = $2`)).
|
|
||||||
WithArgs(componentUUID, "stable").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(int64(1)))
|
|
||||||
|
|
||||||
parameter, err := QueryParameterByDataObjectToken(context.Background(), db, token)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "component", parameter.Component.Tag)
|
|
||||||
assert.Equal(t, "bus_bus_1_stable", parameter.Project.Name)
|
|
||||||
assert.Equal(t, "bus_bus_1_stable", parameter.TableName)
|
|
||||||
assert.Equal(t, "stable", parameter.AttributeGroup)
|
|
||||||
assert.Equal(t, "rated_voltage", parameter.AttributeName)
|
|
||||||
assert.Equal(t, "DOUBLE PRECISION", parameter.AttributeType)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryParameterDataObjectValue(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
|
||||||
parsedUUID, err := uuid.FromString(componentUUID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
parameter := &ParameterDataObject{
|
|
||||||
Component: orm.Component{GlobalUUID: parsedUUID},
|
|
||||||
Project: orm.ProjectManager{
|
|
||||||
Name: "bus_bus_1_stable",
|
|
||||||
},
|
|
||||||
TableName: "bus_bus_1_stable",
|
|
||||||
AttributeGroup: "stable",
|
|
||||||
AttributeName: "rated_voltage",
|
|
||||||
}
|
|
||||||
|
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "bus_bus_1_stable" WHERE global_uuid = $1 AND attribute_group = $2 LIMIT $3`)).
|
|
||||||
WithArgs(componentUUID, "stable", 1).
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
|
||||||
"global_uuid", "attribute_group", "rated_voltage",
|
|
||||||
}).AddRow(componentUUID, "stable", float64(220)))
|
|
||||||
|
|
||||||
value, err := QueryParameterDataObjectValue(context.Background(), db, parameter)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, float64(220), value)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryComponentParameterByDataObjectToken(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
const (
|
|
||||||
token = "grid.zone.station.nspath.component.component.description"
|
|
||||||
componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
|
||||||
)
|
|
||||||
mock.ExpectQuery(`(?s)SELECT c\.\*.*WHERE g\.tagname = \$1.*AND c\.tag = \$5.*LIMIT 2`).
|
|
||||||
WithArgs("grid", "zone", "station", "nspath", "component").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
|
||||||
"global_uuid", "nspath", "tag", "model_name", "grid", "zone", "station", "station_id",
|
|
||||||
}).AddRow(componentUUID, "nspath", "component", "bus_1", "grid", "zone", "station", int64(10)))
|
|
||||||
mock.ExpectQuery(`(?s)SELECT pg_catalog\.format_type.*c\.relname = \$1.*a\.attname = \$2.*LIMIT 1`).
|
|
||||||
WithArgs("component", "description").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"format_type"}).AddRow("character varying(512)"))
|
|
||||||
|
|
||||||
parameter, err := QueryParameterByDataObjectToken(context.Background(), db, token)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "component", parameter.TableName)
|
|
||||||
assert.Equal(t, "component", parameter.AttributeGroup)
|
|
||||||
assert.Equal(t, "description", parameter.AttributeName)
|
|
||||||
assert.Equal(t, "CHARACTER VARYING(512)", parameter.AttributeType)
|
|
||||||
assert.Empty(t, parameter.Project.Name)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryComponentParameterValue(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
parameter := &ParameterDataObject{
|
|
||||||
Component: orm.Component{Tag: "component"},
|
|
||||||
TableName: "component",
|
|
||||||
AttributeGroup: "component",
|
|
||||||
AttributeName: "description",
|
|
||||||
}
|
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "component" WHERE tag = $1 LIMIT $2`)).
|
|
||||||
WithArgs("component", 1).
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"tag", "description"}).AddRow("component", "测试组件"))
|
|
||||||
|
|
||||||
value, err := QueryParameterDataObjectValue(context.Background(), db, parameter)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "测试组件", value)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryParameterAttributeDescription(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
mock.ExpectQuery(`(?s)SELECT attribute_name.*FROM basic\.attribute.*WHERE attribute = \$1.*LIMIT 2`).
|
|
||||||
WithArgs("rated_voltage").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"attribute_name"}).AddRow("额定电压"))
|
|
||||||
|
|
||||||
description, err := QueryParameterAttributeDescription(context.Background(), db, "rated_voltage")
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "额定电压", description)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueryParameterByDataObjectTokenComponentNotFound(t *testing.T) {
|
|
||||||
sqlDB, mock, err := sqlmock.New()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
mock.ExpectQuery(`(?s)SELECT c\.\*.*WHERE c\.nspath = \$1.*AND c\.tag = \$2.*s\.is_local = TRUE.*LIMIT 2`).
|
|
||||||
WithArgs("nspath", "component").
|
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"global_uuid"}))
|
|
||||||
|
|
||||||
_, err = QueryParameterByDataObjectToken(
|
|
||||||
context.Background(),
|
|
||||||
db,
|
|
||||||
"nspath.component.stable.rated_voltage",
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.ErrorIs(t, err, common.ErrParameterTokenNotFound)
|
|
||||||
require.NoError(t, mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Package database define database operation functions
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"modelRT/orm"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QueryMeasurementByID return the result of query circuit diagram component measurement info by id from postgresDB
|
||||||
|
func QueryMeasurementByID(ctx context.Context, tx *gorm.DB, id int64) (orm.Measurement, error) {
|
||||||
|
var measurement orm.Measurement
|
||||||
|
// ctx超时判断
|
||||||
|
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
result := tx.WithContext(cancelCtx).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
First(&measurement)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return orm.Measurement{}, result.Error
|
||||||
|
}
|
||||||
|
return measurement, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryMeasurementByToken define function query circuit diagram component measurement info by token from postgresDB
|
||||||
|
func QueryMeasurementByToken(ctx context.Context, tx *gorm.DB, token string) (orm.Measurement, error) {
|
||||||
|
// TODO parse token to avoid SQL injection
|
||||||
|
|
||||||
|
var component orm.Measurement
|
||||||
|
// ctx超时判断
|
||||||
|
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
result := tx.WithContext(cancelCtx).
|
||||||
|
Where(" = ?", token).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
First(&component)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return orm.Measurement{}, result.Error
|
||||||
|
}
|
||||||
|
return component, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllMeasurements define func to query all measurement info from postgresDB
|
||||||
|
func GetAllMeasurements(ctx context.Context, tx *gorm.DB) ([]orm.Measurement, error) {
|
||||||
|
var measurements []orm.Measurement
|
||||||
|
|
||||||
|
// ctx超时判断
|
||||||
|
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&measurements)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return measurements, nil
|
||||||
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ func generateNormalData(baseValue, normalBase float64) []float64 {
|
||||||
func main() {
|
func main() {
|
||||||
rootCtx := context.Background()
|
rootCtx := context.Background()
|
||||||
|
|
||||||
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "localhost", 5432, "postgres", "coslight", "develop_env")
|
pgURI := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", "192.168.1.101", 5432, "postgres", "coslight", "demo")
|
||||||
|
|
||||||
postgresDBClient, err := gorm.Open(postgres.Open(pgURI))
|
postgresDBClient, err := gorm.Open(postgres.Open(pgURI))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -164,6 +164,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
datas = generateMixedData(highMin, lowMin, highBase, lowBase, baseValue, normalBase)
|
datas = generateMixedData(highMin, lowMin, highBase, lowBase, baseValue, normalBase)
|
||||||
|
// log.Printf("key:%s\n datas:%v\n", key, datas)
|
||||||
|
|
||||||
allHigh := true
|
allHigh := true
|
||||||
for i := highStart; i < highEnd; i++ {
|
for i := highStart; i < highEnd; i++ {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
)
|
)
|
||||||
|
|
@ -62,7 +61,7 @@ func ProcessMeasurements(measurements []orm.Measurement) map[string]CalculationR
|
||||||
device, _ := ioAddress["device"].(string)
|
device, _ := ioAddress["device"].(string)
|
||||||
channel, _ := ioAddress["channel"].(string)
|
channel, _ := ioAddress["channel"].(string)
|
||||||
|
|
||||||
result := strings.ToLower(fmt.Sprintf("%s:%s:phasor:%s", station, device, channel))
|
result := fmt.Sprintf("%s:%s:phasor:%s", station, device, channel)
|
||||||
if measurement.EventPlan == nil {
|
if measurement.EventPlan == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ package diagram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
@ -14,46 +12,6 @@ type RedisClient struct {
|
||||||
Client *redis.Client
|
Client *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryLatestMeasurementValue returns the score whose member contains the
|
|
||||||
// greatest numeric timestamp. Measurement ZSets currently store timestamp in
|
|
||||||
// member and measurement value in score.
|
|
||||||
func (rc *RedisClient) QueryLatestMeasurementValue(ctx context.Context, key string) (float64, error) {
|
|
||||||
if rc.Client == nil {
|
|
||||||
return 0, fmt.Errorf("redis client is not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
members, err := rc.Client.ZRangeWithScores(ctx, key, 0, -1).Result()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return latestMeasurementValue(members, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func latestMeasurementValue(members []redis.Z, key string) (float64, error) {
|
|
||||||
if len(members) == 0 {
|
|
||||||
return 0, fmt.Errorf("real-time measurement value not found for key %q", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
var latestTimestamp int64
|
|
||||||
var latestValue float64
|
|
||||||
found := false
|
|
||||||
for _, member := range members {
|
|
||||||
timestamp, err := strconv.ParseInt(fmt.Sprint(member.Member), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !found || timestamp > latestTimestamp {
|
|
||||||
latestTimestamp = timestamp
|
|
||||||
latestValue = member.Score
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return 0, fmt.Errorf("real-time measurement timestamps are invalid for key %q", key)
|
|
||||||
}
|
|
||||||
return latestValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRedisClient define func of new redis client instance
|
// NewRedisClient define func of new redis client instance
|
||||||
func NewRedisClient() *RedisClient {
|
func NewRedisClient() *RedisClient {
|
||||||
return &RedisClient{
|
return &RedisClient{
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package diagram
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLatestMeasurementValueUsesMemberTimestamp(t *testing.T) {
|
|
||||||
value, err := latestMeasurementValue([]redis.Z{
|
|
||||||
{Member: "100", Score: 999},
|
|
||||||
{Member: "300", Score: 12},
|
|
||||||
{Member: "200", Score: 500},
|
|
||||||
}, "measurement-key")
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, float64(12), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLatestMeasurementValueRejectsMissingOrInvalidTimestamps(t *testing.T) {
|
|
||||||
_, err := latestMeasurementValue(nil, "measurement-key")
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
_, err = latestMeasurementValue([]redis.Z{{Member: "invalid", Score: 1}}, "measurement-key")
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
@ -154,7 +154,8 @@ func validateBatchImportParams(params map[string]any) bool {
|
||||||
func validateTestTaskParams(params map[string]any) bool {
|
func validateTestTaskParams(params map[string]any) bool {
|
||||||
// Test task has optional parameters, all are valid
|
// Test task has optional parameters, all are valid
|
||||||
// sleep_duration defaults to 60 seconds if not provided
|
// sleep_duration defaults to 60 seconds if not provided
|
||||||
fmt.Println("Test task parameters:", params)
|
// TODO Add more validation logic for test task parameters if needed
|
||||||
|
fmt.Println(params)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
|
||||||
"modelRT/common/errcode"
|
"modelRT/common/errcode"
|
||||||
"modelRT/constants"
|
"modelRT/constants"
|
||||||
"modelRT/database"
|
"modelRT/database"
|
||||||
|
|
@ -16,7 +18,6 @@ import (
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gofrs/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
|
// ComponentAttributeQueryHandler define circuit diagram component attribute value query process API
|
||||||
|
|
|
||||||
|
|
@ -1,340 +0,0 @@
|
||||||
// Package handler provides HTTP handlers for various endpoints.
|
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/common"
|
|
||||||
"modelRT/common/errcode"
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/database"
|
|
||||||
"modelRT/diagram"
|
|
||||||
"modelRT/logger"
|
|
||||||
"modelRT/model"
|
|
||||||
"modelRT/orm"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataObjectAttributeQueryHandler define data object attribute value query process API
|
|
||||||
func DataObjectAttributeQueryHandler(c *gin.Context) {
|
|
||||||
ctx := c.Request.Context()
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
|
||||||
|
|
||||||
token := c.Param("token")
|
|
||||||
field := c.Param("field")
|
|
||||||
if token == "" {
|
|
||||||
err := fmt.Errorf("tokens is missing from the path")
|
|
||||||
logger.Error(ctx, "query tokens from path failed", "error", err, "url", c.Request.RequestURI)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
} else if field == "" {
|
|
||||||
err := fmt.Errorf("field is missing from the path")
|
|
||||||
logger.Error(ctx, "query field from path failed", "error", err, "url", c.Request.RequestURI)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedField := strings.ToLower(field)
|
|
||||||
if err := validateDataObjectField(token, dataObjectType, normalizedField); err != nil {
|
|
||||||
logger.Warn(ctx, "validate data object field failed", "token", token, "field", field, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var parameter *database.ParameterDataObject
|
|
||||||
var measurement *orm.Measurement
|
|
||||||
var measurementComponent *orm.Component
|
|
||||||
switch dataObjectType {
|
|
||||||
case constants.DataObjectTypeParameter:
|
|
||||||
// 参量支持两种形式token4.token5.token6.token7与token1.token2.token3.token4.token5.token6.token7
|
|
||||||
parameter, err = database.QueryParameterByDataObjectToken(ctx, pgClient, token)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, common.ErrInvalidParameterToken) ||
|
|
||||||
errors.Is(err, common.ErrParameterTokenNotFound) ||
|
|
||||||
errors.Is(err, common.ErrAmbiguousParameterToken) {
|
|
||||||
logger.Warn(ctx, "validate parameter token failed", "token", token, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(ctx, "query parameter token from postgres failed", "token", token, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "validate parameter token failed", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case constants.DataObjectTypeMeasurement:
|
|
||||||
// 量测支持token1.token2.token3.token4.token5.token6.token7、token4.token5.token6.token7、token4.token7
|
|
||||||
measurement, measurementComponent, err = database.QueryMeasurementByDataObjectToken(ctx, pgClient, token)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, common.ErrInvalidMeasurementToken) ||
|
|
||||||
errors.Is(err, common.ErrMeasurementTokenNotFound) ||
|
|
||||||
errors.Is(err, common.ErrAmbiguousMeasurementToken) {
|
|
||||||
logger.Warn(ctx, "validate measurement token failed", "token", token, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(ctx, "query measurement token from postgres failed", "token", token, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "validate measurement token failed", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dataObjectType {
|
|
||||||
case constants.DataObjectTypeParameter:
|
|
||||||
value, err := buildParameterAttributeValue(
|
|
||||||
ctx,
|
|
||||||
normalizedField,
|
|
||||||
parameter,
|
|
||||||
func(ctx context.Context, parameter *database.ParameterDataObject) (any, error) {
|
|
||||||
return database.QueryParameterDataObjectValue(ctx, pgClient, parameter)
|
|
||||||
},
|
|
||||||
func(ctx context.Context, attributeName string) (string, error) {
|
|
||||||
return database.QueryParameterAttributeDescription(ctx, pgClient, attributeName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, common.ErrUnsupportedParameterField) {
|
|
||||||
logger.Warn(ctx, "query unsupported parameter field", "token", token, "field", field, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(ctx, "build parameter attribute value failed", "token", token, "field", field, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "query parameter attribute failed", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := dataObjectAttributeQueryResult{
|
|
||||||
Token: token,
|
|
||||||
Field: normalizedField,
|
|
||||||
Code: errcode.ErrProcessSuccess.Code(),
|
|
||||||
Msg: errcode.ErrProcessSuccess.Msg(),
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
renderRespSuccess(c, constants.RespCodeSuccess, "query parameter attribute success", map[string]any{
|
|
||||||
"attributes": []dataObjectAttributeQueryResult{result},
|
|
||||||
})
|
|
||||||
case constants.DataObjectTypeMeasurement:
|
|
||||||
value, err := buildMeasurementAttributeValue(
|
|
||||||
ctx,
|
|
||||||
normalizedField,
|
|
||||||
measurement,
|
|
||||||
measurementComponent,
|
|
||||||
queryMeasurementRealtimeValue,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, common.ErrUnsupportedMeasurementField) {
|
|
||||||
logger.Warn(ctx, "query unsupported measurement field", "token", token, "field", field, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(ctx, "build measurement attribute value failed", "token", token, "field", field, "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "query measurement attribute failed", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := dataObjectAttributeQueryResult{
|
|
||||||
Token: token,
|
|
||||||
Field: normalizedField,
|
|
||||||
Code: errcode.ErrProcessSuccess.Code(),
|
|
||||||
Msg: errcode.ErrProcessSuccess.Msg(),
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
renderRespSuccess(c, constants.RespCodeSuccess, "query measurement attribute success", map[string]any{
|
|
||||||
"attributes": []dataObjectAttributeQueryResult{result},
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, "invalid data object type", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
|
|
||||||
|
|
||||||
type parameterValueLoader func(context.Context, *database.ParameterDataObject) (any, error)
|
|
||||||
|
|
||||||
type parameterDescriptionLoader func(context.Context, string) (string, error)
|
|
||||||
|
|
||||||
var measurementDataObjectFields = map[string]struct{}{
|
|
||||||
"value": {},
|
|
||||||
"mode": {},
|
|
||||||
"meta": {},
|
|
||||||
"type": {},
|
|
||||||
"name": {},
|
|
||||||
"description": {},
|
|
||||||
"id": {},
|
|
||||||
"size": {},
|
|
||||||
"data_source": {},
|
|
||||||
"event_plan": {},
|
|
||||||
"binding": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
var parameterDataObjectFields = map[string]struct{}{
|
|
||||||
"value": {},
|
|
||||||
"name": {},
|
|
||||||
"meta": {},
|
|
||||||
"type": {},
|
|
||||||
"description": {},
|
|
||||||
"id": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
type dataObjectAttributeQueryResult struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
Field string `json:"field"`
|
|
||||||
Code int `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Value any `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateDataObjectField(token string, dataObjectType constants.DataObjectType, field string) error {
|
|
||||||
field = strings.ToLower(field)
|
|
||||||
switch dataObjectType {
|
|
||||||
case constants.DataObjectTypeMeasurement:
|
|
||||||
if _, ok := measurementDataObjectFields[field]; ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
|
|
||||||
case constants.DataObjectTypeParameter:
|
|
||||||
if _, ok := parameterDataObjectFields[field]; ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w: %s", common.ErrUnsupportedParameterField, field)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid data object type %q", dataObjectType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildParameterAttributeValue(
|
|
||||||
ctx context.Context,
|
|
||||||
field string,
|
|
||||||
parameter *database.ParameterDataObject,
|
|
||||||
loadValue parameterValueLoader,
|
|
||||||
loadDescription parameterDescriptionLoader,
|
|
||||||
) (any, error) {
|
|
||||||
if parameter == nil {
|
|
||||||
return nil, fmt.Errorf("parameter data object is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
component := parameter.Component
|
|
||||||
switch field {
|
|
||||||
case "value":
|
|
||||||
if loadValue == nil {
|
|
||||||
return nil, fmt.Errorf("parameter value loader is nil")
|
|
||||||
}
|
|
||||||
return loadValue(ctx, parameter)
|
|
||||||
case "meta":
|
|
||||||
return "PARAM", nil
|
|
||||||
case "type":
|
|
||||||
return parameter.AttributeType, nil
|
|
||||||
case "name":
|
|
||||||
return strings.Join([]string{
|
|
||||||
component.NSPath,
|
|
||||||
component.Tag,
|
|
||||||
parameter.AttributeGroup,
|
|
||||||
parameter.AttributeName,
|
|
||||||
}, "."), nil
|
|
||||||
case "description":
|
|
||||||
if loadDescription == nil {
|
|
||||||
return nil, fmt.Errorf("parameter description loader is nil")
|
|
||||||
}
|
|
||||||
return loadDescription(ctx, parameter.AttributeName)
|
|
||||||
case "id":
|
|
||||||
return strings.Join([]string{
|
|
||||||
component.GridName,
|
|
||||||
component.ZoneName,
|
|
||||||
component.StationName,
|
|
||||||
component.NSPath,
|
|
||||||
component.Tag,
|
|
||||||
parameter.AttributeGroup,
|
|
||||||
parameter.AttributeName,
|
|
||||||
}, "."), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: %s", common.ErrUnsupportedParameterField, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMeasurementAttributeValue(
|
|
||||||
ctx context.Context,
|
|
||||||
field string,
|
|
||||||
measurement *orm.Measurement,
|
|
||||||
component *orm.Component,
|
|
||||||
loadValue measurementValueLoader,
|
|
||||||
) (any, error) {
|
|
||||||
if measurement == nil {
|
|
||||||
return nil, fmt.Errorf("measurement is nil")
|
|
||||||
}
|
|
||||||
if component == nil {
|
|
||||||
return nil, fmt.Errorf("measurement component is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch field {
|
|
||||||
case "value":
|
|
||||||
if loadValue == nil {
|
|
||||||
return nil, fmt.Errorf("measurement value loader is nil")
|
|
||||||
}
|
|
||||||
return loadValue(ctx, measurement.DataSource)
|
|
||||||
case "mode":
|
|
||||||
fmt.Println("measurement.Mode:", measurement.Mode)
|
|
||||||
switch measurement.Mode {
|
|
||||||
case 0:
|
|
||||||
return false, nil
|
|
||||||
case 1:
|
|
||||||
return true, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid measurement mode %d: expected 0 or 1", measurement.Mode)
|
|
||||||
}
|
|
||||||
case "meta":
|
|
||||||
return "MEASUREMENT", nil
|
|
||||||
case "type":
|
|
||||||
return model.MeasurementTypeFromDataSource(measurement.DataSource)
|
|
||||||
case "name":
|
|
||||||
// The resolved measurement and component prove that token4.token7 exists.
|
|
||||||
return component.NSPath + "." + measurement.Tag, nil
|
|
||||||
case "description":
|
|
||||||
return measurement.Name, nil
|
|
||||||
case "id":
|
|
||||||
return strings.Join([]string{
|
|
||||||
component.GridName,
|
|
||||||
component.ZoneName,
|
|
||||||
component.StationName,
|
|
||||||
component.NSPath,
|
|
||||||
component.Tag,
|
|
||||||
"bay",
|
|
||||||
measurement.Tag,
|
|
||||||
}, "."), nil
|
|
||||||
case "size":
|
|
||||||
return measurement.Size, nil
|
|
||||||
case "data_source":
|
|
||||||
return measurement.DataSource, nil
|
|
||||||
case "event_plan":
|
|
||||||
return measurement.EventPlan, nil
|
|
||||||
case "binding":
|
|
||||||
return measurement.Binding, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: %s", common.ErrUnsupportedMeasurementField, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryMeasurementRealtimeValue(ctx context.Context, dataSource orm.JSONMap) (any, error) {
|
|
||||||
queryKey, err := model.GenerateMeasureIdentifier(dataSource)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generate measurement redis key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := diagram.NewRedisClient().QueryLatestMeasurementValue(ctx, queryKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query real-time measurement value by key %q: %w", queryKey, err)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,215 +0,0 @@
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"modelRT/common"
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/database"
|
|
||||||
"modelRT/orm"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateDataObjectField(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
token string
|
|
||||||
dataObjectType constants.DataObjectType
|
|
||||||
field string
|
|
||||||
wantErr error
|
|
||||||
}{
|
|
||||||
{name: "bay value", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "value"},
|
|
||||||
{name: "bay name", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "name"},
|
|
||||||
{name: "bay binding", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "binding"},
|
|
||||||
{name: "bay field is case insensitive", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "DATA_SOURCE"},
|
|
||||||
{name: "bay unsupported", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "unknown", wantErr: common.ErrUnsupportedMeasurementField},
|
|
||||||
{name: "parameter value", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "value"},
|
|
||||||
{name: "parameter name", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "name"},
|
|
||||||
{name: "parameter rejects mode", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "mode", wantErr: common.ErrUnsupportedParameterField},
|
|
||||||
{name: "component name", token: "nspath.component.component.name", dataObjectType: constants.DataObjectTypeParameter, field: "name"},
|
|
||||||
{name: "component rejects mode", token: "nspath.component.component.name", dataObjectType: constants.DataObjectTypeParameter, field: "mode", wantErr: common.ErrUnsupportedParameterField},
|
|
||||||
{name: "parameter rejects size", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "size", wantErr: common.ErrUnsupportedParameterField},
|
|
||||||
{name: "invalid type", token: "token", dataObjectType: constants.DataObjectType("unknown"), field: "value", wantErr: assert.AnError},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
err := validateDataObjectField(tt.token, tt.dataObjectType, tt.field)
|
|
||||||
if tt.wantErr == nil {
|
|
||||||
require.NoError(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.Error(t, err)
|
|
||||||
if tt.wantErr != assert.AnError {
|
|
||||||
assert.ErrorIs(t, err, tt.wantErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildParameterAttributeValue(t *testing.T) {
|
|
||||||
parameter := &database.ParameterDataObject{
|
|
||||||
Component: orm.Component{
|
|
||||||
GridName: "grid000",
|
|
||||||
ZoneName: "zone000",
|
|
||||||
StationName: "station000",
|
|
||||||
NSPath: "110kV_TV",
|
|
||||||
Tag: "cable_22",
|
|
||||||
},
|
|
||||||
AttributeGroup: "rated",
|
|
||||||
AttributeName: "rated_voltage",
|
|
||||||
AttributeType: "DOUBLE PRECISION",
|
|
||||||
}
|
|
||||||
loader := func(_ context.Context, actual *database.ParameterDataObject) (any, error) {
|
|
||||||
assert.Same(t, parameter, actual)
|
|
||||||
return float64(220), nil
|
|
||||||
}
|
|
||||||
descriptionLoader := func(_ context.Context, attributeName string) (string, error) {
|
|
||||||
assert.Equal(t, "rated_voltage", attributeName)
|
|
||||||
return "额定电压", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
field string
|
|
||||||
expected any
|
|
||||||
}{
|
|
||||||
{field: "value", expected: float64(220)},
|
|
||||||
{field: "meta", expected: "PARAM"},
|
|
||||||
{field: "type", expected: "DOUBLE PRECISION"},
|
|
||||||
{field: "name", expected: "110kV_TV.cable_22.rated.rated_voltage"},
|
|
||||||
{field: "description", expected: "额定电压"},
|
|
||||||
{field: "id", expected: "grid000.zone000.station000.110kV_TV.cable_22.rated.rated_voltage"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.field, func(t *testing.T) {
|
|
||||||
actual, err := buildParameterAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
tt.field,
|
|
||||||
parameter,
|
|
||||||
loader,
|
|
||||||
descriptionLoader,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildParameterAttributeValueRejectsUnsupportedField(t *testing.T) {
|
|
||||||
_, err := buildParameterAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
"unknown",
|
|
||||||
&database.ParameterDataObject{},
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.ErrorIs(t, err, common.ErrUnsupportedParameterField)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMeasurementAttributeValue(t *testing.T) {
|
|
||||||
dataSource := orm.JSONMap{
|
|
||||||
"type": float64(1),
|
|
||||||
"io_address": map[string]any{
|
|
||||||
"channel": "tm1p",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
eventPlan := orm.JSONMap{"enabled": true}
|
|
||||||
binding := orm.JSONMap{"ct": map[string]any{"ratio": float64(2)}}
|
|
||||||
measurement := &orm.Measurement{
|
|
||||||
Tag: "IA_rms",
|
|
||||||
Name: "A相电流",
|
|
||||||
Mode: 1,
|
|
||||||
Size: 10,
|
|
||||||
DataSource: dataSource,
|
|
||||||
EventPlan: eventPlan,
|
|
||||||
Binding: binding,
|
|
||||||
}
|
|
||||||
component := &orm.Component{
|
|
||||||
GridName: "grid000",
|
|
||||||
ZoneName: "zone000",
|
|
||||||
StationName: "station000",
|
|
||||||
NSPath: "110kV_TV",
|
|
||||||
Tag: "cable_22",
|
|
||||||
}
|
|
||||||
|
|
||||||
loader := func(_ context.Context, source orm.JSONMap) (any, error) {
|
|
||||||
assert.Equal(t, dataSource, source)
|
|
||||||
return float64(220), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
field string
|
|
||||||
expected any
|
|
||||||
}{
|
|
||||||
{field: "value", expected: float64(220)},
|
|
||||||
{field: "mode", expected: true},
|
|
||||||
{field: "meta", expected: "MEASUREMENT"},
|
|
||||||
{field: "type", expected: "TM"},
|
|
||||||
{field: "name", expected: "110kV_TV.IA_rms"},
|
|
||||||
{field: "description", expected: "A相电流"},
|
|
||||||
{field: "id", expected: "grid000.zone000.station000.110kV_TV.cable_22.bay.IA_rms"},
|
|
||||||
{field: "size", expected: 10},
|
|
||||||
{field: "data_source", expected: dataSource},
|
|
||||||
{field: "event_plan", expected: eventPlan},
|
|
||||||
{field: "binding", expected: binding},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.field, func(t *testing.T) {
|
|
||||||
actual, err := buildMeasurementAttributeValue(context.Background(), tt.field, measurement, component, loader)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMeasurementAttributeValueRejectsUnsupportedField(t *testing.T) {
|
|
||||||
_, err := buildMeasurementAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
"unknown",
|
|
||||||
&orm.Measurement{},
|
|
||||||
&orm.Component{},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.ErrorIs(t, err, common.ErrUnsupportedMeasurementField)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMeasurementAttributeValueMode(t *testing.T) {
|
|
||||||
component := &orm.Component{}
|
|
||||||
|
|
||||||
manualMode, err := buildMeasurementAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
"mode",
|
|
||||||
&orm.Measurement{Mode: 0},
|
|
||||||
component,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, false, manualMode)
|
|
||||||
|
|
||||||
automaticMode, err := buildMeasurementAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
"mode",
|
|
||||||
&orm.Measurement{Mode: 1},
|
|
||||||
component,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, true, automaticMode)
|
|
||||||
|
|
||||||
_, err = buildMeasurementAttributeValue(
|
|
||||||
context.Background(),
|
|
||||||
"mode",
|
|
||||||
&orm.Measurement{Mode: 2},
|
|
||||||
component,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "expected 0 or 1")
|
|
||||||
}
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
// Package handler provides HTTP handlers for various endpoints.
|
|
||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/common/errcode"
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/database"
|
|
||||||
"modelRT/diagram"
|
|
||||||
"modelRT/logger"
|
|
||||||
"modelRT/network"
|
|
||||||
"modelRT/orm"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DataObjectAttributeUpdateHandler define data object attribute value update process API
|
|
||||||
func DataObjectAttributeUpdateHandler(c *gin.Context) {
|
|
||||||
pgClient := database.GetPostgresDBClient()
|
|
||||||
var request network.ComponentAttributeUpdateInfo
|
|
||||||
if err := c.ShouldBindJSON(&request); err != nil {
|
|
||||||
logger.Error(c, "unmarshal request params failed", "error", err)
|
|
||||||
renderRespFailure(c, constants.RespCodeInvalidParams, err.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updateResults := make(map[string]*errcode.AppError)
|
|
||||||
attriModifyConfs := make([]attributeModifyConfig, 0, len(request.AttributeConfigs))
|
|
||||||
var attributeComponentTag string
|
|
||||||
for index, attribute := range request.AttributeConfigs {
|
|
||||||
slices := strings.Split(attribute.AttributeToken, ".")
|
|
||||||
if len(slices) < 7 {
|
|
||||||
updateResults[attribute.AttributeToken] = errcode.ErrInvalidToken
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
componentTag := slices[4]
|
|
||||||
if index == 0 {
|
|
||||||
attributeComponentTag = componentTag
|
|
||||||
} else if componentTag != attributeComponentTag {
|
|
||||||
updateResults[attribute.AttributeToken] = errcode.ErrCrossToken
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
attriModifyConfs = append(attriModifyConfs, attributeModifyConfig{
|
|
||||||
attributeToken: attribute.AttributeToken,
|
|
||||||
attributeExtendType: slices[5],
|
|
||||||
attributeName: slices[6],
|
|
||||||
attributeOldVal: attribute.AttributeOldVal,
|
|
||||||
attributeNewVal: attribute.AttributeNewVal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// open transaction
|
|
||||||
tx := pgClient.WithContext(c).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
logger.Error(c, "begin postgres transaction failed", "error", tx.Error)
|
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "begin postgres transaction failed", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
compInfo, err := database.QueryComponentByCompTag(c, tx, attributeComponentTag)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(c, "query component info by component tag failed", "error", err, "tag", attributeComponentTag)
|
|
||||||
|
|
||||||
for _, attribute := range request.AttributeConfigs {
|
|
||||||
if _, exists := updateResults[attribute.AttributeToken]; !exists {
|
|
||||||
updateResults[attribute.AttributeToken] = errcode.ErrDBQueryFailed.WithCause(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Rollback()
|
|
||||||
|
|
||||||
payload := genUpdateRespPayload(updateResults, request.AttributeConfigs)
|
|
||||||
renderRespFailure(c, constants.RespCodeFailed, "query component metadata failed", payload)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
identifiers := make([]orm.ProjectIdentifier, len(attriModifyConfs))
|
|
||||||
for i, mod := range attriModifyConfs {
|
|
||||||
identifiers[i] = orm.ProjectIdentifier{
|
|
||||||
Token: mod.attributeToken,
|
|
||||||
Tag: compInfo.ModelName,
|
|
||||||
GroupName: mod.attributeExtendType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tableNameMap, err := database.BatchGetProjectNames(tx, identifiers)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
|
|
||||||
for _, id := range identifiers {
|
|
||||||
if _, exists := updateResults[id.Token]; !exists {
|
|
||||||
updateResults[id.Token] = errcode.ErrRetrieveFailed.WithCause(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := genUpdateRespPayload(updateResults, request.AttributeConfigs)
|
|
||||||
renderRespFailure(c, constants.RespCodeFailed, "batch retrieve table names failed", payload)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redisUpdateMap := make(map[string][]cacheUpdateItem)
|
|
||||||
for _, mod := range attriModifyConfs {
|
|
||||||
id := orm.ProjectIdentifier{Tag: compInfo.ModelName, GroupName: mod.attributeExtendType}
|
|
||||||
tableName, exists := tableNameMap[id]
|
|
||||||
if !exists {
|
|
||||||
updateResults[mod.attributeToken] = errcode.ErrFoundTargetFailed
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result := tx.Table(tableName).
|
|
||||||
Where(fmt.Sprintf("%s = ? AND global_uuid = ?", mod.attributeName), mod.attributeOldVal, compInfo.GlobalUUID).
|
|
||||||
Updates(map[string]any{mod.attributeName: mod.attributeNewVal})
|
|
||||||
|
|
||||||
if result.Error != nil {
|
|
||||||
updateResults[mod.attributeToken] = errcode.ErrDBUpdateFailed
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
updateResults[mod.attributeToken] = errcode.ErrDBzeroAffectedRows
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheKey := fmt.Sprintf("%s_%s", attributeComponentTag, mod.attributeExtendType)
|
|
||||||
redisUpdateMap[cacheKey] = append(redisUpdateMap[cacheKey],
|
|
||||||
cacheUpdateItem{
|
|
||||||
token: mod.attributeToken,
|
|
||||||
name: mod.attributeName,
|
|
||||||
newVal: mod.attributeNewVal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit transaction
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "transaction commit failed", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, items := range redisUpdateMap {
|
|
||||||
hset := diagram.NewRedisHash(c, key, 5000, false)
|
|
||||||
|
|
||||||
fields := make(map[string]any, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
fields[item.name] = item.newVal
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hset.SetRedisHashByMap(fields); err != nil {
|
|
||||||
logger.Error(c, "batch sync redis failed", "hash_key", key, "error", err)
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
if _, exists := updateResults[item.token]; exists {
|
|
||||||
updateResults[item.token] = errcode.ErrCacheSyncWarn.WithCause(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := genUpdateRespPayload(updateResults, request.AttributeConfigs)
|
|
||||||
if len(updateResults) > 0 {
|
|
||||||
renderRespFailure(c, constants.RespCodeFailed, "process completed with partial failures", payload)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
renderRespSuccess(c, constants.RespCodeSuccess, "process completed successfully", payload)
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
package logger_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"modelRT/config"
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/logger"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCallerPointsToBusinessCode(t *testing.T) {
|
|
||||||
reader, writer, err := os.Pipe()
|
|
||||||
require.NoError(t, err)
|
|
||||||
originalStdout := os.Stdout
|
|
||||||
os.Stdout = writer
|
|
||||||
t.Cleanup(func() {
|
|
||||||
os.Stdout = originalStdout
|
|
||||||
_ = reader.Close()
|
|
||||||
_ = writer.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.InitLoggerInstance(config.LoggerConfig{
|
|
||||||
Mode: constants.DevelopmentLogMode,
|
|
||||||
Level: "info",
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.Info(context.Background(), "facade caller test")
|
|
||||||
logger.NewGormLogger().Trace(context.Background(), time.Now(), func() (string, int64) {
|
|
||||||
return "SELECT 1", 1
|
|
||||||
}, nil)
|
|
||||||
require.NoError(t, writer.Close())
|
|
||||||
|
|
||||||
outputBytes, err := io.ReadAll(reader)
|
|
||||||
require.NoError(t, err)
|
|
||||||
output := string(outputBytes)
|
|
||||||
|
|
||||||
assert.NotContains(t, output, "logger/facede.go")
|
|
||||||
assert.NotContains(t, output, `"func":"modelRT/logger.Info"`)
|
|
||||||
assert.NotContains(t, output, "gorm.io/gorm")
|
|
||||||
assert.Contains(t, output, "caller_test.go")
|
|
||||||
assert.True(t, strings.Count(output, "modelRT/logger_test.TestCallerPointsToBusinessCode") >= 2)
|
|
||||||
}
|
|
||||||
|
|
@ -18,6 +18,8 @@ type facade struct {
|
||||||
_logger *zap.Logger
|
_logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const facadeCallerSkip = 2
|
||||||
|
|
||||||
// Debug define facade func of debug level log
|
// Debug define facade func of debug level log
|
||||||
func Debug(ctx context.Context, msg string, kv ...any) {
|
func Debug(ctx context.Context, msg string, kv ...any) {
|
||||||
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
|
logFacade().log(ctx, zapcore.DebugLevel, msg, kv...)
|
||||||
|
|
@ -39,17 +41,16 @@ func Error(ctx context.Context, msg string, kv ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *facade) log(ctx context.Context, lvl zapcore.Level, msg string, kv ...any) {
|
func (f *facade) log(ctx context.Context, lvl zapcore.Level, msg string, kv ...any) {
|
||||||
f.logSkip(ctx, lvl, 0, msg, kv...)
|
f.logSkip(ctx, lvl, 1, msg, kv...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
|
func (f *facade) logSkip(ctx context.Context, lvl zapcore.Level, extraSkip int, msg string, kv ...any) {
|
||||||
caller := resolveLoggerCaller(extraSkip)
|
fields := makeLogFieldsSkip(ctx, extraSkip, kv...)
|
||||||
fields := makeLogFieldsWithCaller(ctx, caller, kv...)
|
logger := f._logger
|
||||||
ce := f._logger.Check(lvl, msg)
|
if extraSkip > 0 {
|
||||||
if ce == nil {
|
logger = logger.WithOptions(zap.AddCallerSkip(extraSkip))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
setCheckedEntryCaller(ce, caller)
|
ce := logger.Check(lvl, msg)
|
||||||
ce.Write(fields...)
|
ce.Write(fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +72,7 @@ func InfoSkip(ctx context.Context, extraSkip int, msg string, kv ...any) {
|
||||||
func logFacade() *facade {
|
func logFacade() *facade {
|
||||||
fOnce.Do(func() {
|
fOnce.Do(func() {
|
||||||
f = &facade{
|
f = &facade{
|
||||||
_logger: GetLoggerInstance(),
|
_logger: GetLoggerInstance().WithOptions(zap.AddCallerSkip(facadeCallerSkip)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return f
|
return f
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,12 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql
|
||||||
// get gorm exec sql and rows affected
|
// get gorm exec sql and rows affected
|
||||||
sql, rows := fc()
|
sql, rows := fc()
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
ErrorSkip(ctx, 0, "SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
|
ErrorSkip(ctx, 1, "SQL ERROR", "sql", sql, "rows", rows, "dur(ms)", duration)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if duration > l.SlowThreshold.Milliseconds() {
|
if duration > l.SlowThreshold.Milliseconds() {
|
||||||
WarnSkip(ctx, 0, "SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
|
WarnSkip(ctx, 1, "SQL SLOW", "sql", sql, "rows", rows, "dur(ms)", duration)
|
||||||
} else {
|
} else {
|
||||||
InfoSkip(ctx, 0, "SQL INFO", "sql", sql, "rows", rows, "dur(ms)", duration)
|
InfoSkip(ctx, 1, "SQL INFO", "sql", sql, "rows", rows, "dur(ms)", duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
@ -42,13 +41,8 @@ func (l *logger) Error(msg string, kv ...any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) log(lvl zapcore.Level, msg string, kv ...any) {
|
func (l *logger) log(lvl zapcore.Level, msg string, kv ...any) {
|
||||||
caller := resolveLoggerCaller(0)
|
fields := makeLogFields(l.ctx, kv...)
|
||||||
fields := makeLogFieldsWithCaller(l.ctx, caller, kv...)
|
|
||||||
ce := l._logger.Check(lvl, msg)
|
ce := l._logger.Check(lvl, msg)
|
||||||
if ce == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setCheckedEntryCaller(ce, caller)
|
|
||||||
ce.Write(fields...)
|
ce.Write(fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,10 +51,6 @@ func makeLogFields(ctx context.Context, kv ...any) []zap.Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLogFieldsSkip(ctx context.Context, extraSkip int, kv ...any) []zap.Field {
|
func makeLogFieldsSkip(ctx context.Context, extraSkip int, kv ...any) []zap.Field {
|
||||||
return makeLogFieldsWithCaller(ctx, resolveLoggerCaller(extraSkip), kv...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeLogFieldsWithCaller(ctx context.Context, caller loggerCaller, kv ...any) []zap.Field {
|
|
||||||
if len(kv)%2 != 0 {
|
if len(kv)%2 != 0 {
|
||||||
kv = append(kv, "unknown")
|
kv = append(kv, "unknown")
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +60,8 @@ func makeLogFieldsWithCaller(ctx context.Context, caller loggerCaller, kv ...any
|
||||||
spanID := spanCtx.SpanID().String()
|
spanID := spanCtx.SpanID().String()
|
||||||
kv = append(kv, "traceID", traceID, "spanID", spanID)
|
kv = append(kv, "traceID", traceID, "spanID", spanID)
|
||||||
|
|
||||||
kv = append(kv, "func", caller.funcName, "file", caller.shortFile, "line", caller.line)
|
funcName, file, line := getLoggerCallerInfoSkip(extraSkip)
|
||||||
|
kv = append(kv, "func", funcName, "file", file, "line", line)
|
||||||
fields := make([]zap.Field, 0, len(kv)/2)
|
fields := make([]zap.Field, 0, len(kv)/2)
|
||||||
for i := 0; i < len(kv); i += 2 {
|
for i := 0; i < len(kv); i += 2 {
|
||||||
key := kv[i].(string)
|
key := kv[i].(string)
|
||||||
|
|
@ -104,59 +95,13 @@ func getLoggerCallerInfo() (funcName, file string, line int) {
|
||||||
|
|
||||||
// getLoggerCallerInfoSkip returns caller info with additional skip frames beyond the standard depth.
|
// getLoggerCallerInfoSkip returns caller info with additional skip frames beyond the standard depth.
|
||||||
func getLoggerCallerInfoSkip(extraSkip int) (funcName, file string, line int) {
|
func getLoggerCallerInfoSkip(extraSkip int) (funcName, file string, line int) {
|
||||||
caller := resolveLoggerCaller(extraSkip)
|
pc, file, line, ok := runtime.Caller(4 + extraSkip)
|
||||||
return caller.funcName, caller.shortFile, caller.line
|
if !ok {
|
||||||
}
|
|
||||||
|
|
||||||
type loggerCaller struct {
|
|
||||||
pc uintptr
|
|
||||||
funcName string
|
|
||||||
fullFile string
|
|
||||||
shortFile string
|
|
||||||
line int
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveLoggerCaller(extraSkip int) loggerCaller {
|
|
||||||
pcs := make([]uintptr, 32)
|
|
||||||
count := runtime.Callers(2, pcs)
|
|
||||||
frames := runtime.CallersFrames(pcs[:count])
|
|
||||||
|
|
||||||
for {
|
|
||||||
frame, more := frames.Next()
|
|
||||||
if !isLoggerInfrastructureFrame(frame.Function) {
|
|
||||||
if extraSkip > 0 {
|
|
||||||
extraSkip--
|
|
||||||
} else {
|
|
||||||
return loggerCaller{
|
|
||||||
pc: frame.PC,
|
|
||||||
funcName: frame.Function,
|
|
||||||
fullFile: frame.File,
|
|
||||||
shortFile: path.Base(frame.File),
|
|
||||||
line: frame.Line,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !more {
|
|
||||||
return loggerCaller{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLoggerInfrastructureFrame(function string) bool {
|
|
||||||
return strings.HasPrefix(function, "modelRT/logger.") ||
|
|
||||||
strings.HasPrefix(function, "gorm.io/gorm")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCheckedEntryCaller(entry *zapcore.CheckedEntry, caller loggerCaller) {
|
|
||||||
if caller.pc == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry.Entry.Caller = zapcore.EntryCaller{
|
file = path.Base(file)
|
||||||
Defined: true,
|
funcName = runtime.FuncForPC(pc).Name()
|
||||||
PC: caller.pc,
|
return
|
||||||
File: caller.fullFile,
|
|
||||||
Line: caller.line,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted
|
// New returns a logger bound to ctx. Trace fields (traceID, spanID) are extracted
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
// Package model defines data models and domain rules for model runtime service.
|
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/constants"
|
|
||||||
)
|
|
||||||
|
|
||||||
var parameterAttributeGroups = map[string]struct{}{
|
|
||||||
"component": {},
|
|
||||||
"base_extend": {},
|
|
||||||
"rated": {},
|
|
||||||
"setup": {},
|
|
||||||
"model": {},
|
|
||||||
"stable": {},
|
|
||||||
"craft": {},
|
|
||||||
"integrity": {},
|
|
||||||
"behavior": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClassifyDataObjectToken determines whether token identifies a parameter or a
|
|
||||||
// measurement. Seven-part and four-part tokens are classified by token6, while
|
|
||||||
// two-part tokens are treated as measurements at the current stage.
|
|
||||||
func ClassifyDataObjectToken(token string) (constants.DataObjectType, error) {
|
|
||||||
parts := strings.Split(token, ".")
|
|
||||||
if slices.Contains(parts, "") {
|
|
||||||
return "", fmt.Errorf("invalid data object token %q: token segment cannot be empty", token)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(parts) {
|
|
||||||
case 2:
|
|
||||||
return constants.DataObjectTypeMeasurement, nil
|
|
||||||
case 4, 7:
|
|
||||||
token6Index := 2
|
|
||||||
if len(parts) == 7 {
|
|
||||||
token6Index = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
token6 := parts[token6Index]
|
|
||||||
if _, ok := parameterAttributeGroups[token6]; ok {
|
|
||||||
return constants.DataObjectTypeParameter, nil
|
|
||||||
}
|
|
||||||
if token6 == "bay" {
|
|
||||||
return constants.DataObjectTypeMeasurement, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("invalid data object token %q: unsupported token6 %q", token, token6)
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("invalid data object token %q: expected 2, 4, or 7 segments, got %d", token, len(parts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"modelRT/constants"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestClassifyDataObjectToken(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
token string
|
|
||||||
expected constants.DataObjectType
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "seven-part measurement",
|
|
||||||
token: "grid.zone.station.nspath.component.bay.measurement",
|
|
||||||
expected: constants.DataObjectTypeMeasurement,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "four-part measurement",
|
|
||||||
token: "nspath.component.bay.measurement",
|
|
||||||
expected: constants.DataObjectTypeMeasurement,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two-part measurement",
|
|
||||||
token: "nspath.measurement",
|
|
||||||
expected: constants.DataObjectTypeMeasurement,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "seven-part parameter",
|
|
||||||
token: "grid.zone.station.nspath.component.rated.voltage",
|
|
||||||
expected: constants.DataObjectTypeParameter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "four-part parameter",
|
|
||||||
token: "nspath.component.base_extend.description",
|
|
||||||
expected: constants.DataObjectTypeParameter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "component group",
|
|
||||||
token: "nspath.component.component.name",
|
|
||||||
expected: constants.DataObjectTypeParameter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown group",
|
|
||||||
token: "nspath.component.unknown.name",
|
|
||||||
wantErr: "unsupported token6",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid segment count",
|
|
||||||
token: "grid.zone.station",
|
|
||||||
wantErr: "expected 2, 4, or 7 segments",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty segment",
|
|
||||||
token: "nspath..bay.measurement",
|
|
||||||
wantErr: "token segment cannot be empty",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
actual, err := ClassifyDataObjectToken(tt.token)
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), tt.wantErr)
|
|
||||||
assert.Empty(t, actual)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClassifyDataObjectTokenParameterGroups(t *testing.T) {
|
|
||||||
groups := []string{
|
|
||||||
"component",
|
|
||||||
"base_extend",
|
|
||||||
"rated",
|
|
||||||
"setup",
|
|
||||||
"model",
|
|
||||||
"stable",
|
|
||||||
"craft",
|
|
||||||
"integrity",
|
|
||||||
"behavior",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, group := range groups {
|
|
||||||
t.Run(group, func(t *testing.T) {
|
|
||||||
tokens := []string{
|
|
||||||
"nspath.component." + group + ".attribute",
|
|
||||||
"grid.zone.station.nspath.component." + group + ".attribute",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, token := range tokens {
|
|
||||||
actual, err := ClassifyDataObjectToken(token)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, constants.DataObjectTypeParameter, actual)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"modelRT/constants"
|
|
||||||
"modelRT/orm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var allowedMeasurementTypes = map[string]struct{}{
|
|
||||||
"TM": {},
|
|
||||||
"TS": {},
|
|
||||||
"TC": {},
|
|
||||||
"TA": {},
|
|
||||||
"SP": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// MeasurementTypeFromDataSource returns the two-character measurement type
|
|
||||||
// encoded in a CL3611 channel. Only TM, TS, TC, TA, and SP are valid.
|
|
||||||
func MeasurementTypeFromDataSource(dataSource orm.JSONMap) (string, error) {
|
|
||||||
dataSourceType, err := integerJSONValue(dataSource["type"])
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid measurement data_source type: %w", err)
|
|
||||||
}
|
|
||||||
if dataSourceType != constants.DataSourceTypeCL3611 {
|
|
||||||
return "", fmt.Errorf("measurement type requires data_source type %d, got %d", constants.DataSourceTypeCL3611, dataSourceType)
|
|
||||||
}
|
|
||||||
|
|
||||||
ioAddress, ok := dataSource["io_address"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
if value, jsonMapOK := dataSource["io_address"].(orm.JSONMap); jsonMapOK {
|
|
||||||
ioAddress = map[string]any(value)
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf("measurement data_source io_address is not an object")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, ok := ioAddress["channel"].(string)
|
|
||||||
if !ok || len(channel) < 2 {
|
|
||||||
return "", fmt.Errorf("measurement data_source channel must contain at least two characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
measurementType := strings.ToUpper(channel[:2])
|
|
||||||
if _, ok := allowedMeasurementTypes[measurementType]; !ok {
|
|
||||||
return "", fmt.Errorf("unsupported measurement type %q", measurementType)
|
|
||||||
}
|
|
||||||
return measurementType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func integerJSONValue(value any) (int, error) {
|
|
||||||
switch typed := value.(type) {
|
|
||||||
case int:
|
|
||||||
return typed, nil
|
|
||||||
case int8:
|
|
||||||
return int(typed), nil
|
|
||||||
case int16:
|
|
||||||
return int(typed), nil
|
|
||||||
case int32:
|
|
||||||
return int(typed), nil
|
|
||||||
case int64:
|
|
||||||
return int(typed), nil
|
|
||||||
case float32:
|
|
||||||
converted := int(typed)
|
|
||||||
if typed != float32(converted) {
|
|
||||||
return 0, fmt.Errorf("expected integer, got %v", typed)
|
|
||||||
}
|
|
||||||
return converted, nil
|
|
||||||
case float64:
|
|
||||||
converted := int(typed)
|
|
||||||
if typed != float64(converted) {
|
|
||||||
return 0, fmt.Errorf("expected integer, got %v", typed)
|
|
||||||
}
|
|
||||||
return converted, nil
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("expected integer, got %T", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"modelRT/orm"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMeasurementTypeFromDataSource(t *testing.T) {
|
|
||||||
for _, measurementType := range []string{"TM", "TS", "TC", "TA", "SP"} {
|
|
||||||
t.Run(measurementType, func(t *testing.T) {
|
|
||||||
actual, err := MeasurementTypeFromDataSource(orm.JSONMap{
|
|
||||||
"type": float64(1),
|
|
||||||
"io_address": map[string]any{
|
|
||||||
"channel": strings.ToLower(measurementType) + "1_test",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, measurementType, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeasurementTypeFromDataSourceRejectsInvalidValues(t *testing.T) {
|
|
||||||
tests := []orm.JSONMap{
|
|
||||||
{"type": float64(2), "io_address": map[string]any{"channel": "tm1"}},
|
|
||||||
{"type": float64(1), "io_address": map[string]any{"channel": "xx1"}},
|
|
||||||
{"type": float64(1), "io_address": map[string]any{"channel": "t"}},
|
|
||||||
{"type": "1", "io_address": map[string]any{"channel": "tm1"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dataSource := range tests {
|
|
||||||
_, err := MeasurementTypeFromDataSource(dataSource)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGenerateMeasureIdentifierSupportsJSONNumbers(t *testing.T) {
|
|
||||||
identifier, err := GenerateMeasureIdentifier(map[string]any{
|
|
||||||
"type": float64(2),
|
|
||||||
"io_address": map[string]any{
|
|
||||||
"station": "Station000",
|
|
||||||
"packet": float64(10),
|
|
||||||
"offset": float64(35),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "station000:104:10:35", identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateMeasureIdentifierRejectsFractionalJSONNumbers(t *testing.T) {
|
|
||||||
_, err := GenerateMeasureIdentifier(map[string]any{
|
|
||||||
"type": float64(2),
|
|
||||||
"io_address": map[string]any{
|
|
||||||
"station": "station000",
|
|
||||||
"packet": float64(10.5),
|
|
||||||
"offset": float64(35),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
@ -245,18 +245,24 @@ func GenerateMeasureIdentifier(source map[string]any) (string, error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("Power104: missing packet field")
|
return "", fmt.Errorf("Power104: missing packet field")
|
||||||
}
|
}
|
||||||
packet, err := integerJSONValue(packetVal)
|
var packet int
|
||||||
if err != nil {
|
switch v := packetVal.(type) {
|
||||||
return "", fmt.Errorf("Power104:invalid packet format: %w", err)
|
case int:
|
||||||
|
packet = v
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("Power104:invalid packet format")
|
||||||
}
|
}
|
||||||
|
|
||||||
offsetVal, ok := ioAddress["offset"]
|
offsetVal, ok := ioAddress["offset"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("Power104:missing offset field")
|
return "", fmt.Errorf("Power104:missing offset field")
|
||||||
}
|
}
|
||||||
offset, err := integerJSONValue(offsetVal)
|
var offset int
|
||||||
if err != nil {
|
switch v := offsetVal.(type) {
|
||||||
return "", fmt.Errorf("Power104:invalid offset format: %w", err)
|
case int:
|
||||||
|
offset = v
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("Power104:invalid offset format")
|
||||||
}
|
}
|
||||||
return concatP104WithPlus(station, packet, offset), nil
|
return concatP104WithPlus(station, packet, offset), nil
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ func (a *AsyncTask) IsFailed() bool {
|
||||||
return a.Status == AsyncTaskStatusFailed
|
return a.Status == AsyncTaskStatusFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidAsyncTaskType checks if the task type is valid
|
// IsValidTaskType checks if the task type is valid
|
||||||
func IsValidAsyncTaskType(taskType string) bool {
|
func IsValidAsyncTaskType(taskType string) bool {
|
||||||
switch AsyncTaskType(taskType) {
|
switch AsyncTaskType(taskType) {
|
||||||
case AsyncTaskTypeTopologyAnalysis, AsyncTaskTypePerformanceAnalysis,
|
case AsyncTaskTypeTopologyAnalysis, AsyncTaskTypePerformanceAnalysis,
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,10 @@ func NewBusbarSection(name string) (*BusbarSection, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusbarSection) BusNameLenCheck() bool {
|
func (b *BusbarSection) BusNameLenCheck() bool {
|
||||||
return len([]rune(b.BusbarName)) <= 20
|
if len([]rune(b.BusbarName)) > 20 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusbarSection) BusVoltageCheck() bool {
|
func (b *BusbarSection) BusVoltageCheck() bool {
|
||||||
|
|
@ -102,5 +105,8 @@ func (b *BusbarSection) BusVoltageCheck() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusbarSection) BusDescLenCheck() bool {
|
func (b *BusbarSection) BusDescLenCheck() bool {
|
||||||
return len([]rune(b.BusbarDesc)) <= 100
|
if len([]rune(b.BusbarDesc)) > 100 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ type Measurement struct {
|
||||||
Name string `gorm:"column:name;size:64;not null;default:''"`
|
Name string `gorm:"column:name;size:64;not null;default:''"`
|
||||||
Type int16 `gorm:"column:type;not null;default:-1"`
|
Type int16 `gorm:"column:type;not null;default:-1"`
|
||||||
Size int `gorm:"column:size;not null;default:-1"`
|
Size int `gorm:"column:size;not null;default:-1"`
|
||||||
Mode int16 `gorm:"column:mode;not null;default:1"`
|
|
||||||
DataSource JSONMap `gorm:"column:data_source;type:jsonb;not null;default:'{}'"`
|
DataSource JSONMap `gorm:"column:data_source;type:jsonb;not null;default:'{}'"`
|
||||||
EventPlan JSONMap `gorm:"column:event_plan;type:jsonb;not null;default:'{}'"`
|
EventPlan JSONMap `gorm:"column:event_plan;type:jsonb;not null;default:'{}'"`
|
||||||
Binding JSONMap `gorm:"column:binding;type:jsonb;not null;default:'{\"ct\":{\"ratio\":1.0,\"polarity\":1,\"index\":0},\"pt\":{\"ratio\":1.0,\"polarity\":1,\"index\":0}}'"`
|
Binding JSONMap `gorm:"column:binding;type:jsonb;not null;default:'{\"ct\":{\"ratio\":1.0,\"polarity\":1,\"index\":0},\"pt\":{\"ratio\":1.0,\"polarity\":1,\"index\":0}}'"`
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
// Package router provides router config
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"modelRT/handler"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// registerDataObjectRoutes define func of register data object routes
|
|
||||||
func registerDataObjectRoutes(rg *gin.RouterGroup, middlewares ...gin.HandlerFunc) {
|
|
||||||
g := rg.Group("/data-object/")
|
|
||||||
g.Use(middlewares...)
|
|
||||||
g.GET("attribute/:token/:field", handler.DataObjectAttributeQueryHandler)
|
|
||||||
g.POST("update", handler.DataObjectAttributeUpdateHandler)
|
|
||||||
}
|
|
||||||
|
|
@ -27,6 +27,5 @@ func RegisterRoutes(engine *gin.Engine, clientToken string) {
|
||||||
registerDataRoutes(routeGroup)
|
registerDataRoutes(routeGroup)
|
||||||
registerMonitorRoutes(routeGroup)
|
registerMonitorRoutes(routeGroup)
|
||||||
registerComponentRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
registerComponentRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
||||||
registerDataObjectRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
|
||||||
registerAsyncTaskRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
registerAsyncTaskRoutes(routeGroup, middleware.SetTokenMiddleware(clientToken))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
// Package sql defines reusable database SQL statements.
|
|
||||||
package sql
|
|
||||||
|
|
||||||
const (
|
|
||||||
// MeasurementCountSelect selects the number of measurements matching a
|
|
||||||
// token hierarchy and is used during token existence validation.
|
|
||||||
MeasurementCountSelect = "SELECT COUNT(*)"
|
|
||||||
|
|
||||||
// MeasurementRowsSelect selects complete measurement rows after a token has
|
|
||||||
// been parsed into its hierarchy conditions.
|
|
||||||
MeasurementRowsSelect = "SELECT m.*"
|
|
||||||
|
|
||||||
// MeasurementLimitTwo limits token resolution to two rows so callers can
|
|
||||||
// distinguish a unique match from an ambiguous token without loading all matches.
|
|
||||||
MeasurementLimitTwo = "LIMIT 2"
|
|
||||||
|
|
||||||
// MeasurementIDWhere is the GORM condition used to query a measurement by
|
|
||||||
// its database primary key.
|
|
||||||
MeasurementIDWhere = "id = ?"
|
|
||||||
|
|
||||||
// MeasurementTokenValidationQueryBase contains the common hierarchy joins
|
|
||||||
// used by all supported measurement token formats.
|
|
||||||
MeasurementTokenValidationQueryBase = MeasurementCountSelect + `
|
|
||||||
FROM measurement AS m
|
|
||||||
INNER JOIN component AS c ON c.global_uuid = m.component_uuid
|
|
||||||
INNER JOIN bay AS b ON b.bay_uuid = m.bay_uuid
|
|
||||||
INNER JOIN station AS s ON s.id = c.station_id
|
|
||||||
INNER JOIN zone AS z ON z.id = s.zone_id
|
|
||||||
INNER JOIN grid AS g ON g.id = z.grid_id`
|
|
||||||
|
|
||||||
// MeasurementSevenPartTokenWhere matches a complete token in the form
|
|
||||||
// token1.token2.token3.token4.token5.token6.token7. Token6 is validated as
|
|
||||||
// "bay" before this condition is used.
|
|
||||||
MeasurementSevenPartTokenWhere = `WHERE g.tagname = ?
|
|
||||||
AND z.tagname = ? AND s.tagname = ?
|
|
||||||
AND c.nspath = ? AND c.tag = ?
|
|
||||||
AND m.tag = ?`
|
|
||||||
|
|
||||||
// MeasurementFourPartTokenWhere matches a local measurement token in the
|
|
||||||
// form token4.token5.token6.token7. Token6 is validated as "bay" before use.
|
|
||||||
MeasurementFourPartTokenWhere = `WHERE c.nspath = ? AND c.tag = ?
|
|
||||||
AND m.tag = ?`
|
|
||||||
|
|
||||||
// MeasurementTwoPartTokenWhere matches the short measurement token format
|
|
||||||
// token4.token7 using component namespace path and measurement tag.
|
|
||||||
MeasurementTwoPartTokenWhere = `WHERE c.nspath = ? AND m.tag = ?`
|
|
||||||
|
|
||||||
// MeasurementComponentByUUID returns the component hierarchy fields needed
|
|
||||||
// to construct a measurement's canonical name and seven-part ID.
|
|
||||||
MeasurementComponentByUUID = `SELECT global_uuid, nspath,
|
|
||||||
tag, grid, zone, station FROM component
|
|
||||||
WHERE global_uuid = ? LIMIT 1`
|
|
||||||
|
|
||||||
// MeasurementGridTags returns every grid tag used to construct the first
|
|
||||||
// level of the measurement recommendation hierarchy.
|
|
||||||
MeasurementGridTags = `SELECT tagname FROM grid`
|
|
||||||
|
|
||||||
// MeasurementZoneHierarchy returns zones together with their parent grid
|
|
||||||
// tags for building the grid-to-zone recommendation mapping.
|
|
||||||
MeasurementZoneHierarchy = `SELECT zone.*,
|
|
||||||
grid.tagname AS grid_tag FROM zone
|
|
||||||
LEFT JOIN grid ON zone.grid_id = grid.id`
|
|
||||||
|
|
||||||
// MeasurementStationHierarchy returns stations together with their parent
|
|
||||||
// zone tags for building the zone-to-station recommendation mapping.
|
|
||||||
MeasurementStationHierarchy = `SELECT station.*, zone.tagname AS zone_tag
|
|
||||||
FROM station
|
|
||||||
LEFT JOIN zone ON station.zone_id = zone.id`
|
|
||||||
|
|
||||||
// MeasurementComponentHierarchy returns components together with their
|
|
||||||
// parent station tags for building station, namespace, and component mappings.
|
|
||||||
MeasurementComponentHierarchy = `SELECT component.*,
|
|
||||||
station.tagname AS station_tag
|
|
||||||
FROM component LEFT JOIN station
|
|
||||||
ON component.station_id = station.id`
|
|
||||||
|
|
||||||
// MeasurementTagHierarchy returns measurements together with their owning
|
|
||||||
// component tags for building the component-to-measurement mapping.
|
|
||||||
MeasurementTagHierarchy = `
|
|
||||||
SELECT measurement.*,
|
|
||||||
component.tag AS comp_tag,
|
|
||||||
component.nspath AS comp_nspath,
|
|
||||||
bay.tag AS bay_tag
|
|
||||||
FROM measurement
|
|
||||||
LEFT JOIN component
|
|
||||||
ON measurement.component_uuid = component.global_uuid
|
|
||||||
LEFT JOIN bay
|
|
||||||
ON measurement.bay_uuid = bay.bay_uuid`
|
|
||||||
)
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
// Package sql defines reusable database SQL statements.
|
|
||||||
package sql
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ParameterComponentQueryBase selects the component owning a parameter and
|
|
||||||
// joins its complete hierarchy so seven-part tokens can be validated as one path.
|
|
||||||
ParameterComponentQueryBase = `SELECT c.*
|
|
||||||
FROM component AS c
|
|
||||||
INNER JOIN station AS s ON s.id = c.station_id
|
|
||||||
INNER JOIN zone AS z ON z.id = s.zone_id
|
|
||||||
INNER JOIN grid AS g ON g.id = z.grid_id`
|
|
||||||
|
|
||||||
// ParameterSevenPartTokenWhere matches the complete parameter token prefix
|
|
||||||
// token1.token2.token3.token4.token5.
|
|
||||||
ParameterSevenPartTokenWhere = `WHERE g.tagname = ?
|
|
||||||
AND z.tagname = ?
|
|
||||||
AND s.tagname = ?
|
|
||||||
AND c.nspath = ?
|
|
||||||
AND c.tag = ?`
|
|
||||||
|
|
||||||
// ParameterFourPartTokenWhere matches token4.token5 and restricts the short
|
|
||||||
// token form to components belonging to a local station.
|
|
||||||
ParameterFourPartTokenWhere = `WHERE c.nspath = ?
|
|
||||||
AND c.tag = ?
|
|
||||||
AND s.is_local = TRUE`
|
|
||||||
|
|
||||||
// ParameterLimitTwo allows callers to distinguish a unique component from
|
|
||||||
// an ambiguous token without loading every matching row.
|
|
||||||
ParameterLimitTwo = `LIMIT 2`
|
|
||||||
|
|
||||||
// ParameterAttributeColumnType checks that token7 is an actual column of
|
|
||||||
// the dynamic parameter table and returns its PostgreSQL display type.
|
|
||||||
ParameterAttributeColumnType = `SELECT pg_catalog.format_type(a.atttypid, a.atttypmod)
|
|
||||||
FROM pg_catalog.pg_attribute AS a
|
|
||||||
INNER JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
|
|
||||||
INNER JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
|
|
||||||
WHERE n.nspname = 'public'
|
|
||||||
AND c.relname = ?
|
|
||||||
AND a.attname = ?
|
|
||||||
AND a.attnum > 0
|
|
||||||
AND NOT a.attisdropped
|
|
||||||
LIMIT 1`
|
|
||||||
|
|
||||||
// ParameterAttributeDescription returns the display name of token7 from the
|
|
||||||
// basic attribute metadata table. Two rows are enough to detect ambiguity.
|
|
||||||
ParameterAttributeDescription = `SELECT attribute_name
|
|
||||||
FROM basic.attribute
|
|
||||||
WHERE attribute = ?
|
|
||||||
LIMIT 2`
|
|
||||||
)
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
package sql
|
package sql
|
||||||
|
|
||||||
// RecursiveSQL define topologic table recursive query statement
|
// RecursiveSQL define topologic table recursive query statement
|
||||||
const RecursiveSQL = `WITH RECURSIVE recursive_tree as (
|
var RecursiveSQL = `WITH RECURSIVE recursive_tree as (
|
||||||
SELECT uuid_from,uuid_to,flag
|
SELECT uuid_from,uuid_to,flag
|
||||||
FROM "topologic"
|
FROM "topologic"
|
||||||
WHERE uuid_from = ?
|
WHERE uuid_from = ?
|
||||||
|
|
@ -16,7 +16,7 @@ const RecursiveSQL = `WITH RECURSIVE recursive_tree as (
|
||||||
// RecursiveTopologicByStartSQL returns every directed edge reachable from the
|
// RecursiveTopologicByStartSQL returns every directed edge reachable from the
|
||||||
// supplied start component. It tracks the visited node path inside PostgreSQL
|
// supplied start component. It tracks the visited node path inside PostgreSQL
|
||||||
// so cycles in topologic data cannot recurse forever.
|
// so cycles in topologic data cannot recurse forever.
|
||||||
const RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
|
var RecursiveTopologicByStartSQL = `WITH RECURSIVE recursive_tree as (
|
||||||
SELECT uuid_from, uuid_to, flag, ARRAY[uuid_from, uuid_to] AS path
|
SELECT uuid_from, uuid_to, flag, ARRAY[uuid_from, uuid_to] AS path
|
||||||
FROM "topologic"
|
FROM "topologic"
|
||||||
WHERE uuid_from = ?
|
WHERE uuid_from = ?
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue