11package com .metaobjects .integration ;
22
33import com .fasterxml .jackson .databind .ObjectMapper ;
4+ import com .metaobjects .MetaRoot ;
45import com .metaobjects .database .CoreDBMetaDataProvider ;
56import com .metaobjects .field .MetaField ;
67import com .metaobjects .integration .Scenarios .QuerySpec ;
78import com .metaobjects .integration .Scenarios .SortSpec ;
89import com .metaobjects .manager .ObjectConnection ;
910import com .metaobjects .manager .QueryOptions ;
11+ import com .metaobjects .manager .db .M2MResolver ;
1012import com .metaobjects .manager .db .ObjectManagerDB ;
1113import com .metaobjects .manager .exp .Expression ;
1214import com .metaobjects .manager .exp .Range ;
1315import com .metaobjects .manager .exp .SortOrder ;
1416import com .metaobjects .object .MetaObject ;
1517import com .metaobjects .object .value .ValueObject ;
18+ import com .metaobjects .relationship .MetaRelationship ;
1619
1720import java .sql .Types ;
1821import java .time .ZoneOffset ;
@@ -38,13 +41,24 @@ private ObjectManagerDbAdapter() {}
3841 /**
3942 * Execute one {@link QuerySpec} against an open connection. Returns:
4043 * <ul>
41- * <li>{@code op:list} → {@code List<Map<String,Object>>} of normalized rows</li>
42- * <li>{@code op:get} → {@code Map<String,Object>} or {@code null}</li>
43- * <li>{@code op:count} → {@code Long}</li>
44+ * <li>{@code op:list} → {@code List<Map<String,Object>>} of normalized rows</li>
45+ * <li>{@code op:get} → {@code Map<String,Object>} or {@code null}</li>
46+ * <li>{@code op:count} → {@code Long}</li>
47+ * <li>{@code op:relate} → {@code List<Map<String,Object>>} of related target
48+ * rows (order-independent — see {@link Normalization}); traverses an M:N
49+ * relationship via {@link M2MResolver}.</li>
4450 * </ul>
51+ *
52+ * @param root the loaded model root (needed by op:relate to find the junction +
53+ * target entities); other ops ignore it.
4554 */
4655 static Object execute (ObjectManagerDB omdb , ObjectConnection conn , MetaObject mc , QuerySpec spec ,
47- Map <String , Integer > columnSqlTypes ) throws Exception {
56+ Map <String , Integer > columnSqlTypes , MetaRoot root ,
57+ ColumnTypeProbe columnTypeProbe ) throws Exception {
58+ if ("relate" .equals (spec .op ())) {
59+ return executeRelate (omdb , conn , mc , spec , root , columnTypeProbe );
60+ }
61+
4862 Expression filter = buildFilter (spec .by (), spec .filter ());
4963
5064 if ("count" .equals (spec .op ())) {
@@ -64,6 +78,66 @@ static Object execute(ObjectManagerDB omdb, ObjectConnection conn, MetaObject mc
6478 return rows ; // op:list
6579 }
6680
81+ /** Probes the actual SQL column types for a target entity's relation, on demand. */
82+ @ FunctionalInterface
83+ interface ColumnTypeProbe {
84+ Map <String , Integer > probe (MetaObject mc );
85+ }
86+
87+ /**
88+ * op:relate — traverse an M:N relationship from a single source object to its
89+ * related target rows. Loads the source by {@code by:{...}}, locates the named
90+ * {@code relation}, delegates the junction traversal + target load to the
91+ * generic {@link M2MResolver}, then normalizes the target rows. The {@code relate}
92+ * verb is order-independent (the runner sorts both sides before comparing).
93+ */
94+ private static Object executeRelate (ObjectManagerDB omdb , ObjectConnection conn , MetaObject sourceMeta ,
95+ QuerySpec spec , MetaRoot root , ColumnTypeProbe columnTypeProbe ) throws Exception {
96+ // 1. Load the single source object identified by `by`.
97+ Expression byFilter = buildFilter (spec .by (), null );
98+ QueryOptions opts = new QueryOptions ();
99+ if (byFilter != null ) opts .setExpression (byFilter );
100+ Collection <?> sources = omdb .getObjects (conn , sourceMeta , opts );
101+ if (sources .isEmpty ()) return List .of (); // unknown source → no related rows
102+
103+ Object source = sources .iterator ().next ();
104+
105+ // 2. Find the named M:N relationship on the source entity.
106+ MetaRelationship rel = findRelationship (sourceMeta , spec .relation ());
107+ if (rel == null ) {
108+ throw new AssertionError ("op:relate / " + spec .name () + ": no relationship named '"
109+ + spec .relation () + "' on entity '" + sourceMeta .getShortName () + "'" );
110+ }
111+
112+ // 3. Resolve the related targets via the generic OMDB M:N resolver.
113+ Collection <?> related = new M2MResolver (omdb ).resolve (conn , source , sourceMeta , rel , root );
114+
115+ // 4. Normalize the target rows (keyed by the TARGET entity's metadata).
116+ MetaObject targetMeta = mustGetEntity (root , rel .getObjectRef ());
117+ Map <String , Integer > targetColumnSqlTypes = columnTypeProbe .probe (targetMeta );
118+ List <Map <String , Object >> rows = new ArrayList <>(related .size ());
119+ for (Object o : related ) rows .add (toRowMap (targetMeta , o , targetColumnSqlTypes ));
120+ return rows ;
121+ }
122+
123+ private static MetaRelationship findRelationship (MetaObject mc , String relationName ) {
124+ if (relationName == null ) return null ;
125+ for (MetaRelationship rel : mc .getRelationships ()) {
126+ if (relationName .equals (rel .getShortName ())) return rel ;
127+ }
128+ return null ;
129+ }
130+
131+ private static MetaObject mustGetEntity (MetaRoot root , String name ) {
132+ String bare = name ;
133+ int idx = name .lastIndexOf (com .metaobjects .MetaData .PKG_SEPARATOR );
134+ if (idx >= 0 ) bare = name .substring (idx + com .metaobjects .MetaData .PKG_SEPARATOR .length ());
135+ for (MetaObject mc : root .getChildren (MetaObject .class , false )) {
136+ if (bare .equals (mc .getShortName ())) return mc ;
137+ }
138+ throw new IllegalStateException ("Entity '" + name + "' not found in model root" );
139+ }
140+
67141 // -----------------------------------------------------------------------
68142 // Filter translation
69143 // -----------------------------------------------------------------------
0 commit comments