Skip to content

Commit a913fbb

Browse files
akoclaude
andcommitted
feat: support primitive page parameters (String, Integer, etc.)
Page parameters previously only supported entity types. Now primitive types (String, Integer, Long, Decimal, Boolean, DateTime) are parsed, serialized, and round-tripped correctly. Changes across the full stack: - AST: PageParameter now carries DataType alongside EntityType - Visitor: uses buildDataType() for full type parsing - Executor: maps primitive DataType to BSON type, skips entity resolution - Writer: emits DataTypes$StringType etc. instead of hardcoded ObjectType - Parser: reads $Type from ParameterType to detect primitives - DESCRIBE: outputs "Integer", "String" etc. for primitive params Closes #139 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 75f5ba2 commit a913fbb

File tree

7 files changed

+104
-34
lines changed

7 files changed

+104
-34
lines changed

mdl/ast/ast_page.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ package ast
66
// Page Statements
77
// ============================================================================
88

9-
// PageParameter represents a page parameter: $Name: EntityType
9+
// PageParameter represents a page parameter: $Name: Type
10+
// Type can be an entity (Module.Entity) or a primitive (String, Integer, etc.).
1011
type PageParameter struct {
1112
Name string // Parameter name (without $ prefix)
12-
EntityType QualifiedName // Entity type
13+
EntityType QualifiedName // Entity type (for backward compatibility; empty for primitives)
14+
Type DataType // Full data type (primitives + entities)
1315
}
1416

1517
// PageVariable represents a page variable: $Name: DataType = 'defaultExpression'

mdl/executor/cmd_pages_builder_v3.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,12 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error)
8686
IsRequired: true, // Page parameters are required by default
8787
}
8888

