Skip to content

Commit 7c30b60

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 afdd17e commit 7c30b60

3 files changed

Lines changed: 705 additions & 6 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import {
9494
} from './psl-field-resolution';
9595
import { resolveNamedTypeDeclarations } from './psl-named-type-resolution';
9696
import {
97+
type ArrowPath,
9798
applyBackrelationCandidates,
9899
type FkRelationMetadata,
99100
indexFkRelations,
@@ -520,6 +521,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
520521
});
521522
const relationAttribute = getAttribute(field.attributes, 'relation');
522523
let through: ParsedThrough | undefined;
524+
let arrowPath: ArrowPath | undefined;
523525
let inverse: string | undefined;
524526
if (relationAttribute) {
525527
const parsedRelation = parseRelationAttribute({
@@ -551,6 +553,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
551553
continue;
552554
}
553555
through = parsedRelation.through;
556+
arrowPath = parsedRelation.arrowPath;
554557
inverse = parsedRelation.inverse;
555558
}
556559
if (!attributesValid) {
@@ -563,6 +566,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
563566
field,
564567
targetModelName: field.typeName,
565568
...ifDefined('through', through),
569+
...ifDefined('arrowPath', arrowPath),
566570
...ifDefined('inverse', inverse),
567571
});
568572
}
@@ -2056,19 +2060,27 @@ export function interpretPslDocumentToSqlContract(
20562060
const modelIdColumns = new Map<string, readonly string[]>();
20572061
const modelTableNames = new Map<string, string>();
20582062
const declaredTableNames = new Set<string>();
2063+
// Field-name → storage-column maps per model, so an arrow-path `through:` can
2064+
// resolve its path-named columns straight from the declared models.
2065+
const modelFieldColumns = new Map<string, ReadonlyMap<string, string>>();
20592066
for (const modelNode of modelNodes) {
20602067
if (modelNode.id) {
20612068
modelIdColumns.set(modelNode.modelName, modelNode.id.columns);
20622069
}
20632070
modelTableNames.set(modelNode.modelName, modelNode.tableName);
20642071
declaredTableNames.add(modelNode.modelName);
20652072
declaredTableNames.add(modelNode.tableName);
2073+
modelFieldColumns.set(
2074+
modelNode.modelName,
2075+
new Map(modelNode.fields.map((field) => [field.fieldName, field.columnName])),
2076+
);
20662077
}
20672078
const { synthesizedJunctions } = applyBackrelationCandidates({
20682079
backrelationCandidates,
20692080
fkRelationsByPair,
20702081
fkRelationsByDeclaringModel,
20712082
modelIdColumns,
2083+
modelFieldColumns,
20722084
modelTableNames,
20732085
modelNamespaceIds,
20742086
declaredTableNames,

0 commit comments

Comments
 (0)