friendly-name-resolver.vala 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. using Invercargill.DataStructures;
  2. using Invercargill.Expressions;
  3. namespace InvercargillSql.Orm.Projections {
  4. /**
  5. * Result of resolving a nested property path.
  6. *
  7. * When navigating into nested projections (e.g., "profile.id"), this class
  8. * holds information about the nested selection and the resolved path.
  9. *
  10. * Example:
  11. * {{{
  12. * // For a projection with:
  13. * // .select<ProfileSummary>("profile", expr("pr"), ...)
  14. *
  15. * var resolution = resolver.resolve_nested_property("profile.id");
  16. * // resolution.selection refers to the ProfileSummary selection
  17. * // resolution.remaining_path == "id"
  18. * // resolution.resolved_expression is an Expression representing "pr.id"
  19. * }}}
  20. */
  21. public class NestedResolution : Object {
  22. /**
  23. * The selection definition for the nested projection.
  24. *
  25. * This is the selection that corresponds to the first segment
  26. * of the property path (e.g., "profile" in "profile.id").
  27. */
  28. public SelectionDefinition selection { get; construct; }
  29. /**
  30. * The type of the nested projection.
  31. *
  32. * For NestedProjectionSelection, this is the TNested type.
  33. * For CollectionProjectionSelection, this is the TItem type.
  34. */
  35. public Type nested_projection_type { get; construct; }
  36. /**
  37. * The remaining path after the first segment.
  38. *
  39. * For "profile.id", this would be "id".
  40. * For "profile.address.city", this would be "address.city".
  41. */
  42. public string remaining_path { get; construct; }
  43. /**
  44. * The first segment of the path (the friendly name).
  45. *
  46. * For "profile.id", this would be "profile".
  47. */
  48. public string first_segment { get; construct; }
  49. /**
  50. * The final resolved expression, if available.
  51. *
  52. * This may be null if the resolution only identified the nested
  53. * selection but did not fully resolve the property within it.
  54. *
  55. * This is an Expression object that can be used directly in
  56. * expression trees or converted to string using ExpressionStringVisitor.
  57. */
  58. public Expression? resolved_expression { get; construct; }
  59. /**
  60. * Indicates whether this is a nested projection (1:1).
  61. */
  62. public bool is_single_nested { get; construct; }
  63. /**
  64. * Indicates whether this is a collection projection (1:N).
  65. */
  66. public bool is_collection { get; construct; }
  67. /**
  68. * Creates a new NestedResolution.
  69. *
  70. * @param selection The nested selection definition
  71. * @param nested_projection_type The type of the nested projection
  72. * @param remaining_path The path after the first segment
  73. * @param first_segment The first segment (friendly name)
  74. * @param resolved_expression The fully resolved Expression (optional)
  75. * @param is_single_nested True if this is a 1:1 nested projection
  76. * @param is_collection True if this is a 1:N collection projection
  77. */
  78. public NestedResolution(
  79. SelectionDefinition selection,
  80. Type nested_projection_type,
  81. string remaining_path,
  82. string first_segment,
  83. Expression? resolved_expression,
  84. bool is_single_nested,
  85. bool is_collection
  86. ) {
  87. Object(
  88. selection: selection,
  89. nested_projection_type: nested_projection_type,
  90. remaining_path: remaining_path,
  91. first_segment: first_segment,
  92. resolved_expression: resolved_expression,
  93. is_single_nested: is_single_nested,
  94. is_collection: is_collection
  95. );
  96. }
  97. }
  98. /**
  99. * Resolves friendly names to Expression objects and handles nested navigation.
  100. *
  101. * FriendlyNameResolver provides lookup services for the friendly names
  102. * defined in a projection. These names are used in WHERE and ORDER BY
  103. * expressions to reference selection values.
  104. *
  105. * The resolver also handles nested navigation, allowing expressions like
  106. * "profile.id" to reference properties within nested projections.
  107. *
  108. * Example:
  109. * {{{
  110. * // For a projection with:
  111. * // .select<int64>("user_id", expr("r.id"), ...)
  112. * // .select<ProfileSummary>("profile", expr("pr"), ...)
  113. *
  114. * var resolver = new FriendlyNameResolver(definition);
  115. *
  116. * // Simple resolution - returns Expression object
  117. * Expression? expr = resolver.resolve_to_expression("user_id");
  118. *
  119. * // Nested navigation
  120. * var nested = resolver.resolve_nested_property("profile.id");
  121. * // nested.first_segment == "profile"
  122. * // nested.remaining_path == "id"
  123. *
  124. * // Check if name exists
  125. * resolver.is_friendly_name("user_id") // true
  126. * }}}
  127. */
  128. public class FriendlyNameResolver : Object {
  129. /**
  130. * The projection definition this resolver is associated with.
  131. */
  132. private ProjectionDefinition _definition;
  133. /**
  134. * Cache of friendly name to Expression mappings.
  135. * Built lazily on first access.
  136. */
  137. private Dictionary<string, Expression>? _friendly_name_cache;
  138. /**
  139. * Cache of friendly name to selection mappings.
  140. * Built lazily on first access.
  141. */
  142. private Dictionary<string, SelectionDefinition>? _selection_cache;
  143. /**
  144. * Cache of all friendly names including nested ones.
  145. * Built lazily on first access.
  146. */
  147. private Vector<string>? _all_names_cache;
  148. /**
  149. * Creates a new FriendlyNameResolver for the given projection.
  150. *
  151. * @param definition The projection definition to resolve names for
  152. */
  153. public FriendlyNameResolver(ProjectionDefinition definition) {
  154. _definition = definition;
  155. }
  156. /**
  157. * Resolves a friendly name to its underlying Expression.
  158. *
  159. * For scalar selections, this returns the Expression defined in
  160. * the .select() call. For nested projections, this returns the
  161. * entry point expression.
  162. *
  163. * @param friendly_name The friendly name to resolve
  164. * @return The underlying Expression, or null if not found
  165. */
  166. public Expression? resolve_to_expression(string friendly_name) {
  167. ensure_cache_built();
  168. // First check if it's a direct friendly name
  169. // Use has() before get() to avoid exception on missing key
  170. if (_friendly_name_cache.has(friendly_name)) {
  171. return _friendly_name_cache.get(friendly_name);
  172. }
  173. // Check if it's a nested path (e.g., "profile.id")
  174. int dot_index = friendly_name.index_of(".");
  175. if (dot_index > 0) {
  176. // Find the nested selection
  177. var nested = resolve_nested_property(friendly_name);
  178. if (nested != null) {
  179. return nested.resolved_expression;
  180. }
  181. }
  182. return null;
  183. }
  184. /**
  185. * Resolves a nested property path.
  186. *
  187. * This method handles paths like "profile.id" or "address.city.name"
  188. * by finding the nested selection and determining the remaining path.
  189. *
  190. * @param property_path The full property path (e.g., "profile.id")
  191. * @return A NestedResolution with details, or null if not found
  192. */
  193. public NestedResolution? resolve_nested_property(string property_path) {
  194. ensure_cache_built();
  195. int dot_index = property_path.index_of(".");
  196. if (dot_index < 0) {
  197. // Not a nested path
  198. return null;
  199. }
  200. string first_segment = property_path.substring(0, dot_index);
  201. string remaining_path = property_path.substring(dot_index + 1);
  202. // Find the selection for the first segment
  203. SelectionDefinition? selection = _selection_cache[first_segment];
  204. if (selection == null) {
  205. return null;
  206. }
  207. // Check if it's a nested or collection projection
  208. Type? nested_type = selection.nested_projection_type;
  209. if (nested_type == null) {
  210. // Not a nested projection - can't navigate further
  211. return null;
  212. }
  213. // Determine if it's a single nested or collection by checking type name
  214. string type_name = selection.get_type().name();
  215. bool is_single = type_name.contains("NestedProjectionSelection");
  216. bool is_collection = type_name.contains("CollectionProjectionSelection");
  217. // Get the entry point expression for the nested selection
  218. Expression? entry_point = get_entry_point_from_selection(selection);
  219. // Build the resolved expression by combining entry point with remaining path
  220. Expression? resolved_expr = null;
  221. if (entry_point != null && remaining_path.length > 0) {
  222. // Create a property chain expression: entry_point.remaining_path
  223. resolved_expr = create_property_chain(entry_point, remaining_path);
  224. } else if (entry_point != null) {
  225. resolved_expr = entry_point;
  226. }
  227. return new NestedResolution(
  228. selection,
  229. nested_type,
  230. remaining_path,
  231. first_segment,
  232. resolved_expr,
  233. is_single,
  234. is_collection
  235. );
  236. }
  237. /**
  238. * Checks if a name is a known friendly name.
  239. *
  240. * This checks only top-level friendly names, not nested paths.
  241. *
  242. * @param name The name to check
  243. * @return True if the name is a defined friendly name
  244. */
  245. public bool is_friendly_name(string name) {
  246. ensure_cache_built();
  247. // Check for exact match first
  248. if (_selection_cache.has(name)) {
  249. return true;
  250. }
  251. // Check if it's a nested path with a valid first segment
  252. int dot_index = name.index_of(".");
  253. if (dot_index > 0) {
  254. string first_segment = name.substring(0, dot_index);
  255. return _selection_cache.has(first_segment);
  256. }
  257. return false;
  258. }
  259. /**
  260. * Gets all friendly names defined in this projection.
  261. *
  262. * This includes both scalar and nested projection selections,
  263. * but does not include properties within nested projections.
  264. *
  265. * @return A vector of all friendly names
  266. */
  267. public Vector<string> get_all_friendly_names() {
  268. ensure_cache_built();
  269. var names = new Vector<string>();
  270. foreach (var selection in _definition.selections) {
  271. names.add(selection.friendly_name);
  272. }
  273. return names;
  274. }
  275. /**
  276. * Gets all friendly names including nested property paths.
  277. *
  278. * This method recursively explores nested projections to build
  279. * a complete list of navigable property paths.
  280. *
  281. * Note: This requires the nested projections to be registered
  282. * in the session to fully expand their properties.
  283. *
  284. * @param session Optional session for looking up nested projection definitions
  285. * @return A vector of all friendly names including nested paths
  286. */
  287. public Vector<string> get_all_friendly_names_recursive(
  288. InvercargillSql.Orm.OrmSession? session = null
  289. ) {
  290. ensure_cache_built();
  291. var all_names = new Vector<string>();
  292. foreach (var selection in _definition.selections) {
  293. all_names.add(selection.friendly_name);
  294. // If this is a nested projection, explore its properties
  295. if (selection.nested_projection_type != null && session != null) {
  296. add_nested_names(selection, selection.friendly_name, session, all_names, 0);
  297. }
  298. }
  299. return all_names;
  300. }
  301. /**
  302. * Gets the selection definition for a friendly name.
  303. *
  304. * @param friendly_name The friendly name to look up
  305. * @return The selection definition, or null if not found
  306. */
  307. public SelectionDefinition? get_selection(string friendly_name) {
  308. ensure_cache_built();
  309. // Handle nested paths
  310. int dot_index = friendly_name.index_of(".");
  311. if (dot_index > 0) {
  312. string first_segment = friendly_name.substring(0, dot_index);
  313. if (_selection_cache.has(first_segment)) {
  314. return _selection_cache[first_segment];
  315. }
  316. return null;
  317. }
  318. if (_selection_cache.has(friendly_name)) {
  319. return _selection_cache[friendly_name];
  320. }
  321. return null;
  322. }
  323. /**
  324. * Checks if a friendly name refers to a nested projection.
  325. *
  326. * @param friendly_name The friendly name to check
  327. * @return True if it's a nested projection selection
  328. */
  329. public bool is_nested_projection(string friendly_name) {
  330. var selection = get_selection(friendly_name);
  331. if (selection == null) {
  332. return false;
  333. }
  334. return selection.nested_projection_type != null;
  335. }
  336. /**
  337. * Checks if a friendly name refers to a collection projection.
  338. *
  339. * @param friendly_name The friendly name to check
  340. * @return True if it's a collection projection selection
  341. */
  342. public bool is_collection_projection(string friendly_name) {
  343. var selection = get_selection(friendly_name);
  344. if (selection == null) {
  345. return false;
  346. }
  347. string type_name = selection.get_type().name();
  348. return type_name.contains("CollectionProjectionSelection");
  349. }
  350. /**
  351. * Gets the type of value for a friendly name.
  352. *
  353. * @param friendly_name The friendly name to look up
  354. * @return The value type, or null if not found
  355. */
  356. public Type? get_value_type(string friendly_name) {
  357. var selection = get_selection(friendly_name);
  358. if (selection == null) {
  359. return null;
  360. }
  361. return selection.value_type;
  362. }
  363. /**
  364. * Ensures the internal caches are built.
  365. */
  366. private void ensure_cache_built() {
  367. if (_friendly_name_cache != null && _selection_cache != null) {
  368. return;
  369. }
  370. _friendly_name_cache = new Dictionary<string, Expression>();
  371. _selection_cache = new Dictionary<string, SelectionDefinition>();
  372. foreach (var selection in _definition.selections) {
  373. _selection_cache[selection.friendly_name] = selection;
  374. // Get the expression for the selection
  375. Expression? expr = get_expression_from_selection(selection);
  376. if (expr != null) {
  377. _friendly_name_cache[selection.friendly_name] = expr;
  378. }
  379. }
  380. }
  381. /**
  382. * Gets the Expression from a selection using reflection.
  383. *
  384. * Uses GObject's property system to access the expression property
  385. * without knowing the generic type parameters.
  386. *
  387. * @param selection The selection to get the expression from
  388. * @return The Expression object, or null if not applicable
  389. */
  390. private Expression? get_expression_from_selection(SelectionDefinition selection) {
  391. // Try to get "expression" property (ScalarSelection)
  392. var param = selection.get_class().find_property("expression");
  393. if (param != null) {
  394. Value val = Value(typeof(Object));
  395. selection.get_property("expression", ref val);
  396. return val.get_object() as Expression;
  397. }
  398. // Try to get "entry-point-expression" property (nested/collection)
  399. param = selection.get_class().find_property("entry-point-expression");
  400. if (param != null) {
  401. Value val = Value(typeof(Object));
  402. selection.get_property("entry-point-expression", ref val);
  403. return val.get_object() as Expression;
  404. }
  405. return null;
  406. }
  407. /**
  408. * Gets the entry point Expression from a nested/collection selection.
  409. *
  410. * @param selection The selection to get the entry point from
  411. * @return The entry point Expression, or null if not applicable
  412. */
  413. private Expression? get_entry_point_from_selection(SelectionDefinition selection) {
  414. var param = selection.get_class().find_property("entry-point-expression");
  415. if (param != null) {
  416. Value val = Value(typeof(Object));
  417. selection.get_property("entry-point-expression", ref val);
  418. return val.get_object() as Expression;
  419. }
  420. return null;
  421. }
  422. /**
  423. * Creates a property chain expression from a base expression and path.
  424. *
  425. * Given a base expression (e.g., VariableExpression("pr")) and a path
  426. * (e.g., "id"), creates a chain of PropertyExpressions.
  427. *
  428. * For "pr.id", creates: PropertyExpression(VariableExpression("pr"), "id")
  429. * For "pr.address.city", creates nested PropertyExpressions.
  430. *
  431. * @param base_expr The base expression to build upon
  432. * @param path The property path (dot-separated)
  433. * @return A new Expression representing the property chain
  434. */
  435. private Expression create_property_chain(Expression base_expr, string path) {
  436. // Split the path by dots and build property chain
  437. string[] parts = path.split(".");
  438. Expression current = base_expr;
  439. foreach (string part in parts) {
  440. if (part.length > 0) {
  441. current = new PropertyExpression(current, part);
  442. }
  443. }
  444. return current;
  445. }
  446. /**
  447. * Recursively adds nested friendly names.
  448. */
  449. private void add_nested_names(
  450. SelectionDefinition selection,
  451. string prefix,
  452. InvercargillSql.Orm.OrmSession session,
  453. Vector<string> names,
  454. int depth
  455. ) {
  456. // Prevent infinite recursion
  457. if (depth > 10) {
  458. return;
  459. }
  460. // We can't easily look up nested projection definitions without
  461. // the actual generic types, so we just add the prefix itself
  462. // In a full implementation, we would look up the nested projection
  463. // definition and add its selections with the prefix
  464. }
  465. }
  466. }