89-
// Resolve entity type
90-
if param.EntityType.Name != "" {
89+
// Check if this is a primitive type or entity type
90+
if bsonType := pageParamBSONType(param.Type); bsonType != "" {
91+
// Primitive type parameter
92+
pageParam.TypeName = bsonType
93+
} else if param.EntityType.Name != "" {
94+
// Entity type parameter
9195
entityID, err := pb.resolveEntity(param.EntityType)
9296
if err != nil {
9397
return nil, fmt.Errorf("failed to resolve entity %s: %w", param.EntityType.String(), err)
@@ -968,6 +972,27 @@ func (pb *pageBuilder) getEntityNameByID(entityID model.ID) (string, error) {
968972
return "", fmt.Errorf("entity not found by ID: %s", entityID)
969973
}
970974

975+
// pageParamBSONType maps a DataType to the BSON $Type string for primitive page parameters.
976+
// Returns empty string for entity/enum types (which use DataTypes$ObjectType instead).
977+
func pageParamBSONType(dt ast.DataType) string {
978+
switch dt.Kind {
979+
case ast.TypeString:
980+
return "DataTypes$StringType"
981+
case ast.TypeInteger:
982+
return "DataTypes$IntegerType"
983+
case ast.TypeLong:
984+
return "DataTypes$LongType"
985+
case ast.TypeDecimal:
986+
return "DataTypes$DecimalType"
987+
case ast.TypeBoolean:
988+
return "DataTypes$BooleanType"
989+
case ast.TypeDateTime:
990+
return "DataTypes$DateTimeType"
991+
default:
992+
return ""
993+
}
994+
}
995+
971996
// resolveNanoflowByName resolves a nanoflow qualified name to its ID.
972997
func (pb *pageBuilder) resolveNanoflowByName(nfName string) (model.ID, error) {
973998
parts := strings.Split(nfName, ".")

mdl/executor/cmd_pages_describe.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,8 @@ func (e *Executor) describePage(name ast.QualifiedName) error {
103103
if len(foundPage.Parameters) > 0 {
104104
params := []string{}
105105
for _, p := range foundPage.Parameters {
106-
entityName := p.EntityName
107-
if entityName == "" {
108-
entityName = string(p.EntityID)
109-
}
110-
params = append(params, fmt.Sprintf("$%s: %s", p.Name, entityName))
106+
typeName := pageParamTypeMDL(p)
107+
params = append(params, fmt.Sprintf("$%s: %s", p.Name, typeName))
111108
}
112109
props = append(props, fmt.Sprintf("Params: { %s }", strings.Join(params, ", ")))
113110
}
@@ -721,3 +718,30 @@ func wrapStringLiteralExpression(value string) string {
721718
// Otherwise wrap in single quotes as a string literal
722719
return "'" + value + "'"
723720
}
721+
722+
// pageParamTypeMDL returns the MDL type string for a page parameter.
723+
// Primitive params return "String", "Integer", etc.; entity params return the qualified name.
724+
func pageParamTypeMDL(p *pages.PageParameter) string {
725+
if p.TypeName != "" {
726+
switch p.TypeName {
727+
case "DataTypes$StringType":
728+
return "String"
729+
case "DataTypes$IntegerType":
730+
return "Integer"
731+
case "DataTypes$LongType":
732+
return "Long"
733+
case "DataTypes$DecimalType":
734+
return "Decimal"
735+
case "DataTypes$BooleanType":
736+
return "Boolean"
737+
case "DataTypes$DateTimeType":
738+
return "DateTime"
739+
default:
740+
return p.TypeName
741+
}
742+
}
743+
if p.EntityName != "" {
744+
return p.EntityName
745+
}
746+
return string(p.EntityID)
747+
}

mdl/visitor/visitor_page.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ func buildPageParameters(ctx parser.IPageParameterListContext) []ast.PageParamet
4343
name = strings.TrimPrefix(v.GetText(), "$")
4444
}
4545
var entityType ast.QualifiedName
46+
var dataType ast.DataType
4647
if dt := paramCtx.DataType(); dt != nil {
48+
dataType = buildDataType(dt)
49+
// For backward compatibility, also populate EntityType for entity/enum refs
4750
dtCtx := dt.(*parser.DataTypeContext)
4851
if qn := dtCtx.QualifiedName(); qn != nil {
4952
entityType = buildQualifiedName(qn)
@@ -52,6 +55,7 @@ func buildPageParameters(ctx parser.IPageParameterListContext) []ast.PageParamet
5255
params = append(params, ast.PageParameter{
5356
Name: name,
5457
EntityType: entityType,
58+
Type: dataType,
5559
})
5660
}
5761
return params

sdk/mpr/parser_page.go

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -162,38 +162,41 @@ func parsePageParameter(raw map[string]any) *pages.PageParameter {
162162
param.EntityID = model.ID(entityID)
163163
}
164164

165-
// Parse ParameterType to get entity name
165+
// Parse ParameterType to get entity name and/or primitive type
166166
// ParameterType can be a map/bson.D (single object) or array (with version marker)
167-
if paramType, ok := raw["ParameterType"].(bson.D); ok {
168-
// Direct bson.D format
169-
for _, elem := range paramType {
170-
if elem.Key == "Entity" {
167+
parseParamTypeDoc := func(doc bson.D) {
168+
for _, elem := range doc {
169+
switch elem.Key {
170+
case "$Type":
171+
if typeName, ok := elem.Value.(string); ok && typeName != "DataTypes$ObjectType" {
172+
param.TypeName = typeName
173+
}
174+
case "Entity":
171175
if entity, ok := elem.Value.(string); ok {
172176
param.EntityName = entity
173177
}
174178
}
175179
}
176-
} else if paramType, ok := raw["ParameterType"].(map[string]any); ok {
177-
// Direct map format: {"$Type": "DataTypes$ObjectType", "Entity": "..."}
178-
if entity, ok := paramType["Entity"].(string); ok {
180+
}
181+
parseParamTypeMap := func(m map[string]any) {
182+
if typeName, ok := m["$Type"].(string); ok && typeName != "DataTypes$ObjectType" {
183+
param.TypeName = typeName
184+
}
185+
if entity, ok := m["Entity"].(string); ok {
179186
param.EntityName = entity
180187
}
188+
}
189+
190+
if paramType, ok := raw["ParameterType"].(bson.D); ok {
191+
parseParamTypeDoc(paramType)
192+
} else if paramType, ok := raw["ParameterType"].(map[string]any); ok {
193+
parseParamTypeMap(paramType)
181194
} else if paramTypeArr, ok := raw["ParameterType"].(bson.A); ok {
182-
// Array format: [{...}] or [version, {...}]
183195
for _, item := range paramTypeArr {
184196
if typeDoc, ok := item.(bson.D); ok {
185-
for _, elem := range typeDoc {
186-
if elem.Key == "Entity" {
187-
if entity, ok := elem.Value.(string); ok {
188-
param.EntityName = entity
189-
}
190-
}
191-
}
197+
parseParamTypeDoc(typeDoc)
192198
} else if typeMap, ok := item.(map[string]any); ok {
193-
if entity, ok := typeMap["Entity"].(string); ok {
194-
param.EntityName = entity
195-
break
196-
}
199+
parseParamTypeMap(typeMap)
197200
}
198201
}
199202
}

sdk/mpr/writer_pages.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,23 @@ func (w *Writer) serializePage(page *pages.Page) ([]byte, error) {
188188
paramID = generateUUID()
189189
}
190190

191-
// Build ParameterType with DataTypes$ObjectType (as bson.D, not wrapped in bson.A)
191+
// Build ParameterType — entity params use DataTypes$ObjectType,
192+
// primitive params use DataTypes$StringType, DataTypes$IntegerType, etc.
192193
paramTypeID := generateUUID()
193-
paramType := bson.D{
194-
{Key: "$ID", Value: idToBsonBinary(paramTypeID)},
195-
{Key: "$Type", Value: "DataTypes$ObjectType"},
196-
{Key: "Entity", Value: p.EntityName},
194+
var paramType bson.D
195+
if p.TypeName != "" {
196+
// Primitive type parameter
197+
paramType = bson.D{
198+
{Key: "$ID", Value: idToBsonBinary(paramTypeID)},
199+
{Key: "$Type", Value: p.TypeName},
200+
}
201+
} else {
202+
// Entity type parameter (default)
203+
paramType = bson.D{
204+
{Key: "$ID", Value: idToBsonBinary(paramTypeID)},
205+
{Key: "$Type", Value: "DataTypes$ObjectType"},
206+
{Key: "Entity", Value: p.EntityName},
207+
}
197208
}
198209

199210
paramDoc := bson.D{

sdk/pages/pages_parameters.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type PageParameter struct {
2828
Name string `json:"name"`
2929
EntityID model.ID `json:"entityId,omitempty"`
3030
EntityName string `json:"entityName,omitempty"` // Qualified entity name (e.g., "Module.Entity")
31+
TypeName string `json:"typeName,omitempty"` // BSON $Type for ParameterType (e.g., "DataTypes$StringType")
3132
DefaultValue string `json:"defaultValue,omitempty"` // Default value expression
3233
IsRequired bool `json:"isRequired"` // Whether the parameter is required
3334
}

0 commit comments

Comments
 (0)