using Connected.Expressions.Translation; using Connected.Expressions.Translation.Resolvers; using Connected.Expressions.TypeSystem; using Connected.Interop; using System.Collections; using System.Linq.Expressions; using System.Reflection; namespace Connected.Expressions.Languages; public abstract class QueryLanguage { private const string AggregateCount = "Count"; private const string AggregateLongCount = "LongCount"; private const string AggregateSum = "Sum"; private const string AggregateMin = "Min"; private const string AggregateMax = "Max"; private const string AggregateAverage = "Average"; public virtual bool AllowDistinctInAggregates => false; public abstract QueryTypeSystem TypeSystem { get; } public virtual bool AllowsMultipleCommands => false; public virtual bool AllowSubqueryInSelectWithoutFrom => false; public virtual Expression GetRowsAffectedExpression(Expression command) { return new FunctionExpression(typeof(int), "@@ROWCOUNT", null); } public virtual bool IsRowsAffectedExpressions(Expression expression) { var fex = expression as FunctionExpression; return fex is not null && string.Equals(fex.Name, "@@ROWCOUNT", StringComparison.OrdinalIgnoreCase); } public virtual string Quote(string name) { return name; } public virtual bool IsAggregate(MemberInfo member) { var method = member as MethodInfo; if (method is not null) { if (method.DeclaringType == typeof(Queryable) || method.DeclaringType == typeof(Enumerable)) { switch (method.Name) { case AggregateCount: case AggregateLongCount: case AggregateSum: case AggregateMin: case AggregateMax: case AggregateAverage: return true; } } } var property = member as PropertyInfo; if (property is not null && string.Equals(property.Name, AggregateCount, StringComparison.Ordinal) && typeof(IEnumerable).IsAssignableFrom(property.DeclaringType)) return true; return false; } public virtual bool IsAggregateArgumentPredicate(string aggregateName) { return string.Equals(aggregateName, AggregateCount, StringComparison.Ordinal) || string.Equals(aggregateName, AggregateLongCount, StringComparison.Ordinal); } public virtual Expression GetOuterJoinTest(SelectExpression select) { /* * if the column is used in the join condition (equality test) * if it is null in the database then the join test won't match (null != null) so the row won't appear * we can safely use this existing column as our test to determine if the outer join produced a row * * find a column that is used in equality test */ var aliases = DeclaredAliasesResolver.Resolve(select.From); var columns = JoinColumnResolver.Resolve(aliases, select).ToList(); if (columns.Any()) { /* * prefer one that is already in the projection list. */ foreach (var column in columns) { foreach (var col in select.Columns) { if (column.Equals(col.Expression)) return column; } } return columns[0]; } /* * fall back to introducing a constant */ return Expression.Constant(1, typeof(int?)); } public virtual ProjectionExpression AddOuterJoinTest(ProjectionExpression expression) { var test = GetOuterJoinTest(expression.Select); var select = expression.Select; ColumnExpression? testCol = null; /* * look to see if test expression exists in columns already */ foreach (var column in select.Columns) { if (test.Equals(column.Expression)) { var colType = TypeSystem.ResolveColumnType(test.Type); testCol = new ColumnExpression(test.Type, colType, select.Alias, column.Name); break; } } if (testCol is null) { /* * add expression to projection */ testCol = test as ColumnExpression; var colName = (testCol is not null) ? testCol.Name : "Test"; colName = expression.Select.Columns.ResolveAvailableColumnName(colName); var colType = TypeSystem.ResolveColumnType(test.Type); select = select.AddColumn(new ColumnDeclaration(colName, test, colType)); testCol = new ColumnExpression(test.Type, colType, select.Alias, colName); } var newProjector = new OuterJoinedExpression(testCol, expression.Projector); return new ProjectionExpression(select, newProjector, expression.Aggregator); } public virtual bool IsScalar(Type type) { type = Nullables.GetNonNullableType(type); return Interop.TypeSystem.GetTypeCode(type) switch { TypeCode.Empty => false, TypeCode.Object => type == typeof(DateTimeOffset) || type == typeof(TimeSpan) || type == typeof(Guid) || type == typeof(byte[]), _ => true, }; } /// /// Determines whether the given expression can be represented as a column in a select expressionss /// public virtual bool CanBeColumn(Expression expression) { return MustBeColumn(expression) || IsScalar(expression.Type); } /// /// Determines whether the given expression must be represented as a column in a SELECT column list /// public virtual bool MustBeColumn(Expression expression) { return expression.NodeType switch { (ExpressionType)DatabaseExpressionType.Column or (ExpressionType)DatabaseExpressionType.Scalar or (ExpressionType)DatabaseExpressionType.Exists or (ExpressionType)DatabaseExpressionType.AggregateSubquery or (ExpressionType)DatabaseExpressionType.Aggregate => true, _ => false, }; } public virtual Linguist CreateLinguist(ExpressionCompilationContext context, Translator translator) { return new Linguist(context, this, translator); } }