Skip to content

Commit 88e729d

Browse files
dmealingclaude
andcommitted
fix(python): close attr-schema coverage gap — typed value_type for cross-port attrs
Python's field schema previously only registered @column, @storage, and @objectref with a typed value_type, so the YAML desugar's D2 type-coercion guard (ADR-0006) never fired for the other common field attrs. TS / C# / Java already enforce these via their typed schemas — Python was the outlier, silently accepting `maxLength: TRUE` etc. Adds typed AttrSchema for every common field attr Python was missing: @required boolean @unique boolean @default None (polymorphic — value-type follows owning field's subtype; guard skips when value_type is None) @maxlength int @precision int @scale int @filterable boolean @Sortable boolean @sortableDefaultOrder string (allowed: asc, desc) @autoset string (allowed: onCreate, onUpdate) Mirrors server/typescript/.../core/field/field-schema.ts `commonFieldAttrs` and server/csharp/.../Core/Field/FieldSchema.cs `CommonFieldAttrs`. The new constants live in field_constants.py alongside their TS/C# peers (cross- port parity). _FIELD_COMMON_ATTRS is now shared between field.* and field.enum (was a separate hand-typed copy). Also: AttrSchema.value_type is now Optional[str] to permit declaring a "known but untyped" attr (e.g. @default), matching TS's `valueType?: string` shape. The coercion guard already short-circuits when value_type is None. Re-adds fixture error-yaml-coerced-bool-in-int that the YAML corpus expansion (b2dc546) had to drop because Python's gap meant only 3/4 ports fired. Now all four ports (TS, C#, Java, Python) emit ERR_YAML_COERCION + ERR_BAD_ATTR_VALUE for `maxLength: TRUE`. Verification: Python 506 passed (was 505 baseline + 1 new fixture) TS yaml-conformance 13/13 C# YamlConformance 13/13 Java YamlConformanceTest 13/13 TS metadata full 1226/1226 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ad5c2cc commit 88e729d

5 files changed

Lines changed: 113 additions & 18 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_YAML_COERCION",
5+
"source": {
6+
"format": "yaml",
7+
"files": [
8+
"input.yaml"
9+
],
10+
"jsonPath": "$"
11+
}
12+
},
13+
{
14+
"code": "ERR_BAD_ATTR_VALUE",
15+
"source": {
16+
"format": "json",
17+
"files": [
18+
"input.yaml"
19+
],
20+
"jsonPath": "$['metadata.root'].children[0]['object.entity'].children[0]['field.string']"
21+
}
22+
}
23+
],
24+
"warnings": []
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
metadata:
2+
children:
3+
- object.entity:
4+
name: Product
5+
children:
6+
- field.string:
7+
name: sku
8+
maxLength: TRUE

