@metaobjectsdev/metadata 0.23.1-rc.1 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +89 -11
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/attr/attr-constants.d.ts +2 -1
- package/dist/core/attr/attr-constants.d.ts.map +1 -1
- package/dist/core/attr/attr-constants.js +6 -0
- package/dist/core/attr/attr-constants.js.map +1 -1
- package/dist/core/attr/attr-definition.embedded.d.ts.map +1 -1
- package/dist/core/attr/attr-definition.embedded.js +6 -0
- package/dist/core/attr/attr-definition.embedded.js.map +1 -1
- package/dist/core/attr/meta-attr-int-map.d.ts +9 -0
- package/dist/core/attr/meta-attr-int-map.d.ts.map +1 -0
- package/dist/core/attr/meta-attr-int-map.js +50 -0
- package/dist/core/attr/meta-attr-int-map.js.map +1 -0
- package/dist/core/field/field-constants.d.ts +9 -0
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +9 -0
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-definition.embedded.d.ts.map +1 -1
- package/dist/core/field/field-definition.embedded.js +8 -0
- package/dist/core/field/field-definition.embedded.js.map +1 -1
- package/dist/core/query/query-constants.d.ts +30 -0
- package/dist/core/query/query-constants.d.ts.map +1 -1
- package/dist/core/query/query-constants.js +35 -1
- package/dist/core/query/query-constants.js.map +1 -1
- package/dist/core-types.d.ts +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +1 -0
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +7 -3
- package/dist/loader/validation-passes.js.map +1 -1
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +113 -10
- package/src/core/attr/attr-constants.ts +6 -0
- package/src/core/attr/attr-definition.embedded.ts +6 -0
- package/src/core/attr/meta-attr-int-map.ts +54 -0
- package/src/core/field/field-constants.ts +10 -0
- package/src/core/field/field-definition.embedded.ts +8 -0
- package/src/core/query/query-constants.ts +47 -1
- package/src/core-types.ts +1 -0
- package/src/errors.ts +6 -0
- package/src/loader/validation-passes.ts +7 -2
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
FIELD_ATTR_VALUES,
|
|
53
53
|
FIELD_ATTR_COERCE_DEFAULT,
|
|
54
54
|
FIELD_ATTR_DEFAULT,
|
|
55
|
+
FIELD_ATTR_INT_VALUE_MAP,
|
|
55
56
|
ENUM_MEMBER_PATTERN,
|
|
56
57
|
} from "./core/field/field-constants.js";
|
|
57
58
|
import {
|
|
@@ -309,6 +310,19 @@ function validateNode(
|
|
|
309
310
|
// only own @values need checking here (mirrors the own-attrs-only policy of
|
|
310
311
|
// Checks 2+3 above — inherited attrs were validated on the declaring node).
|
|
311
312
|
if (node.type === TYPE_FIELD && node.subType === FIELD_SUBTYPE_ENUM) {
|
|
313
|
+
// #246: the shared-enum super, if any — a root-level abstract field.enum (one
|
|
314
|
+
// whose parent is the metadata root, not an object). FR-019 materializes such a
|
|
315
|
+
// declaration ONCE per port as a single named type, so anything a consuming
|
|
316
|
+
// field re-declares that is part of the shared TYPE's contract is a conflict.
|
|
317
|
+
// Immediate-super-only, matching codegen's resolveSharedEnumDecl (enum-shared.ts)
|
|
318
|
+
// so the validator and the collapse agree on what "shared" means.
|
|
319
|
+
const sharedSuper =
|
|
320
|
+
node.superData !== undefined &&
|
|
321
|
+
node.superData.isAbstract &&
|
|
322
|
+
node.superData.parent?.type === TYPE_METADATA
|
|
323
|
+
? node.superData
|
|
324
|
+
: undefined;
|
|
325
|
+
|
|
312
326
|
// ADR-0039: own — validates the @values DECLARED on this node; a concrete enum
|
|
313
327
|
// extending an abstract one inherits already-validated @values (own-attrs-only).
|
|
314
328
|
const rawValues = node.ownAttrs().get(FIELD_ATTR_VALUES);
|
|
@@ -347,18 +361,16 @@ function validateNode(
|
|
|
347
361
|
}
|
|
348
362
|
}
|
|
349
363
|
|
|
350
|
-
// #246: a field.enum extending a shared package-level abstract enum
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
const sup = node.superData;
|
|
358
|
-
if (sup !== undefined && sup.isAbstract && sup.parent?.type === TYPE_METADATA) {
|
|
364
|
+
// #246: a field.enum extending a shared package-level abstract enum that
|
|
365
|
+
// ALSO declares its own @values is a conflict: one shared enum type has one
|
|
366
|
+
// member set, so codegen's shared-enum collapse would silently drop this
|
|
367
|
+
// field's own @values in favor of the shared type's. Own-attrs-only
|
|
368
|
+
// (matches the rest of Check 4): only fires when THIS node declares
|
|
369
|
+
// @values itself, not when it merely inherits.
|
|
370
|
+
if (sharedSuper !== undefined) {
|
|
359
371
|
errors.push(
|
|
360
372
|
new ParseError(
|
|
361
|
-
`${nodeLabel(node)} extends shared abstract enum '${nodeLabel(
|
|
373
|
+
`${nodeLabel(node)} extends shared abstract enum '${nodeLabel(sharedSuper)}' AND declares its own ` +
|
|
362
374
|
`'@${FIELD_ATTR_VALUES}' — a shared enum's member set is owned by the shared declaration; ` +
|
|
363
375
|
`remove the own '@${FIELD_ATTR_VALUES}' to inherit it, or extend a non-shared enum instead.`,
|
|
364
376
|
{ code: "ERR_ENUM_EXTENDS_VALUES_CONFLICT", source: node.source },
|
|
@@ -399,6 +411,97 @@ function validateNode(
|
|
|
399
411
|
}
|
|
400
412
|
}
|
|
401
413
|
}
|
|
414
|
+
|
|
415
|
+
// --- Check 5a: @intValueMap is scalar-only (design D7) ---
|
|
416
|
+
//
|
|
417
|
+
// Int-backing is a persistence-layer CODEC, and no port implements it
|
|
418
|
+
// element-wise over an array column: OMDB's EnumCodec and Kotlin's
|
|
419
|
+
// customEnumeration are scalar by construction, Python would bind the symbol
|
|
420
|
+
// list straight into an integer[], and TS's sqlite branch serializes an array
|
|
421
|
+
// as JSON text before the enum case is ever reached. Two ports that DO compose
|
|
422
|
+
// (TS/Postgres via .array(), C# via PrimitiveCollection) are not a feature —
|
|
423
|
+
// shipping a claim four ports silently get wrong is the field.byte/short/class
|
|
424
|
+
// mistake. Rejected at LOAD so it fails identically everywhere.
|
|
425
|
+
//
|
|
426
|
+
// BOTH reads are RESOLVING, unlike Check 5b below: the illegal thing is the
|
|
427
|
+
// EFFECTIVE combination. Post-#246 the map must live on the shared abstract
|
|
428
|
+
// declaration, so the field that inherits it is exactly where isArray gets
|
|
429
|
+
// declared — an own-only read would see the two halves on different nodes and
|
|
430
|
+
// never fire.
|
|
431
|
+
if (node.attrs().get(FIELD_ATTR_INT_VALUE_MAP) !== undefined && node.resolvedIsArray()) {
|
|
432
|
+
errors.push(
|
|
433
|
+
new ParseError(
|
|
434
|
+
`${nodeLabel(node)} declares '@${FIELD_ATTR_INT_VALUE_MAP}' with isArray=true; ` +
|
|
435
|
+
`int-backing is scalar-only — an array-of-enum persists its member symbols. ` +
|
|
436
|
+
`Remove '@${FIELD_ATTR_INT_VALUE_MAP}', or make the field scalar.`,
|
|
437
|
+
{ code: "ERR_ENUM_INT_VALUE_MAP_ARRAY", source: node.source },
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// --- Check 5b: field.enum @intValueMap content rules ---
|
|
443
|
+
//
|
|
444
|
+
// Optional. Own-only (mirrors Checks 4/5's own-attrs-only policy) — an
|
|
445
|
+
// inherited @intValueMap is validated on its declaring node. The generic
|
|
446
|
+
// "is this an object of integers" shape check already ran via IntMapAttr
|
|
447
|
+
// (attr subtype `intMap`); this validates the field.enum-SPECIFIC
|
|
448
|
+
// semantics: key-set-equals-@values, and no two members share a value.
|
|
449
|
+
const rawIntValueMap = node.ownAttrs().get(FIELD_ATTR_INT_VALUE_MAP);
|
|
450
|
+
if (rawIntValueMap !== undefined && typeof rawIntValueMap === "object" && rawIntValueMap !== null) {
|
|
451
|
+
// #246 (int-backed twin): the symbol→int mapping is a property of the enum
|
|
452
|
+
// VOCABULARY, not of one column that uses it — it is @values' numeric half.
|
|
453
|
+
// A shared enum is materialized once as a single type, so a per-field map
|
|
454
|
+
// would give one logical type N storage encodings (and, where a port emits
|
|
455
|
+
// per-TYPE codec artifacts, two same-named declarations). Same remedy as the
|
|
456
|
+
// @values half: declare it on the shared declaration and inherit it.
|
|
457
|
+
if (sharedSuper !== undefined) {
|
|
458
|
+
errors.push(
|
|
459
|
+
new ParseError(
|
|
460
|
+
`${nodeLabel(node)} extends shared abstract enum '${nodeLabel(sharedSuper)}' AND declares its own ` +
|
|
461
|
+
`'@${FIELD_ATTR_INT_VALUE_MAP}' — a shared enum's integer backing is owned by the shared ` +
|
|
462
|
+
`declaration; move '@${FIELD_ATTR_INT_VALUE_MAP}' onto '${nodeLabel(sharedSuper)}' to inherit it, ` +
|
|
463
|
+
`or extend a non-shared enum instead.`,
|
|
464
|
+
{ code: "ERR_ENUM_EXTENDS_VALUES_CONFLICT", source: node.source },
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const map = rawIntValueMap as Record<string, number>;
|
|
469
|
+
const effectiveValues = node.attrs().get(FIELD_ATTR_VALUES);
|
|
470
|
+
const declaredMembers: string[] = Array.isArray(effectiveValues) ? effectiveValues : [];
|
|
471
|
+
const memberSet = new Set(declaredMembers);
|
|
472
|
+
const mapKeys = Object.keys(map);
|
|
473
|
+
const keySet = new Set(mapKeys);
|
|
474
|
+
|
|
475
|
+
const missing = declaredMembers.filter((m) => !keySet.has(m));
|
|
476
|
+
const extra = mapKeys.filter((k) => !memberSet.has(k));
|
|
477
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
478
|
+
errors.push(
|
|
479
|
+
new ParseError(
|
|
480
|
+
`${nodeLabel(node)} attribute '@${FIELD_ATTR_INT_VALUE_MAP}' keys must exactly match '@${FIELD_ATTR_VALUES}' members` +
|
|
481
|
+
(missing.length > 0 ? ` (missing: ${missing.join(", ")})` : "") +
|
|
482
|
+
(extra.length > 0 ? ` (unknown: ${extra.join(", ")})` : "") + ".",
|
|
483
|
+
{ code: "ERR_BAD_ATTR_VALUE", source: node.source },
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const seenValues = new Map<number, string>();
|
|
489
|
+
for (const [member, value] of Object.entries(map)) {
|
|
490
|
+
if (typeof value !== "number" || !Number.isInteger(value)) continue; // IntMapAttr already reported this
|
|
491
|
+
const owner = seenValues.get(value);
|
|
492
|
+
if (owner !== undefined) {
|
|
493
|
+
errors.push(
|
|
494
|
+
new ParseError(
|
|
495
|
+
`${nodeLabel(node)} attribute '@${FIELD_ATTR_INT_VALUE_MAP}' members '${owner}' and '${member}' ` +
|
|
496
|
+
`share the same value ${value}; every member must have a unique int.`,
|
|
497
|
+
{ code: "ERR_BAD_ATTR_VALUE", source: node.source },
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
} else {
|
|
501
|
+
seenValues.set(value, member);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
402
505
|
}
|
|
403
506
|
|
|
404
507
|
// --- Check 6 (R6 Plan 2b): @dbColumnType (logical subtype × value) pairing ---
|
|
@@ -17,6 +17,11 @@ export const ATTR_SUBTYPE_FILTER = "filter";
|
|
|
17
17
|
// #195 — a structured expression tree over a base entity's own fields (backs
|
|
18
18
|
// origin.computed). Object-shaped; a closed node grammar (see meta-attr-expression.ts).
|
|
19
19
|
export const ATTR_SUBTYPE_EXPRESSION = "expression";
|
|
20
|
+
// An object-shaped attr whose values are all integers (e.g. field.enum's
|
|
21
|
+
// @intValueMap: {memberSymbol: int}). Generic shape check only — semantic
|
|
22
|
+
// rules specific to a consumer (key-set membership, uniqueness) are that
|
|
23
|
+
// consumer's own content-rule validation, not this attr's.
|
|
24
|
+
export const ATTR_SUBTYPE_INT_MAP = "intMap";
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* The retired `stringarray` array attr subtype. It is NO LONGER a registered
|
|
@@ -41,6 +46,7 @@ export const ATTR_SUBTYPES = [
|
|
|
41
46
|
ATTR_SUBTYPE_PROPERTIES,
|
|
42
47
|
ATTR_SUBTYPE_FILTER,
|
|
43
48
|
ATTR_SUBTYPE_EXPRESSION,
|
|
49
|
+
ATTR_SUBTYPE_INT_MAP,
|
|
44
50
|
] as const;
|
|
45
51
|
export type AttrSubType =
|
|
46
52
|
| (typeof ATTR_SUBTYPES)[number]
|
|
@@ -27,6 +27,12 @@ export const ATTR_DEFINITION: ProviderDefinition = {
|
|
|
27
27
|
"dataType": "int",
|
|
28
28
|
"description": "A 32-bit-integer-valued metadata attribute. Coerces to and validates as a number."
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"type": "attr",
|
|
32
|
+
"subType": "intMap",
|
|
33
|
+
"dataType": "object",
|
|
34
|
+
"description": "An object-shaped attribute whose values are all integers (e.g. field.enum's @intValueMap: {memberSymbol: int}). Generic shape check only; a consumer field type layers its own semantic rules (key-set membership, uniqueness) in its own content-rule validation."
|
|
35
|
+
},
|
|
30
36
|
{
|
|
31
37
|
"type": "attr",
|
|
32
38
|
"subType": "long",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// IntMapAttr — attr subtype `intMap`. Object-shaped value whose members must
|
|
2
|
+
// all be integers (e.g. field.enum's @intValueMap). No desugar; validates
|
|
3
|
+
// shape (object, not array) and every value's type (integer). A consumer's
|
|
4
|
+
// own semantic rules (key-set membership, uniqueness) are validated by that
|
|
5
|
+
// consumer, not here — mirrors how StringArrayAttr validates shape while
|
|
6
|
+
// field.enum's own content-rule pass validates its @values semantics.
|
|
7
|
+
|
|
8
|
+
import { MetaAttr, type ValueError, runtimeTypeName } from "./meta-attr.js";
|
|
9
|
+
import { type AttrValue } from "../../shared/meta-data.js";
|
|
10
|
+
import { DATA_TYPE_OBJECT, type DataType } from "../../data-type.js";
|
|
11
|
+
import { registerAttrClass } from "../../attr-class-map.js";
|
|
12
|
+
import { ATTR_SUBTYPE_INT_MAP } from "./attr-constants.js";
|
|
13
|
+
|
|
14
|
+
// 32-bit signed integer bounds (inclusive) — the eventual DB column for an
|
|
15
|
+
// int-backed enum is a 32-bit Postgres/SQLite `integer` (design doc D5).
|
|
16
|
+
const INT32_MIN = -2147483648;
|
|
17
|
+
const INT32_MAX = 2147483647;
|
|
18
|
+
|
|
19
|
+
export class IntMapAttr extends MetaAttr {
|
|
20
|
+
override get dataType(): DataType {
|
|
21
|
+
return DATA_TYPE_OBJECT;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override coerce(raw: unknown): AttrValue {
|
|
25
|
+
return raw as AttrValue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override validateValue(value: AttrValue): ValueError[] {
|
|
29
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
30
|
+
return [{ message: `attribute '@${this.name}' must be of type 'intMap' but got ${runtimeTypeName(value)}` }];
|
|
31
|
+
}
|
|
32
|
+
const errors: ValueError[] = [];
|
|
33
|
+
for (const [key, member] of Object.entries(value as Record<string, unknown>)) {
|
|
34
|
+
if (typeof member !== "number" || !Number.isInteger(member)) {
|
|
35
|
+
errors.push({
|
|
36
|
+
message: `attribute '@${this.name}' member '${key}' has value '${String(member)}' which is not an integer`,
|
|
37
|
+
});
|
|
38
|
+
} else if (member < INT32_MIN || member > INT32_MAX) {
|
|
39
|
+
// The eventual DB column for an int-backed enum is a 32-bit Postgres/
|
|
40
|
+
// SQLite `integer` (design doc D5; matches field.int's existing
|
|
41
|
+
// 32-bit mapping in expected-schema.ts) — mirrors Java's
|
|
42
|
+
// IntMapAttribute#setValueAsString bound check exactly (inclusive at
|
|
43
|
+
// both ends) so a value that could never be persisted fails at load
|
|
44
|
+
// time, not silently, on every port.
|
|
45
|
+
errors.push({
|
|
46
|
+
message: `attribute '@${this.name}' member '${key}' has value '${member}' which is outside the 32-bit signed integer range`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerAttrClass(ATTR_SUBTYPE_INT_MAP, IntMapAttr);
|
|
@@ -166,6 +166,16 @@ export const FIELD_ATTR_CURRENCY_DEFAULT = "USD";
|
|
|
166
166
|
/** Member symbols of an enum-subtype field. Required, string array. */
|
|
167
167
|
export const FIELD_ATTR_VALUES = "values";
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Optional per-member explicit integer value ({memberSymbol: int}) switching
|
|
171
|
+
* this enum field's DB persistence from string+CHECK to integer+CHECK. Keys
|
|
172
|
+
* must exactly match @values; values must be unique integers. The generated
|
|
173
|
+
* native type and wire format are UNCHANGED in every language — this is a
|
|
174
|
+
* persistence-layer-only concern (docs/superpowers/specs/2026-07-23-int-backed-
|
|
175
|
+
* enum-values-design.md).
|
|
176
|
+
*/
|
|
177
|
+
export const FIELD_ATTR_INT_VALUE_MAP = "intValueMap";
|
|
178
|
+
|
|
169
179
|
/**
|
|
170
180
|
* Pattern every enum member must satisfy: a legal identifier in all target
|
|
171
181
|
* languages (TS union member, Java/C#/Python enum member). Ensures symbol ==
|
|
@@ -279,6 +279,14 @@ export const FIELD_DEFINITION: ProviderDefinition = {
|
|
|
279
279
|
"min": 0,
|
|
280
280
|
"max": 1,
|
|
281
281
|
"description": "FR-019: marks an abstract package-level field.enum as externally provided — codegen references the type (resolved via per-port codegen config) instead of materializing it. Default false. Not a field attr — it lives on the type declaration."
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"type": "attr",
|
|
285
|
+
"subType": "intMap",
|
|
286
|
+
"name": "intValueMap",
|
|
287
|
+
"min": 0,
|
|
288
|
+
"max": 1,
|
|
289
|
+
"description": "Optional per-member int values ({member: int}) switching this enum field's DB persistence from string+CHECK to integer+CHECK. Keys must exactly match @values; values must be unique integers. The generated native type and wire format are unchanged in every language."
|
|
282
290
|
}
|
|
283
291
|
]
|
|
284
292
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FIELD_SUBTYPE_UUID, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_URI, FIELD_SUBTYPE_INET } from "../field/field-constants.js";
|
|
1
|
+
import { FIELD_SUBTYPE_UUID, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_URI, FIELD_SUBTYPE_INET, FIELD_ATTR_INT_VALUE_MAP } from "../field/field-constants.js";
|
|
2
2
|
|
|
3
3
|
// Query concern constants — filter operators, sort order values.
|
|
4
4
|
//
|
|
@@ -67,6 +67,52 @@ export function opsForSubType(subType: string): readonly FilterOp[] {
|
|
|
67
67
|
return OPS_BY_SUBTYPE[subType] ?? [];
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** The int-backed-enum band: the enum band minus `like`. Hoisted so the narrowing
|
|
71
|
+
* is one named constant rather than a filter re-derived at every call. */
|
|
72
|
+
const OPS_ENUM_INT_BACKED: readonly FilterOp[] = [
|
|
73
|
+
FILTER_OP_EQ, FILTER_OP_NE, FILTER_OP_IN, FILTER_OP_IS_NULL,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The structural shape `opsForField` needs. Declared here rather than importing
|
|
78
|
+
* `MetaField`: `query-constants` is foundational and `core/field` imports it, so a
|
|
79
|
+
* type import back the other way would close a cycle.
|
|
80
|
+
*/
|
|
81
|
+
export interface FilterOpBandField {
|
|
82
|
+
readonly subType: string;
|
|
83
|
+
attr(name: string): unknown;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The filter-operator band for a FIELD — the entry point every consumer that has a
|
|
88
|
+
* field in hand must use (loader validation, generated allowlists, generated client
|
|
89
|
+
* filter types, the cross-port `field.filter-ops` capability).
|
|
90
|
+
*
|
|
91
|
+
* Identical to {@link opsForSubType} except for ONE case: an int-backed `field.enum`
|
|
92
|
+
* (one declaring `@intValueMap`, design D5) persists as an INTEGER column, so `like`
|
|
93
|
+
* — a substring match — is dropped. `eq`/`ne`/`in` survive because the member symbol
|
|
94
|
+
* encodes to its integer before it reaches SQL; `like` has no such encoding, and an
|
|
95
|
+
* unencoded `LIKE 'DRAFT'` against an integer column is a request-time type error.
|
|
96
|
+
*
|
|
97
|
+
* `opsForSubType` cannot express this: it only ever sees the subtype `"enum"`. It is
|
|
98
|
+
* deliberately left unchanged for the one caller that genuinely has no field — the
|
|
99
|
+
* expression grammar's declared operand type.
|
|
100
|
+
*
|
|
101
|
+
* ADR-0039: the `@intValueMap` read is RESOLVING. Post-#246 the map lives on a shared
|
|
102
|
+
* root-level abstract declaration and consuming fields INHERIT it, so an own-only read
|
|
103
|
+
* would see `undefined` on exactly the shape adopters are steered toward and wrongly
|
|
104
|
+
* keep `like` in the band.
|
|
105
|
+
*/
|
|
106
|
+
export function opsForField(field: FilterOpBandField): readonly FilterOp[] {
|
|
107
|
+
if (field.subType === FIELD_SUBTYPE_ENUM && isIntBacked(field)) return OPS_ENUM_INT_BACKED;
|
|
108
|
+
return opsForSubType(field.subType);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isIntBacked(field: FilterOpBandField): boolean {
|
|
112
|
+
const raw = field.attr(FIELD_ATTR_INT_VALUE_MAP);
|
|
113
|
+
return raw !== undefined && raw !== null && typeof raw === "object";
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
// ---------------------------------------------------------------------------
|
|
71
117
|
// Sort order values (used by @sortableDefaultOrder on fields and
|
|
72
118
|
// @defaultSortOrder on dataGrid layouts)
|
package/src/core-types.ts
CHANGED
|
@@ -20,6 +20,7 @@ import "./core/attr/meta-attr-stringarray.js";
|
|
|
20
20
|
import "./core/attr/meta-attr-filter.js";
|
|
21
21
|
import "./core/attr/meta-attr-properties.js";
|
|
22
22
|
import "./core/attr/meta-attr-expression.js";
|
|
23
|
+
import "./core/attr/meta-attr-int-map.js";
|
|
23
24
|
import {
|
|
24
25
|
MetaValidator,
|
|
25
26
|
MetaRequiredValidator,
|
package/src/errors.ts
CHANGED
|
@@ -202,6 +202,12 @@ export const ERROR_CODES = [
|
|
|
202
202
|
// own @values would be silently dropped in codegen. Remove the own @values
|
|
203
203
|
// to inherit the shared set, or extend a concrete (non-shared) enum instead.
|
|
204
204
|
"ERR_ENUM_EXTENDS_VALUES_CONFLICT",
|
|
205
|
+
// A field.enum carries @intValueMap together with isArray=true. Int-backing is
|
|
206
|
+
// a persistence-layer codec and no port implements it element-wise over an
|
|
207
|
+
// array column, so the combination would silently persist member SYMBOLS into
|
|
208
|
+
// an integer array. An array-of-enum stays string-backed: drop @intValueMap,
|
|
209
|
+
// or make the field scalar.
|
|
210
|
+
"ERR_ENUM_INT_VALUE_MAP_ARRAY",
|
|
205
211
|
"ERR_UNKNOWN",
|
|
206
212
|
] as const;
|
|
207
213
|
|
|
@@ -130,6 +130,7 @@ import {
|
|
|
130
130
|
FILTER_COMPOSE_OR,
|
|
131
131
|
FILTER_COMPOSE_AND,
|
|
132
132
|
opsForSubType,
|
|
133
|
+
opsForField,
|
|
133
134
|
} from "../core/query/query-constants.js";
|
|
134
135
|
|
|
135
136
|
// ---------------------------------------------------------------------------
|
|
@@ -1684,7 +1685,9 @@ export function validateDataGridFilterValues(root: MetaData): ParseError[] {
|
|
|
1684
1685
|
for (const f of effective.filter((c) => c.type === TYPE_FIELD)) {
|
|
1685
1686
|
// ADR-0039: resolving — a concrete field may inherit @filterable via extends.
|
|
1686
1687
|
if (f.attr(FIELD_ATTR_FILTERABLE) === true) {
|
|
1687
|
-
|
|
1688
|
+
// opsForField, not opsForSubType — an int-backed field.enum (@intValueMap)
|
|
1689
|
+
// stores as an integer, so `like` is not in its band.
|
|
1690
|
+
allow.set(f.name, opsForField(f));
|
|
1688
1691
|
}
|
|
1689
1692
|
}
|
|
1690
1693
|
for (const layout of effective.filter(
|
|
@@ -2034,7 +2037,9 @@ export function validateProjectionFilter(root: MetaData): ParseError[] {
|
|
|
2034
2037
|
origin !== undefined &&
|
|
2035
2038
|
origin.subType !== ORIGIN_SUBTYPE_PASSTHROUGH &&
|
|
2036
2039
|
origin.subType !== ORIGIN_SUBTYPE_COMPUTED;
|
|
2037
|
-
|
|
2040
|
+
// opsForField, not opsForSubType — an int-backed field.enum (@intValueMap)
|
|
2041
|
+
// stores as an integer, so `like` is not in its band.
|
|
2042
|
+
fields.set(f.name, { derived, ops: opsForField(f) });
|
|
2038
2043
|
}
|
|
2039
2044
|
checkProjectionFilterRefs(filter as Record<string, unknown>, fields, obj.name, obj.source, errors);
|
|
2040
2045
|
}
|