public override (string, QxType, QxNullity) CompileNative(QxCompilationContext ctx) { // Strings must be added in parameters to prevent SQL injection vulnerability var parameterName = ctx.Parameters.AddParameter(Value); var sql = $"@{parameterName}"; return(sql, QxType.String, QxNullity.NotNull); }
public override bool TryCompile(QxType targetType, QxCompilationContext ctx, out string resultSql, out QxNullity resultNullity) { switch (targetType) { case QxType.Date: if (DateTime.TryParse(Value, out DateTime d)) { d = d.Date; // Remove the time component string varDef = $"N'{d:yyyy-MM-dd}'"; string varName = ctx.Variables.AddVariable("DATE", varDef); resultSql = $"@{varName}"; resultNullity = QxNullity.NotNull; return(true); } else { resultSql = null; resultNullity = default; return(false); } case QxType.DateTime: if (DateTime.TryParse(Value, out DateTime dt)) { string varDef = $"N'{dt:yyyy-MM-ddTHH:mm:ss.ff}'"; string varName = ctx.Variables.AddVariable("DATETIME2(2)", varDef); resultSql = $"@{varName}"; resultNullity = QxNullity.NotNull; return(true); } else { resultSql = null; resultNullity = default; return(false); } case QxType.DateTimeOffset: if (DateTimeOffset.TryParse(Value, out DateTimeOffset dto)) { string varDef = $"N'{dto:o}'"; string varName = ctx.Variables.AddVariable("DATETIMEOFFSET(7)", varDef); resultSql = $"@{varName}"; resultNullity = QxNullity.NotNull; return(true); } else { resultSql = null; resultNullity = default; return(false); } } return(base.TryCompile(targetType, ctx, out resultSql, out resultNullity)); }
/// <summary> /// Compiles the expression to the first <see cref="QxType"/> other than boolean that it can be compiled to. /// </summary> public string CompileToNonBoolean(QxCompilationContext ctx) { var(nativeSql, nativeType, _) = CompileNative(ctx); if (nativeType == QxType.Boolean) { throw new QueryException($"Expression {this} cannot be a {QxType.Boolean}."); } return(nativeSql); }
/// <summary> /// Compiles the expression to a boolean SQL, throws an exception if the expression cannot be compiled to boolean. /// </summary> public string CompileToBoolean(QxCompilationContext ctx) { if (TryCompile(QxType.Boolean, ctx, out string result, out QxNullity nullity)) { if (nullity != QxNullity.NotNull) { // Developer mistake throw new InvalidOperationException($"[Bug] nullable boolean expression {this}."); } return(result); }
public override bool TryCompile(QxType targetType, QxCompilationContext ctx, out string resultSql, out QxNullity resultNullity) { string nameLower = Name?.ToLower(); switch (nameLower) { case "min": case "max": { var(arg1, conditionSql) = AggregationParameters(ctx); if (arg1.TryCompile(targetType, ctx, out string expSql, out resultNullity)) { resultSql = AggregationCompile(expSql, conditionSql); return(true); }
public override bool TryCompile(QxType targetType, QxCompilationContext ctx, out string resultSql, out QxNullity resultNullity) { // This here is merely an optimization if (Operator == "+") { if ((targetType == QxType.String && // String concatenation Left.TryCompile(QxType.String, ctx, out string leftSql, out QxNullity leftNullity) && Right.TryCompile(QxType.String, ctx, out string rightSql, out QxNullity rightNullity)) || (targetType == QxType.Numeric && // Numeric Left.TryCompile(QxType.Numeric, ctx, out leftSql, out leftNullity) && Right.TryCompile(QxType.Numeric, ctx, out rightSql, out rightNullity))) { resultNullity = leftNullity | rightNullity; resultSql = $"({leftSql} + {rightSql})"; return(true); } else { // No other types are possible for + resultNullity = default; resultSql = null; return(false); } }
public override (string sql, QxType type, QxNullity nullity) CompileNative(QxCompilationContext ctx) { // Note: The way the logic is structured assumes that for ALL operators // if either operand is NULL, then the result is NULL // Convenience variables string operandSql; QxNullity operandNullity; // The result string resultSql; QxType resultType; QxNullity resultNullity; string opLower = Operator?.ToLower(); switch (opLower) { case "+": case "-": // + maye be either addition or string concatenation // The output type is uniquely determined by the input types (context doesn't matter) // since there is no implicit cast from numeric to string or vice versa { if (!Operand.TryCompile(QxType.Numeric, ctx, out operandSql, out operandNullity)) { throw new QueryException($"Operator '{Operator}': Operand {Operand} could not be interpreted as {QxType.Numeric}."); } // Addition resultType = QxType.Numeric; resultNullity = operandNullity; if (opLower == "+") { resultSql = operandSql; // +ve sign (does not do anything) } else if (opLower == "-") { resultSql = $"(-{operandSql})"; // -ve sign } else { // Developer mistake throw new InvalidOperationException($"Unknown unary arithmetic operator {opLower}."); } break; } case "!": case "not": // These only accept booleans and return a boolean { resultType = QxType.Boolean; if (!Operand.TryCompile(QxType.Boolean, ctx, out operandSql, out operandNullity)) { throw new QueryException($"Operator '{Operator}': Operand {Operand} could not be interpreted as {QxType.Boolean}."); } resultNullity = operandNullity; if (resultNullity != QxNullity.NotNull) { // Developer mistake throw new InvalidOperationException($"[Bug] A nullable boolean expression: {this}"); } if (operandSql == FALSE) { resultSql = TRUE; } else if (operandSql == TRUE) { resultSql = FALSE; } else { resultSql = $"(NOT {operandSql})"; } break; } default: // Developer mistake throw new InvalidOperationException($"Unknown unary operator {Operator}"); // Future proofing } // Return the result (or NULL if that's the only possible value) if (resultNullity == QxNullity.Null) { resultSql = "NULL"; } return(resultSql, resultType, resultNullity); }
public override (string, QxType, QxNullity) CompileNative(QxCompilationContext ctx) { if (string.IsNullOrWhiteSpace(Property)) { // Developer mistake throw new InvalidOperationException($"Bug: Invoking {nameof(CompileNative)} on a {nameof(QueryexColumnAccess)} that does not have a property."); } var propName = Property; // (A) Calculate Nullity (the entire path foreign keys + the final property must be all NOT NULL) bool pathNotNull = true; var join = ctx.Joins; foreach (var step in Path) { var navPropDesc = join.EntityDescriptor.NavigationProperty(step); pathNotNull = pathNotNull && navPropDesc.ForeignKey.IsNotNull; join = join[step]; } var propDesc = join.EntityDescriptor.Property(propName); if (propDesc == null) { throw new QueryException($"Property '{propName}' does not exist on type {join.EntityDescriptor.Name}."); } QxNullity nullity = pathNotNull && propDesc.IsNotNull ? QxNullity.NotNull : QxNullity.Nullable; // (B) Calculate the type QxType type; var propType = Nullable.GetUnderlyingType(propDesc.Type) ?? propDesc.Type; switch (propType.Name) { case nameof(Char): case nameof(String): type = QxType.String; break; case nameof(Byte): case nameof(SByte): case nameof(Int16): case nameof(UInt16): case nameof(Int32): case nameof(UInt32): case nameof(Int64): case nameof(UInt64): case nameof(Single): case nameof(Double): case nameof(Decimal): type = QxType.Numeric; break; case nameof(Boolean): type = QxType.Bit; break; case nameof(DateTime): type = propDesc.IncludesTime ? QxType.DateTime : QxType.Date; break; case nameof(DateTimeOffset): type = QxType.DateTimeOffset; break; case nameof(HierarchyId): type = QxType.HierarchyId; break; case nameof(Geography): type = QxType.Geography; break; default: if (propDesc is NavigationPropertyDescriptor || propDesc is CollectionPropertyDescriptor) { throw new QueryException($"A column access cannot terminate with a navigation property like {propDesc.Name}."); } else { // Developer mistake throw new InvalidOperationException($"[Bug] Could not map type {propType.Name} to a {nameof(QxType)}"); // Future proofing } } // (C) Calculate the SQL var sql = $"[{join.Symbol}].[{propName}]"; // Return the result return(sql, type, nullity); }