server/python/src/metaobjects/core_types.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@
1414
)
1515
from .meta.core.field import field_constants as fc
1616
from .meta.core.field.field_constants import (
17+
AUTO_SET_VALUES,
18+
FIELD_ATTR_AUTO_SET,
19+
FIELD_ATTR_COLUMN,
20+
FIELD_ATTR_DEFAULT,
21+
FIELD_ATTR_FILTERABLE,
22+
FIELD_ATTR_MAX_LENGTH,
1723
FIELD_ATTR_OBJECT_REF,
24+
FIELD_ATTR_PRECISION,
25+
FIELD_ATTR_REQUIRED,
26+
FIELD_ATTR_SCALE,
27+
FIELD_ATTR_SORTABLE,
28+
FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
1829
FIELD_ATTR_STORAGE,
30+
FIELD_ATTR_UNIQUE,
1931
FIELD_ATTR_VALUES,
2032
FIELD_SUBTYPE_ENUM,
33+
SORT_ORDER_VALUES,
2134
STORAGE_VALUES,
2235
)
2336
from .meta.core.field.meta_field import MetaField
@@ -175,14 +188,20 @@ def _register_subtypes(
175188
ChildRule(TYPE_VIEW, "*"),
176189
ChildRule(TYPE_VALIDATOR, "*"),
177190
]
178-
# Common field attrs declared by the core port. `@column` carries a declared
179-
# string valueType so the YAML desugar's D2 type-coercion guard (ADR-0006) can
180-
# detect a YAML 1.2 silently-coerced unquoted boolean/number value
181-
# (e.g. `column: TRUE` → boolean True instead of the string "TRUE"). The TS
182-
# port registers this through a dedicated dbProvider that extends field types;
183-
# Python keeps it on the core field defs until a full Python db-codegen port lands.
191+
# Common field attrs declared by the core port. Each attr carries a declared
192+
# `value_type` so the YAML desugar's D2 type-coercion guard (ADR-0006) can
193+
# detect a YAML 1.2 silently-coerced unquoted value (e.g. `maxLength: true`
194+
# coerced to boolean instead of the int it should be).
195+
#
196+
# Mirrors server/typescript/packages/metadata/src/core/field/field-schema.ts
197+
# `commonFieldAttrs` and server/csharp/MetaObjects/Core/Field/FieldSchema.cs
198+
# `CommonFieldAttrs` so Python's coercion coverage stays at parity.
199+
#
200+
# The TS port registers `@column` through a dedicated dbProvider that extends
201+
# field types; Python keeps it on the core field defs until a full Python
202+
# db-codegen port lands.
184203
_FIELD_COMMON_ATTRS = [
185-
AttrSchema(name="column", value_type=ATTR_SUBTYPE_STRING, required=False),
204+
AttrSchema(name=FIELD_ATTR_OBJECT_REF, value_type=ATTR_SUBTYPE_STRING, required=False),
186205
# @storage applies to field.object only (cross-port); validation_passes enforces
187206
# the shape rules + non-object-subtype guard. Declared at the common level so
188207
# the AttrSchema parser doesn't reject it on field.object before validation runs.
@@ -192,10 +211,31 @@ def _register_subtypes(
192211
required=False,
193212
allowed_values=STORAGE_VALUES,
194213
),
195-
# @objectRef is the cross-entity reference for field.object — declared on the
196-
# common attrs so an inline @objectRef on any field subtype routes correctly
197-
# (validation lives in validation_passes.py).
198-
AttrSchema(name=FIELD_ATTR_OBJECT_REF, value_type=ATTR_SUBTYPE_STRING, required=False),
214+
AttrSchema(name=FIELD_ATTR_REQUIRED, value_type=ATTR_SUBTYPE_BOOLEAN, required=False),
215+
AttrSchema(name=FIELD_ATTR_UNIQUE, value_type=ATTR_SUBTYPE_BOOLEAN, required=False),
216+
# @default is polymorphic: its value type follows the OWNING field's
217+
# subtype. No single fixed valueType can capture that, so value_type is
218+
# intentionally None (declared-but-untyped). The YAML coercion guard
219+
# skips entries with value_type=None.
220+
AttrSchema(name=FIELD_ATTR_DEFAULT, value_type=None, required=False),
221+
AttrSchema(name=FIELD_ATTR_MAX_LENGTH, value_type=ATTR_SUBTYPE_INT, required=False),
222+
AttrSchema(name=FIELD_ATTR_PRECISION, value_type=ATTR_SUBTYPE_INT, required=False),
223+
AttrSchema(name=FIELD_ATTR_SCALE, value_type=ATTR_SUBTYPE_INT, required=False),
224+
AttrSchema(name=FIELD_ATTR_FILTERABLE, value_type=ATTR_SUBTYPE_BOOLEAN, required=False),
225+
AttrSchema(name=FIELD_ATTR_SORTABLE, value_type=ATTR_SUBTYPE_BOOLEAN, required=False),
226+
AttrSchema(
227+
name=FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
228+
value_type=ATTR_SUBTYPE_STRING,
229+
required=False,
230+
allowed_values=SORT_ORDER_VALUES,
231+
),
232+
AttrSchema(
233+
name=FIELD_ATTR_AUTO_SET,
234+
value_type=ATTR_SUBTYPE_STRING,
235+
required=False,
236+
allowed_values=AUTO_SET_VALUES,
237+
),
238+
AttrSchema(name=FIELD_ATTR_COLUMN, value_type=ATTR_SUBTYPE_STRING, required=False),
199239
]
200240
_register_subtypes(
201241
core_provider,
@@ -206,21 +246,21 @@ def _register_subtypes(
206246
attrs=_FIELD_COMMON_ATTRS,
207247
)
208248

209-
# field.enum — dedicated registration with required @values attr
249+
# field.enum — dedicated registration with required @values attr.
250+
# Inherits every common field attr (column / required / unique / default /
251+
# maxLength / filterable / sortable / etc.) so the D2 type-coercion guard
252+
# applies uniformly across all fields.
210253
core_provider.add(
211254
TypeDefinition(
212255
type=TYPE_FIELD,
213256
sub_type=FIELD_SUBTYPE_ENUM,
214257
factory=MetaField,
215-
attrs=[
258+
attrs=list(_FIELD_COMMON_ATTRS) + [
216259
AttrSchema(
217260
name=FIELD_ATTR_VALUES,
218261
value_type=ATTR_SUBTYPE_STRINGARRAY,
219262
required=True,
220263
),
221-
# See _FIELD_COMMON_ATTRS above — `@column` is declared on enum too
222-
# so the D2 type-coercion guard applies uniformly across all fields.
223-
AttrSchema(name="column", value_type=ATTR_SUBTYPE_STRING, required=False),
224264
],
225265
child_rules=_FIELD_CHILD_RULES,
226266
)

server/python/src/metaobjects/meta/core/field/field_constants.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,35 @@
3434
)
3535

3636
# Reserved field attribute names (read by codegen; open attrs at load time).
37+
# Mirrors server/typescript/packages/metadata/src/core/field/field-constants.ts.
3738
FIELD_ATTR_REQUIRED = "required"
39+
FIELD_ATTR_UNIQUE = "unique"
40+
FIELD_ATTR_DEFAULT = "default"
3841
FIELD_ATTR_MAX_LENGTH = "maxLength"
42+
FIELD_ATTR_PRECISION = "precision"
43+
FIELD_ATTR_SCALE = "scale"
44+
FIELD_ATTR_FILTERABLE = "filterable"
45+
FIELD_ATTR_SORTABLE = "sortable"
46+
FIELD_ATTR_SORTABLE_DEFAULT_ORDER = "sortableDefaultOrder"
47+
FIELD_ATTR_AUTO_SET = "autoSet"
3948
FIELD_ATTR_OBJECT_REF = "objectRef"
40-
FIELD_ATTR_DEFAULT = "default"
4149
FIELD_ATTR_VALUES = "values"
4250
# Persistence-side storage shape for owned field.object data. Cross-port values.
4351
FIELD_ATTR_STORAGE = "storage"
4452
STORAGE_VALUES = ("flattened", "jsonb", "subdocument")
4553
# Physical column name override (cross-port; renamed from @dbColumn).
4654
FIELD_ATTR_COLUMN = "column"
4755

56+
# @autoSet allowed values (cross-port).
57+
AUTO_SET_ON_CREATE = "onCreate"
58+
AUTO_SET_ON_UPDATE = "onUpdate"
59+
AUTO_SET_VALUES = (AUTO_SET_ON_CREATE, AUTO_SET_ON_UPDATE)
60+
61+
# @sortableDefaultOrder allowed values (cross-port).
62+
SORT_ORDER_ASC = "asc"
63+
SORT_ORDER_DESC = "desc"
64+
SORT_ORDER_VALUES = (SORT_ORDER_ASC, SORT_ORDER_DESC)
65+
4866
# Regex pattern for enum member symbols — must be identifier-safe.
4967
# Cross-language contract: every port enforces this pattern.
5068
ENUM_MEMBER_PATTERN = r"^[A-Za-z_][A-Za-z0-9_]*$"

server/python/src/metaobjects/registry.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
@dataclass(frozen=True)
99
class AttrSchema:
1010
name: str
11-
value_type: str # an attr subtype name, e.g. "string", "boolean", "stringArray"
11+
# An attr subtype name, e.g. "string", "boolean", "stringArray".
12+
# Optional: a None value_type declares the attr as "known but untyped",
13+
# which the YAML coercion guard skips. Used for polymorphic attrs like
14+
# @default whose value type follows the OWNING field's subtype.
15+
value_type: str | None
1216
required: bool = False
1317
allowed_values: tuple[str, ...] | None = None
1418
default: object | None = None

0 commit comments

Comments
 (0)