Skip to content

Commit 075600e

Browse files
committed
feat(contract-psl): arrow-path through: column-based M:N recognition
Recognise the arrow-path many-to-many form `through: "localKey -> Junction.nearCol -> Junction.farCol -> Target.targetKey"` on terminal models over a junction that carries scalar columns + @@id but no relation fields. The relation-field-based junction recognition cannot fire when the junction declares no relation fields, so the resolver builds the `through` descriptor straight from the path-named columns. Value form: a quoted string (no grammar change). The parser already produces a StringLiteralExpr; the resolver detects the `->` separator, strips quotes, and splits the four segments. An unquoted arrow grammar would require a new tokenizer token plus an expression node, AST class, union/cast extension, printSyntax rendering, and formatter spacing rules — out of scope for this escape-hatch slice. Each end carries its own mirror-imaged path; both ends lower to N:M + through. Diagnostics: malformed path (not four segments / bad arrows), a named column absent on its model, the two junction columns on different models, and a junction that is not a declared model. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent f8559d9 commit 075600e

4 files changed

Lines changed: 739 additions & 12 deletions

File tree

packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export type PslDiagnosticCode =
3030
| 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE'
3131
| 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE'
3232
| 'PSL_INVALID_RELATION_ATTRIBUTE'
33+
/**
34+
* A quoted arrow-path `through:` value that does not parse as the
35+
* four-segment `"localKey -> Junction.nearColumn -> Junction.farColumn ->
36+
* Target.targetKey"` form.
37+
*/
38+
| 'PSL_ARROW_PATH_MALFORMED'
3339
| 'PSL_INVALID_REFERENTIAL_ACTION'
3440
| 'PSL_INVALID_DEFAULT_VALUE'
3541
| 'PSL_INVALID_ENUM_MEMBER'

packages/2-sql/2-authoring/contract-psl/src/interpreter.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import {
100100
} from './psl-field-resolution';
101101
import { resolveNamedTypeDeclarations } from './psl-named-type-resolution';
102102
import {
103+
type ArrowPath,
103104
applyBackrelationCandidates,
104105
type FkRelationMetadata,
105106
indexFkRelations,
@@ -568,6 +569,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
568569
});
569570
const relationAttribute = getAttribute(field.attributes, 'relation');
570571
let through: ParsedThrough | undefined;
572+
let arrowPath: ArrowPath | undefined;
571573
let inverse: string | undefined;
572574
if (relationAttribute) {
573575
const parsedRelation = interpretRelationAttribute({
@@ -600,6 +602,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
600602
continue;
601603
}
602604
through = parsedRelation.through;
605+
arrowPath = parsedRelation.arrowPath;
603606
inverse = parsedRelation.inverse;
604607
}
605608
if (!attributesValid) {
@@ -612,6 +615,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
612615
field,
613616
targetModelName: field.typeName,
614617
...ifDefined('through', through),
618+
...ifDefined('arrowPath', arrowPath),
615619
...ifDefined('inverse', inverse),
616620
});
617621
}
@@ -2158,19 +2162,27 @@ export function interpretPslDocumentToSqlContract(
21582162
const modelIdColumns = new Map<string, readonly string[]>();
21592163
const modelTableNames = new Map<string, string>();
21602164
const declaredTableNames = new Set<string>();
2165+
// Field-name → storage-column maps per model, so an arrow-path `through:` can
2166+
// resolve its path-named columns straight from the declared models.
2167+
const modelFieldColumns = new Map<string, ReadonlyMap<string, string>>();
21612168
for (const modelNode of modelNodes) {
21622169
if (modelNode.id) {
21632170
modelIdColumns.set(modelNode.modelName, modelNode.id.columns);
21642171
}
21652172
modelTableNames.set(modelNode.modelName, modelNode.tableName);
21662173
declaredTableNames.add(modelNode.modelName);
21672174
declaredTableNames.add(modelNode.tableName);
2175+
modelFieldColumns.set(
2176+
modelNode.modelName,
2177+
new Map(modelNode.fields.map((field) => [field.fieldName, field.columnName])),
2178+
);
21682179
}
21692180
const { synthesizedJunctions } = applyBackrelationCandidates({
21702181
backrelationCandidates,
21712182
fkRelationsByPair,
21722183
fkRelationsByDeclaringModel,
21732184
modelIdColumns,
2185+
modelFieldColumns,
21742186
modelTableNames,
21752187
modelNamespaceIds,
21762188
declaredTableNames,

0 commit comments

Comments
 (0)