feat: add parameter data-object attribute APIs
- classify component and attribute groups as parameter tokens - validate four-part and seven-part parameter identifiers - resolve component and project-backed parameter values and database types - query parameter descriptions from basic attribute metadata - enforce data-object field support and build PARAM attribute responses - organize data-object SQL and add parameter query coverage
This commit is contained in:
parent
07ef921b75
commit
c6a8ce7991
|
|
@ -4,6 +4,14 @@ package common
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
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 define error of unsupport measurement field
|
||||||
ErrUnsupportedMeasurementField = errors.New("unsupported measurement field")
|
ErrUnsupportedMeasurementField = errors.New("unsupported measurement field")
|
||||||
// ErrInvalidMeasurementToken indicates that a token cannot represent a measurement.
|
// ErrInvalidMeasurementToken indicates that a token cannot represent a measurement.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
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), " ")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
@ -38,9 +38,6 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO 根据传入的token查询是measurement还是attribute,分别查询不同的表
|
|
||||||
// 量测支持token1.token2.token3.token4.token5.token6.token7、token4.token5.token6.token7、token4.token7
|
|
||||||
// 参量支持两种形式token4.token5.token6.token7与token1.token2.token3.token4.token5.token6.token7
|
|
||||||
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
dataObjectType, err := model.ClassifyDataObjectToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
|
logger.Error(ctx, "classify data object token failed", "error", err, "url", c.Request.RequestURI)
|
||||||
|
|
@ -48,12 +45,35 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
return
|
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 measurement *orm.Measurement
|
||||||
var measurementComponent *orm.Component
|
var measurementComponent *orm.Component
|
||||||
switch dataObjectType {
|
switch dataObjectType {
|
||||||
case constants.DataObjectTypeParameter:
|
case constants.DataObjectTypeParameter:
|
||||||
// TODO 验证参量token的合法性
|
// 参量支持两种形式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:
|
case constants.DataObjectTypeMeasurement:
|
||||||
|
// 量测支持token1.token2.token3.token4.token5.token6.token7、token4.token5.token6.token7、token4.token7
|
||||||
measurement, measurementComponent, err = database.QueryMeasurementByDataObjectToken(ctx, pgClient, token)
|
measurement, measurementComponent, err = database.QueryMeasurementByDataObjectToken(ctx, pgClient, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, common.ErrInvalidMeasurementToken) ||
|
if errors.Is(err, common.ErrInvalidMeasurementToken) ||
|
||||||
|
|
@ -72,11 +92,40 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
|
|
||||||
switch dataObjectType {
|
switch dataObjectType {
|
||||||
case constants.DataObjectTypeParameter:
|
case constants.DataObjectTypeParameter:
|
||||||
// TODO 在根据传递的field来组装具体的查询结果
|
value, err := buildParameterAttributeValue(
|
||||||
// 目前先直接返回错误,后续再实现具体的查询逻辑
|
ctx,
|
||||||
renderRespFailure(c, constants.RespCodeServerError, "query parameter attribute is not implemented yet", nil)
|
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:
|
case constants.DataObjectTypeMeasurement:
|
||||||
normalizedField := strings.ToLower(field)
|
|
||||||
value, err := buildMeasurementAttributeValue(
|
value, err := buildMeasurementAttributeValue(
|
||||||
ctx,
|
ctx,
|
||||||
normalizedField,
|
normalizedField,
|
||||||
|
|
@ -113,6 +162,33 @@ func DataObjectAttributeQueryHandler(c *gin.Context) {
|
||||||
|
|
||||||
type measurementValueLoader func(context.Context, orm.JSONMap) (any, error)
|
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 {
|
type dataObjectAttributeQueryResult struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Field string `json:"field"`
|
Field string `json:"field"`
|
||||||
|
|
@ -121,6 +197,73 @@ type dataObjectAttributeQueryResult struct {
|
||||||
Value any `json:"value"`
|
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(
|
func buildMeasurementAttributeValue(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
field string,
|
field string,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,112 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"modelRT/common"
|
"modelRT/common"
|
||||||
|
"modelRT/constants"
|
||||||
|
"modelRT/database"
|
||||||
"modelRT/orm"
|
"modelRT/orm"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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 mode", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "mode"},
|
||||||
|
{name: "parameter rejects name", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "name", 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) {
|
func TestBuildMeasurementAttributeValue(t *testing.T) {
|
||||||
dataSource := orm.JSONMap{
|
dataSource := orm.JSONMap{
|
||||||
"type": float64(1),
|
"type": float64(1),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var parameterAttributeGroups = map[string]struct{}{
|
var parameterAttributeGroups = map[string]struct{}{
|
||||||
|
"component": {},
|
||||||
"base_extend": {},
|
"base_extend": {},
|
||||||
"rated": {},
|
"rated": {},
|
||||||
"setup": {},
|
"setup": {},
|
||||||
|
|
@ -45,11 +46,6 @@ func ClassifyDataObjectToken(token string) (constants.DataObjectType, error) {
|
||||||
if token6 == "bay" {
|
if token6 == "bay" {
|
||||||
return constants.DataObjectTypeMeasurement, nil
|
return constants.DataObjectTypeMeasurement, nil
|
||||||
}
|
}
|
||||||
// TODO 后续要完成对component这个token6类型的支持
|
|
||||||
if token6 == "component" {
|
|
||||||
return "", fmt.Errorf("unsupported data object token %q: token6 component is not queryable", token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("invalid data object token %q: unsupported token6 %q", token, token6)
|
return "", fmt.Errorf("invalid data object token %q: unsupported token6 %q", token, token6)
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("invalid data object token %q: expected 2, 4, or 7 segments, got %d", token, len(parts))
|
return "", fmt.Errorf("invalid data object token %q: expected 2, 4, or 7 segments, got %d", token, len(parts))
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func TestClassifyDataObjectToken(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "component group",
|
name: "component group",
|
||||||
token: "nspath.component.component.name",
|
token: "nspath.component.component.name",
|
||||||
wantErr: "token6 component is not queryable",
|
expected: constants.DataObjectTypeParameter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown group",
|
name: "unknown group",
|
||||||
|
|
@ -81,6 +81,7 @@ func TestClassifyDataObjectToken(t *testing.T) {
|
||||||
|
|
||||||
func TestClassifyDataObjectTokenParameterGroups(t *testing.T) {
|
func TestClassifyDataObjectTokenParameterGroups(t *testing.T) {
|
||||||
groups := []string{
|
groups := []string{
|
||||||
|
"component",
|
||||||
"base_extend",
|
"base_extend",
|
||||||
"rated",
|
"rated",
|
||||||
"setup",
|
"setup",
|
||||||
|
|
@ -93,9 +94,16 @@ func TestClassifyDataObjectTokenParameterGroups(t *testing.T) {
|
||||||
|
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
t.Run(group, func(t *testing.T) {
|
t.Run(group, func(t *testing.T) {
|
||||||
actual, err := ClassifyDataObjectToken("nspath.component." + group + ".attribute")
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, constants.DataObjectTypeParameter, actual)
|
assert.Equal(t, constants.DataObjectTypeParameter, actual)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
// 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`
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue