using Invercargill.DataStructures; using Invercargill.Expressions; namespace InvercargillSql.Orm.Projections { /** * Result of resolving a nested property path. * * When navigating into nested projections (e.g., "profile.id"), this class * holds information about the nested selection and the resolved path. * * Example: * {{{ * // For a projection with: * // .select("profile", expr("pr"), ...) * * var resolution = resolver.resolve_nested_property("profile.id"); * // resolution.selection refers to the ProfileSummary selection * // resolution.remaining_path == "id" * // resolution.resolved_expression is an Expression representing "pr.id" * }}} */ public class NestedResolution : Object { /** * The selection definition for the nested projection. * * This is the selection that corresponds to the first segment * of the property path (e.g., "profile" in "profile.id"). */ public SelectionDefinition selection { get; construct; } /** * The type of the nested projection. * * For NestedProjectionSelection, this is the TNested type. * For CollectionProjectionSelection, this is the TItem type. */ public Type nested_projection_type { get; construct; } /** * The remaining path after the first segment. * * For "profile.id", this would be "id". * For "profile.address.city", this would be "address.city". */ public string remaining_path { get; construct; } /** * The first segment of the path (the friendly name). * * For "profile.id", this would be "profile". */ public string first_segment { get; construct; } /** * The final resolved expression, if available. * * This may be null if the resolution only identified the nested * selection but did not fully resolve the property within it. * * This is an Expression object that can be used directly in * expression trees or converted to string using ExpressionStringVisitor. */ public Expression? resolved_expression { get; construct; } /** * Indicates whether this is a nested projection (1:1). */ public bool is_single_nested { get; construct; } /** * Indicates whether this is a collection projection (1:N). */ public bool is_collection { get; construct; } /** * Creates a new NestedResolution. * * @param selection The nested selection definition * @param nested_projection_type The type of the nested projection * @param remaining_path The path after the first segment * @param first_segment The first segment (friendly name) * @param resolved_expression The fully resolved Expression (optional) * @param is_single_nested True if this is a 1:1 nested projection * @param is_collection True if this is a 1:N collection projection */ public NestedResolution( SelectionDefinition selection, Type nested_projection_type, string remaining_path, string first_segment, Expression? resolved_expression, bool is_single_nested, bool is_collection ) { Object( selection: selection, nested_projection_type: nested_projection_type, remaining_path: remaining_path, first_segment: first_segment, resolved_expression: resolved_expression, is_single_nested: is_single_nested, is_collection: is_collection ); } } /** * Resolves friendly names to Expression objects and handles nested navigation. * * FriendlyNameResolver provides lookup services for the friendly names * defined in a projection. These names are used in WHERE and ORDER BY * expressions to reference selection values. * * The resolver also handles nested navigation, allowing expressions like * "profile.id" to reference properties within nested projections. * * Example: * {{{ * // For a projection with: * // .select("user_id", expr("r.id"), ...) * // .select("profile", expr("pr"), ...) * * var resolver = new FriendlyNameResolver(definition); * * // Simple resolution - returns Expression object * Expression? expr = resolver.resolve_to_expression("user_id"); * * // Nested navigation * var nested = resolver.resolve_nested_property("profile.id"); * // nested.first_segment == "profile" * // nested.remaining_path == "id" * * // Check if name exists * resolver.is_friendly_name("user_id") // true * }}} */ public class FriendlyNameResolver : Object { /** * The projection definition this resolver is associated with. */ private ProjectionDefinition _definition; /** * Cache of friendly name to Expression mappings. * Built lazily on first access. */ private Dictionary? _friendly_name_cache; /** * Cache of friendly name to selection mappings. * Built lazily on first access. */ private Dictionary? _selection_cache; /** * Cache of all friendly names including nested ones. * Built lazily on first access. */ private Vector? _all_names_cache; /** * Creates a new FriendlyNameResolver for the given projection. * * @param definition The projection definition to resolve names for */ public FriendlyNameResolver(ProjectionDefinition definition) { _definition = definition; } /** * Resolves a friendly name to its underlying Expression. * * For scalar selections, this returns the Expression defined in * the .select() call. For nested projections, this returns the * entry point expression. * * @param friendly_name The friendly name to resolve * @return The underlying Expression, or null if not found */ public Expression? resolve_to_expression(string friendly_name) { ensure_cache_built(); // First check if it's a direct friendly name // Use has() before get() to avoid exception on missing key if (_friendly_name_cache.has(friendly_name)) { return _friendly_name_cache.get(friendly_name); } // Check if it's a nested path (e.g., "profile.id") int dot_index = friendly_name.index_of("."); if (dot_index > 0) { // Find the nested selection var nested = resolve_nested_property(friendly_name); if (nested != null) { return nested.resolved_expression; } } return null; } /** * Resolves a nested property path. * * This method handles paths like "profile.id" or "address.city.name" * by finding the nested selection and determining the remaining path. * * @param property_path The full property path (e.g., "profile.id") * @return A NestedResolution with details, or null if not found */ public NestedResolution? resolve_nested_property(string property_path) { ensure_cache_built(); int dot_index = property_path.index_of("."); if (dot_index < 0) { // Not a nested path return null; } string first_segment = property_path.substring(0, dot_index); string remaining_path = property_path.substring(dot_index + 1); // Find the selection for the first segment SelectionDefinition? selection = _selection_cache[first_segment]; if (selection == null) { return null; } // Check if it's a nested or collection projection Type? nested_type = selection.nested_projection_type; if (nested_type == null) { // Not a nested projection - can't navigate further return null; } // Determine if it's a single nested or collection by checking type name string type_name = selection.get_type().name(); bool is_single = type_name.contains("NestedProjectionSelection"); bool is_collection = type_name.contains("CollectionProjectionSelection"); // Get the entry point expression for the nested selection Expression? entry_point = get_entry_point_from_selection(selection); // Build the resolved expression by combining entry point with remaining path Expression? resolved_expr = null; if (entry_point != null && remaining_path.length > 0) { // Create a property chain expression: entry_point.remaining_path resolved_expr = create_property_chain(entry_point, remaining_path); } else if (entry_point != null) { resolved_expr = entry_point; } return new NestedResolution( selection, nested_type, remaining_path, first_segment, resolved_expr, is_single, is_collection ); } /** * Checks if a name is a known friendly name. * * This checks only top-level friendly names, not nested paths. * * @param name The name to check * @return True if the name is a defined friendly name */ public bool is_friendly_name(string name) { ensure_cache_built(); // Check for exact match first if (_selection_cache.has(name)) { return true; } // Check if it's a nested path with a valid first segment int dot_index = name.index_of("."); if (dot_index > 0) { string first_segment = name.substring(0, dot_index); return _selection_cache.has(first_segment); } return false; } /** * Gets all friendly names defined in this projection. * * This includes both scalar and nested projection selections, * but does not include properties within nested projections. * * @return A vector of all friendly names */ public Vector get_all_friendly_names() { ensure_cache_built(); var names = new Vector(); foreach (var selection in _definition.selections) { names.add(selection.friendly_name); } return names; } /** * Gets all friendly names including nested property paths. * * This method recursively explores nested projections to build * a complete list of navigable property paths. * * Note: This requires the nested projections to be registered * in the session to fully expand their properties. * * @param session Optional session for looking up nested projection definitions * @return A vector of all friendly names including nested paths */ public Vector get_all_friendly_names_recursive( InvercargillSql.Orm.OrmSession? session = null ) { ensure_cache_built(); var all_names = new Vector(); foreach (var selection in _definition.selections) { all_names.add(selection.friendly_name); // If this is a nested projection, explore its properties if (selection.nested_projection_type != null && session != null) { add_nested_names(selection, selection.friendly_name, session, all_names, 0); } } return all_names; } /** * Gets the selection definition for a friendly name. * * @param friendly_name The friendly name to look up * @return The selection definition, or null if not found */ public SelectionDefinition? get_selection(string friendly_name) { ensure_cache_built(); // Handle nested paths int dot_index = friendly_name.index_of("."); if (dot_index > 0) { string first_segment = friendly_name.substring(0, dot_index); if (_selection_cache.has(first_segment)) { return _selection_cache[first_segment]; } return null; } if (_selection_cache.has(friendly_name)) { return _selection_cache[friendly_name]; } return null; } /** * Checks if a friendly name refers to a nested projection. * * @param friendly_name The friendly name to check * @return True if it's a nested projection selection */ public bool is_nested_projection(string friendly_name) { var selection = get_selection(friendly_name); if (selection == null) { return false; } return selection.nested_projection_type != null; } /** * Checks if a friendly name refers to a collection projection. * * @param friendly_name The friendly name to check * @return True if it's a collection projection selection */ public bool is_collection_projection(string friendly_name) { var selection = get_selection(friendly_name); if (selection == null) { return false; } string type_name = selection.get_type().name(); return type_name.contains("CollectionProjectionSelection"); } /** * Gets the type of value for a friendly name. * * @param friendly_name The friendly name to look up * @return The value type, or null if not found */ public Type? get_value_type(string friendly_name) { var selection = get_selection(friendly_name); if (selection == null) { return null; } return selection.value_type; } /** * Ensures the internal caches are built. */ private void ensure_cache_built() { if (_friendly_name_cache != null && _selection_cache != null) { return; } _friendly_name_cache = new Dictionary(); _selection_cache = new Dictionary(); foreach (var selection in _definition.selections) { _selection_cache[selection.friendly_name] = selection; // Get the expression for the selection Expression? expr = get_expression_from_selection(selection); if (expr != null) { _friendly_name_cache[selection.friendly_name] = expr; } } } /** * Gets the Expression from a selection using reflection. * * Uses GObject's property system to access the expression property * without knowing the generic type parameters. * * @param selection The selection to get the expression from * @return The Expression object, or null if not applicable */ private Expression? get_expression_from_selection(SelectionDefinition selection) { // Try to get "expression" property (ScalarSelection) var param = selection.get_class().find_property("expression"); if (param != null) { Value val = Value(typeof(Object)); selection.get_property("expression", ref val); return val.get_object() as Expression; } // Try to get "entry-point-expression" property (nested/collection) param = selection.get_class().find_property("entry-point-expression"); if (param != null) { Value val = Value(typeof(Object)); selection.get_property("entry-point-expression", ref val); return val.get_object() as Expression; } return null; } /** * Gets the entry point Expression from a nested/collection selection. * * @param selection The selection to get the entry point from * @return The entry point Expression, or null if not applicable */ private Expression? get_entry_point_from_selection(SelectionDefinition selection) { var param = selection.get_class().find_property("entry-point-expression"); if (param != null) { Value val = Value(typeof(Object)); selection.get_property("entry-point-expression", ref val); return val.get_object() as Expression; } return null; } /** * Creates a property chain expression from a base expression and path. * * Given a base expression (e.g., VariableExpression("pr")) and a path * (e.g., "id"), creates a chain of PropertyExpressions. * * For "pr.id", creates: PropertyExpression(VariableExpression("pr"), "id") * For "pr.address.city", creates nested PropertyExpressions. * * @param base_expr The base expression to build upon * @param path The property path (dot-separated) * @return A new Expression representing the property chain */ private Expression create_property_chain(Expression base_expr, string path) { // Split the path by dots and build property chain string[] parts = path.split("."); Expression current = base_expr; foreach (string part in parts) { if (part.length > 0) { current = new PropertyExpression(current, part); } } return current; } /** * Recursively adds nested friendly names. */ private void add_nested_names( SelectionDefinition selection, string prefix, InvercargillSql.Orm.OrmSession session, Vector names, int depth ) { // Prevent infinite recursion if (depth > 10) { return; } // We can't easily look up nested projection definitions without // the actual generic types, so we just add the prefix itself // In a full implementation, we would look up the nested projection // definition and add its selections with the prefix } } }