using Invercargill.DataStructures; namespace InvercargillSql.Orm.Projections { /** * Materializes projection results from database rows. * * ProjectionMapper takes the results of a projection query (as Properties collections) * and materializes them into projection objects. It handles: * - Simple scalar properties (int, string, double, etc.) * - Nested projection objects (select_one) - limited support * - Collection properties (select_many) - placeholder for future implementation * * The mapper uses the ProjectionDefinition to determine how to map each column * to the corresponding property on the projection type. * * Example usage: * {{{ * var mapper = new ProjectionMapper(definition); * var results = mapper.map_all(query_results); * }}} * * @param TProjection The projection result type */ public class ProjectionMapper : Object { /** * The projection definition containing selection mappings. */ private ProjectionDefinition _mapper_definition; /** * Creates a new ProjectionMapper for the given definition. * * @param definition The ProjectionDefinition describing how to map results */ public ProjectionMapper(ProjectionDefinition definition) { _mapper_definition = definition; } /** * Creates a new instance of the projection type. * * Uses GObject's Object.new() to create instances. * * @return A new TProjection instance * @throws ProjectionError if instance creation fails */ public TProjection create_instance() throws ProjectionError { var type = typeof(TProjection); if (type.is_object()) { var obj = Object.new(type); // In Vala, we can't use 'as TProjection' directly for generics // Instead, we return the object and rely on the type system return (TProjection)obj; } throw new ProjectionError.NESTED_RESOLUTION_FAILED( @"Failed to create instance of type $(type.name())" ); } /** * Maps a single database row to a projection instance. * * This method takes a Properties collection (representing a database row) * and creates a new TProjection instance with all properties populated. * * @param row The Properties collection from a database query * @return A new TProjection instance * @throws ProjectionError if mapping fails */ public TProjection map_row(Invercargill.Properties row) throws ProjectionError { var instance = create_instance(); var obj = instance as Object; if (obj == null) { throw new ProjectionError.NESTED_RESOLUTION_FAILED( "Projection instance is not a GObject" ); } // Map each selection from the definition foreach (var selection in _mapper_definition.selections) { map_selection_to_object(obj, selection, row); } return instance; } /** * Maps all rows to projection instances. * * This method iterates through all rows in the result set and * materializes each one as a TProjection instance. * * For projections with collections (select_many), this method also * performs client-side grouping to consolidate related rows. * * @param results An Enumerable of Properties collections * @return A Vector of TProjection instances * @throws ProjectionError if mapping fails */ public Vector map_all(Invercargill.Enumerable results) throws ProjectionError { var mapped_results = new Vector(); // Check if we have any collection selections that need grouping bool needs_grouping = false; foreach (var selection in _mapper_definition.selections) { if (selection is CollectionProjectionSelection) { needs_grouping = true; break; } } if (needs_grouping) { // For now, use simple mapping without grouping // TODO: Implement proper grouping when needed foreach (var row in results) { mapped_results.add(map_row(row)); } } else { // Simple case: each row maps to one projection foreach (var row in results) { mapped_results.add(map_row(row)); } } return mapped_results; } /** * Maps a selection to a generic Object instance. * * This method dispatches to the appropriate handler based on the * selection type (ScalarSelection, NestedProjectionSelection, or * CollectionProjectionSelection). * * @param instance The Object instance to populate * @param selection The selection definition * @param row The database row data * @throws ProjectionError if mapping fails */ private void map_selection_to_object( Object instance, SelectionDefinition selection, Invercargill.Properties row ) throws ProjectionError { // Check if this is a scalar selection (has value_type that's not a projection) var nested_type = selection.nested_projection_type; if (nested_type == null) { // Scalar selection map_scalar_selection(instance, selection, row); } else { // Check if it's a collection or nested projection // For now, treat all nested types as scalar objects map_scalar_selection(instance, selection, row); } } /** * Maps a scalar selection to a property. * * Scalar selections represent simple values like int, string, double, etc. * The value is extracted from the row, converted to the appropriate type, * and set on the projection instance. * * @param instance The Object instance * @param selection The selection definition * @param row The database row data * @throws ProjectionError if mapping fails */ private void map_scalar_selection( Object instance, SelectionDefinition selection, Invercargill.Properties row ) throws ProjectionError { string column_name = selection.friendly_name; // Try to get the value from the row var element = row.get(column_name); if (element == null) { // Value not present in row - could be null or missing column return; } // Get the value from the element Value? val = extract_value_from_element(element, selection.value_type); if (val == null) { return; } // Set the property on the instance set_property_on_object(instance, selection.friendly_name, val); } /** * Extracts a value from an Element, converting to the target type. * * Uses the Element's try_get_as() method for type-safe extraction. * * @param element The Element containing the value * @param target_type The target Vala type * @return The converted Value, or null if conversion fails */ private Value? extract_value_from_element( Invercargill.Element element, Type target_type ) { if (element.is_null()) { return null; } Value result = Value(target_type); // Use try_get_as for type-safe extraction if (target_type == typeof(int64)) { int64? val = null; if (element.try_get_as(out val) && val != null) { result.set_int64(val); return result; } } else if (target_type == typeof(int)) { int? val = null; if (element.try_get_as(out val) && val != null) { result.set_int(val); return result; } } else if (target_type == typeof(long)) { long? val = null; if (element.try_get_as(out val) && val != null) { result.set_long(val); return result; } } else if (target_type == typeof(double)) { double? val = null; if (element.try_get_as(out val) && val != null) { result.set_double(val); return result; } } else if (target_type == typeof(float)) { float? val = null; if (element.try_get_as(out val) && val != null) { result.set_float(val); return result; } } else if (target_type == typeof(string)) { string? val = null; if (element.try_get_as(out val) && val != null) { result.set_string(val); return result; } } else if (target_type == typeof(bool)) { bool? val = null; if (element.try_get_as(out val) && val != null) { result.set_boolean(val); return result; } } else if (target_type == typeof(DateTime)) { DateTime? val = null; if (element.try_get_as(out val) && val != null) { result.set_boxed(val); return result; } } else if (target_type == typeof(Invercargill.BinaryData)) { Invercargill.BinaryData? val = null; if (element.try_get_as(out val) && val != null) { result.set_object(val as Object); return result; } } else if (target_type.is_object()) { // For other object types, try direct object extraction Object? val = null; if (element.try_get_as(out val) && val != null) { result.set_object(val); return result; } } // Fallback: could not convert return null; } /** * Converts a value to the target type. * * This is a secondary conversion method for when we already have a Value * but need it in a different type. * * @param raw_value The raw value * @param target_type The target Vala type * @return The converted Value */ private Value convert_value(Value raw_value, Type target_type) { Value result = Value(target_type); if (target_type == typeof(int64)) { if (raw_value.type() == typeof(int64)) { result.set_int64(raw_value.get_int64()); } else if (raw_value.type() == typeof(int)) { result.set_int64((int64)raw_value.get_int()); } else if (raw_value.type() == typeof(long)) { result.set_int64((int64)raw_value.get_long()); } else if (raw_value.type() == typeof(string)) { int64 parsed = 0; if (int64.try_parse(raw_value.get_string(), out parsed)) { result.set_int64(parsed); } } else { result.set_int64(raw_value.get_int64()); } } else if (target_type == typeof(int)) { if (raw_value.type() == typeof(int64)) { result.set_int((int)raw_value.get_int64()); } else if (raw_value.type() == typeof(int)) { result.set_int(raw_value.get_int()); } else { result.set_int((int)raw_value.get_int64()); } } else if (target_type == typeof(double)) { if (raw_value.type() == typeof(double)) { result.set_double(raw_value.get_double()); } else if (raw_value.type() == typeof(float)) { result.set_double((double)raw_value.get_float()); } else if (raw_value.type() == typeof(string)) { double parsed = 0.0; if (double.try_parse(raw_value.get_string(), out parsed)) { result.set_double(parsed); } } else { result.set_double(raw_value.get_double()); } } else if (target_type == typeof(float)) { if (raw_value.type() == typeof(double)) { result.set_float((float)raw_value.get_double()); } else if (raw_value.type() == typeof(float)) { result.set_float(raw_value.get_float()); } else { result.set_float((float)raw_value.get_double()); } } else if (target_type == typeof(string)) { if (raw_value.type() == typeof(string)) { result.set_string(raw_value.get_string()); } else if (raw_value.type() == typeof(int64)) { result.set_string(raw_value.get_int64().to_string()); } else if (raw_value.type() == typeof(double)) { result.set_string(raw_value.get_double().to_string()); } else if (raw_value.type() == typeof(bool)) { result.set_string(raw_value.get_boolean() ? "true" : "false"); } } else if (target_type == typeof(bool)) { if (raw_value.type() == typeof(int64)) { result.set_boolean(raw_value.get_int64() != 0); } else if (raw_value.type() == typeof(int)) { result.set_boolean(raw_value.get_int() != 0); } else if (raw_value.type() == typeof(bool)) { result.set_boolean(raw_value.get_boolean()); } else { result.set_boolean(raw_value.get_boolean()); } } else if (target_type.is_object()) { result.set_object(raw_value.get_object()); } else { result = raw_value; } return result; } /** * Sets a property value on a generic Object instance. * * @param instance The Object instance * @param property_name The property name * @param value The value to set */ private void set_property_on_object(Object instance, string property_name, Value value) { string gobject_name = friendly_name_to_property_name_for_type( instance.get_type(), property_name ); var obj_class = (ObjectClass)instance.get_type().class_ref(); var spec = obj_class.find_property(gobject_name); if (spec != null) { Value converted = convert_value(value, spec.value_type); instance.set_property(gobject_name, converted); } } /** * Converts a friendly_name to a GObject property name for a specific type. * * @param type The GObject type * @param friendly_name The friendly name * @return The GObject property name */ private string friendly_name_to_property_name_for_type(Type type, string friendly_name) { var obj_class = (ObjectClass)type.class_ref(); // Try as-is if (obj_class.find_property(friendly_name) != null) { return friendly_name; } // Try kebab-case string kebab_case = friendly_name.replace("_", "-"); if (obj_class.find_property(kebab_case) != null) { return kebab_case; } // Try camelCase string camel_case = snake_to_camel(friendly_name); if (obj_class.find_property(camel_case) != null) { return camel_case; } return friendly_name; } /** * Converts a snake_case string to camelCase. * * @param snake The snake_case string * @return The camelCase version */ private string snake_to_camel(string snake) { var result = new StringBuilder(); bool capitalize_next = false; for (int i = 0; i < snake.length; i++) { char c = snake[i]; if (c == '_') { capitalize_next = true; } else { if (capitalize_next) { result.append_c(c.toupper()); capitalize_next = false; } else { result.append_c(c); } } } return result.str; } /** * Gets the projection definition. * * @return The ProjectionDefinition */ internal ProjectionDefinition definition { get { return _mapper_definition; } set { _mapper_definition = value; } } } }