unit XData.QueryBuilder;

{$I XData.inc}

interface

uses
  Generics.Collections, System.Classes, System.SysUtils, System.Types, System.StrUtils,
  System.Rtti, System.TypInfo,
  Aurelius.Criteria.Base,
  XData.Model.Classes,
  XData.Types.Map;

type
  IXDataQueryBuilder = interface
    ['{B746F1DD-3231-4014-B746-7195B63F59F3}']
    function From(const TypeName: string): IXDataQueryBuilder; overload;
    function From(const AClass: TClass): IXDataQueryBuilder; overload;
    function Filter(const ACriteria: TCustomCriterion): IXDataQueryBuilder; overload;
    function FilterRaw(const ACriteria: string): IXDataQueryBuilder; overload;
    function OrderBy(const APropName: string; const AAscending: Boolean = True): IXDataQueryBuilder; overload;
    function OrderBy(const AProjection: TSimpleProjection; const AAscending: Boolean = True): IXDataQueryBuilder; overload;
    function OrderByRaw(const AExpression: string; const AAscending: Boolean = True): IXDataQueryBuilder;
    function Top(const ANumber: Integer): IXDataQueryBuilder;
    function Skip(const ANumber: Integer): IXDataQueryBuilder;
    function Expand(const APropName: string): IXDataQueryBuilder;
    function ExpandRaw(const AExpression: string): IXDataQueryBuilder;
    function InlineCount(const AEnabled: Boolean = True): IXDataQueryBuilder;
    function QueryString: string;
  end;

  EXDataQueryBuilderException = class(Exception)
  end;

resourcestring
  SInvalidQueryType = 'Invalid query type "%s"';
  SInvalidSubPropInPath = 'Invalid subproperty "%s" in property "%s"';
  SQueryPropertyNotFoundInType = 'Property "%s" not found in type "%s"';
  SQueryInvalidTypeCast = 'Invalid value "%s" for type "%s"';
  SQueryUnsupportedType = 'Cannot query property of type "%s"';
  SQueryUnknownType = 'Cannot query property of unknown type';
  SQueryTypeNotSpecified = 'Query type not specified, use "From" method to specify it';
  SQueryUnsupportedFeature = 'Unsupported feature "%s"';
  SQueryFunctionWrongParamNumber = 'Invalid number of parameters for function "%s", expected: %d, actual: %d';

function CreateQuery(AModel: TXDataModel = nil): IXDataQueryBuilder;

implementation

uses
  System.Variants,
  Bcl.Utils,
  Bcl.Rtti.Utils,
  Aurelius.Global.Utils,
  Aurelius.Mapping.Optimization,
  XData.Aurelius.Model;

type
  TXDataQueryBuilder = class(TInterfacedObject, IXDataQueryBuilder)
  strict private
    FModel: TXDataModel;
    FTypedQuery: Boolean;
    FBaseType: TXDataStructuredType;
    FFilter: string;
    FOrderBy: string;
    FTop: string;
    FSkip: string;
    FExpand: string;
    FInlineCount: string;
    FTypes: TXDataTypeMap;
    function UrlEncode(const AValue: string): string;
    procedure MapProp(BaseType: TXDataStructuredType; const PropName: string;
      var TranslatedPropName: string; var AType: TXDataType);
    function FindTypedProperty(BaseType: TXDataStructuredType; const PropName: string): TXDataProperty;
  private
    procedure Map(const Path: string; var TranslatedPath: string; var AType: TXDataType);
    function FindType(const TypeInfo: PTypeInfo): TXDataType;
    function ValueLiteral(const Value: TValue; ValueType: TXDataType): string;
  public
    constructor Create(AModel: TXDataModel); reintroduce;
    destructor Destroy; override;
  public
    function From(const TypeName: string): IXDataQueryBuilder; overload;
    function From(const AClass: TClass): IXDataQueryBuilder; overload;
    function Filter(const AExpression: TCustomCriterion): IXDataQueryBuilder;
    function FilterRaw(const AExpression: string): IXDataQueryBuilder;
    function OrderBy(const APropName: string; const AAscending: Boolean = True): IXDataQueryBuilder; overload;
    function OrderBy(const AProjection: TSimpleProjection; const AAscending: Boolean = True): IXDataQueryBuilder; overload;
    function OrderByRaw(const AExpression: string; const AAscending: Boolean = True): IXDataQueryBuilder;
    function Top(const ANumber: Integer): IXDataQueryBuilder;
    function Skip(const ANumber: Integer): IXDataQueryBuilder;
    function Expand(const APropName: string): IXDataQueryBuilder;
    function ExpandRaw(const AExpression: string): IXDataQueryBuilder;
    function InlineCount(const AEnabled: Boolean = True): IXDataQueryBuilder;
  public
    function QueryString: string;
  end;

  TBuildFilterFunction = reference to procedure(const AValue: string);

  TXDataQueryProjectionVisitor = class(TProjectionVisitor)
  strict private
    FBuilder: TXDataQueryBuilder;
    FBaseType: TXDataStructuredType;
    FBuildFilter: TBuildFilterFunction;
    FValueType: TXDataType;
    function FindType(const TypeInfo: PTypeInfo): TXDataType;
  public
    constructor Create(ABuilder: TXDataQueryBuilder; ABaseType: TXDataStructuredType; ABuildFilter: TBuildFilterFunction);
    destructor Destroy; override;
    property ValueType: TXDataType read FValueType;
  private
    procedure AppendValue(const Value: TValue; ValueType: TXDataType);
  public
    procedure Constant(Proj: TConstantProjection); override;
    procedure Prop(Proj: TPropertyProjection); override;
    procedure List(Proj: TProjectionList); override;
    procedure Alias(Proj: TAliasedProjection); override;
    procedure Column(Proj: TColumnProjection); override;
    procedure Group(Proj: TGroupProjection); override;
    procedure Aggregate(Proj: TAggregateProjection); override;
    procedure Conditional(Proj: TConditionalProjection); override;
    procedure Null(Proj: TNullProjection); override;
    procedure Sql(Proj: TSqlProjection); override;
    procedure SqlFunction(Proj: TSqlFunctionProjection); override;
  end;

  TXDataQueryCriterionVisitor = class(TCriterionVisitor)
  strict private
    FBuilder: TXDataQueryBuilder;
    FBaseType: TXDataStructuredType;
    FProjectionVisitor: TXDataQueryProjectionVisitor;
    FBuildFilter: TBuildFilterFunction;
    procedure AppendOper(Oper: TExpressionOperator);
    procedure AppendVariant(const Value: Variant; ValueType: TXDataType);
    procedure AppendValue(const Value: TValue; ValueType: TXDataType);
  public
    constructor Create(ABuilder: TXDataQueryBuilder; ABaseType: TXDataStructuredType; ABuildFilter: TBuildFilterFunction);
    destructor Destroy; override;
  public
    procedure Simple(Expr: TSimpleExpression); override;
    procedure Projection(Expr: TProjectionExpression); override;
    procedure IdentifierEq(Expr: TIdentifierEqExpression); override;
    procedure _Or(Expr: TOrExpression); override;
    procedure _And(Expr: TAndExpression); override;
    procedure _Not(Expr: TNotExpression); override;
    procedure Null(Expr: TNullExpression); override;
    procedure NotNull(Expr: TNotNullExpression); override;
    procedure Sql(Expr: TSqlCriterion); override;
    procedure SqlDB(Expr: TSqlDBCriterion); override;
    procedure SqlFunction(Expr: TSqlFunctionCriterion); override;
    procedure _In(Expr: TInExpression); override;
  end;

function CreateQuery(AModel: TXDataModel = nil): IXDataQueryBuilder;
begin
  if AModel = nil then
    AModel := TXDataAureliusModel.Default;

  Result := TXDataQueryBuilder.Create(AModel);
end;

{ TXDataQueryBuilder }

function TXDataQueryBuilder.ValueLiteral(const Value: TValue; ValueType: TXDataType): string;
begin
  if (ValueType = nil) or (ValueType.UrlConverter = nil) then
    raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedType, [ValueType.Name]);

  Result := ValueType.UrlConverter.ValueToUrl(Value, nil);
end;

constructor TXDataQueryBuilder.Create(AModel: TXDataModel);
begin
  inherited Create;
  FModel := AModel;
  FTypes := TXDataTypeMap.Create;
  FillDefaultTypes(FTypes);
end;

destructor TXDataQueryBuilder.Destroy;
begin
  FTypes.Free;
  inherited;
end;

function TXDataQueryBuilder.UrlEncode(const AValue: string): string;
begin
  Result := AValue;
  Result := TBclUtils.PercentEncode(AValue);
end;

function TXDataQueryBuilder.Filter(const AExpression: TCustomCriterion): IXDataQueryBuilder;
var
  LVisitor: TXDataQueryCriterionVisitor;
  LExpression: string;
begin
  try
    Result := Self;
    LExpression := '';
    LVisitor := TXDataQueryCriterionVisitor.Create(Self, FBaseType,
      procedure(const AValue: string)
      begin
        LExpression := LExpression + AValue;
      end);
    try
      AExpression.Accept(LVisitor);
      FilterRaw(LExpression);
    finally
      LVisitor.Free;
    end;
  finally
    AExpression.Free;
  end;
end;

function TXDataQueryBuilder.FilterRaw(const AExpression: string): IXDataQueryBuilder;
begin
  Result := Self;
  if AExpression = '' then Exit;

  if FFilter = '' then
    FFilter := AExpression
  else
    FFilter := Format('(%s and %s)', [FFilter, AExpression]);
end;

function TXDataQueryBuilder.FindType(const TypeInfo: PTypeInfo): TXDataType;
var
  EnumType: TXDataEnumType;
begin
  if TypeInfo = nil then
    raise EXDataQueryBuilderException.Create(SQueryUnknownType);

  if FTypes.TryGetValue(TypeInfo, Result) then Exit;

  for EnumType in FModel.DefaultSchema.EnumTypes do
    if EnumType.Data.AsType<TRttiType>.Handle = TypeInfo then
      Exit(EnumType);

  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedType, [GetTypeName(TypeInfo)]);
end;

function TXDataQueryBuilder.FindTypedProperty(BaseType: TXDataStructuredType; const PropName: string): TXDataProperty;

  function SamePropName(RttiInfo: TRttiOptimization; const PropName: string): Boolean;
  var
    TargetName: string;
  begin
    TargetName := RttiInfo.MemberName;
    if SameText(TargetName, PropName) then Exit(True);

    if (RttiInfo.MemberKind = TMemberKind.Field) and (Length(TargetName) > 1) and
      (TargetName[1] = 'F') and SameText(PropName, Copy(TargetName, 2)) then
      Exit(True);

    Result := False;
  end;

var
  XModel: TXDataAureliusModel;
  I: Integer;
  Info: TXDataAureliusPropInfo;
  NavInfo: TXDataAureliusNavPropInfo;
begin
  Assert(FModel is TXDataAureliusModel);
  XModel := TXDataAureliusModel(FModel);
  for I := 0 to BaseType.ScalarPropertyCount - 1 do
  begin
    Info := XModel.GetScalarPropertyInfo(BaseType.GetScalarProperty(I));
    if (Info <> nil) and SamePropName(Info.RttiInfo, PropName) then
      Exit(BaseType.GetScalarProperty(I));
  end;

  for I := 0 to BaseType.StructuredPropertyCount - 1 do
  begin
    NavInfo := XModel.GetStructuredPropInfo(BaseType.GetStructuredProperty(I));
    if (NavInfo <> nil) and SamePropName(NavInfo.RttiInfo, PropName) then
      Exit(BaseType.GetStructuredProperty(I));
  end;

  if (BaseType is TXDataEntityType) and (TXDataEntityType(BaseType).BaseType <> nil) then
    Result := FindTypedProperty(TXDataEntityType(BaseType).BaseType, PropName)
  else
    Result := nil;
end;

function TXDataQueryBuilder.From(const AClass: TClass): IXDataQueryBuilder;
var
  TargetType: TXDataStructuredType;
begin
  Result := Self;

  TargetType := nil;
  if FModel is TXDataAureliusModel then
  begin
    TargetType := TXDataAureliusModel(FModel).FindEntityTypeByClass(AClass);
    if TargetType = nil then
      TargetType := TXDataAureliusModel(FModel).FindInstanceTypeByClass(AClass);
  end;

  if TargetType = nil then
    raise EXDataQueryBuilderException.CreateFmt(SInvalidQueryType, [AClass.ClassName]);

  FBaseType := TargetType;
  FTypedQuery := FModel is TXDataAureliusModel;
end;

function TXDataQueryBuilder.From(const TypeName: string): IXDataQueryBuilder;
var
  LType: TXDataType;
begin
  Result := Self;

  LType := FModel.DefaultSchema.FindType(TypeName);
  if not ((LType <> nil) and (LType is TXDataStructuredType)) then
    raise EXDataQueryBuilderException.CreateFmt(SInvalidQueryType, [TypeName]);

  FBaseType := TXDataStructuredType(LType);
  FTypedQuery := False;
end;

function TXDataQueryBuilder.OrderBy(const APropName: string; const AAscending: Boolean = True): IXDataQueryBuilder;
begin
  Result := OrderBy(TPropertyProjection.Create(APropName), AAscending);
end;

function TXDataQueryBuilder.OrderByRaw(const AExpression: string;
  const AAscending: Boolean = True): IXDataQueryBuilder;
const
  S_SORT_ORDER: array [Boolean] of string = (' desc', '');

begin
  Result := Self;
  if AExpression  = '' then Exit;

  if FOrderBy <> '' then
    FOrderBy := FOrderBy + ',';
  FOrderBy := FOrderBy + Format('%s%s', [AExpression, S_SORT_ORDER[AAscending]]);
end;

function TXDataQueryBuilder.Top(const ANumber: Integer): IXDataQueryBuilder;
begin
  Result := Self;
  if ANumber > 0 then
    FTop := IntToStr(ANumber)
  else
    FTop := '';
end;

function TXDataQueryBuilder.Skip(const ANumber: Integer): IXDataQueryBuilder;
begin
  Result := Self;
  if ANumber > 0 then
    FSkip := IntToStr(ANumber)
  else
    FSkip := '';
end;

function TXDataQueryBuilder.Expand(const APropName: string): IXDataQueryBuilder;
var
  LVisitor: TXDataQueryProjectionVisitor;
  LExpression: string;
  LProjection: TProjection;
begin
  LProjection := TPropertyProjection.Create(APropName);
  try
    Result := Self;
    LExpression := '';
    LVisitor := TXDataQueryProjectionVisitor.Create(Self, FBaseType,
      procedure(const AValue: string)
      begin
        LExpression := LExpression + AValue;
      end);
    try
      LProjection.Accept(LVisitor);
      ExpandRaw(LExpression);
    finally
      LVisitor.Free;
    end;
  finally
    LProjection.Free;
  end;
end;

function TXDataQueryBuilder.ExpandRaw(const AExpression: string): IXDataQueryBuilder;
begin
  Result := Self;
  if AExpression = '' then Exit;

  if FExpand <> '' then
    FExpand := FExpand + ',';
  FExpand := FExpand + AExpression;
end;

function TXDataQueryBuilder.InlineCount(const AEnabled: Boolean): IXDataQueryBuilder;
begin
  Result := Self;
  if AEnabled then
    FInlineCount := 'allpages'
  else
    FInlineCount := '';
end;

procedure TXDataQueryBuilder.Map(const Path: string; var TranslatedPath: string; var AType: TXDataType);
var
  PathItem: string;
  NewPathItem: string;
  TargetType: TXDataType;
begin
  TranslatedPath := '';
  TargetType := FBaseType;
  if FBaseType = nil then
    raise EXDataQueryBuilderException.Create(SQueryTypeNotSpecified);

  AType := nil;
  for PathItem in SplitString(Path, '/.') do
  begin
    if not (TargetType is TXDataStructuredType) then
      raise EXDataQueryBuilderException.CreateFmt(SInvalidSubPropInPath, [PathItem, Path]);

    MapProp(TXDataStructuredType(TargetType), PathItem, NewPathItem, AType);
    if TranslatedPath <> '' then
      TranslatedPath := TranslatedPath + '/';
    TranslatedPath := TranslatedPath + NewPathItem;
    TargetType := AType;
  end;
end;

procedure TXDataQueryBuilder.MapProp(BaseType: TXDataStructuredType; const PropName: string;
  var TranslatedPropName: string; var AType: TXDataType);
var
  Prop: TXDataProperty;
begin
  if FTypedQuery then
    Prop := FindTypedProperty(BaseType, PropName)
  else
    Prop := BaseType.FindAnyProperty(PropName);

  if Prop = nil then
    raise EXDataQueryBuilderException.CreateFmt(SQueryPropertyNotFoundInType, [PropName, BaseType.Name]);
  TranslatedPropName := Prop.Name;
  AType := Prop.ValueType;
end;

function TXDataQueryBuilder.OrderBy(const AProjection: TSimpleProjection;
  const AAscending: Boolean = True): IXDataQueryBuilder;
var
  LVisitor: TXDataQueryProjectionVisitor;
  LExpression: string;
begin
  try
    Result := Self;
    LExpression := '';
    LVisitor := TXDataQueryProjectionVisitor.Create(Self, FBaseType,
      procedure(const AValue: string)
      begin
        LExpression := LExpression + AValue;
      end);
    try
      AProjection.Accept(LVisitor);
      OrderByRaw(LExpression, AAscending);
    finally
      LVisitor.Free;
    end;
  finally
    AProjection.Free;
  end;
end;

function TXDataQueryBuilder.QueryString: string;

  procedure AddParam(var Query: string; const Name, Value: string);
  begin
    if Value = '' then Exit;
    
    if Query <> '' then
      Query := Query + '&';
    Query := Query + Name + '=' + UrlEncode(Value);
  end;

const
  cFilter = '$filter';
  cOrderBy = '$orderby';
  cTop = '$top';
  cSkip = '$skip';
  cExpand = '$expand';
  cInlineCount = '$inlinecount';
begin
  Result := '';
  AddParam(Result, cFilter, FFilter);
  AddParam(Result, cOrderBy, FOrderBy);
  AddParam(Result, cTop, FTop);
  AddParam(Result, cSkip, FSkip);
  AddParam(Result, cExpand, FExpand);
  AddParam(Result, cInlineCount, FInlineCount);
end;

{ TXDataQueryCriterionVisitor }

procedure TXDataQueryCriterionVisitor.AppendOper(Oper: TExpressionOperator);
begin
  case Oper of
    TExpressionOperator.Equal:
      FBuildFilter(' eq ');
    TExpressionOperator.Like:
      FBuildFilter(' like ');
    TExpressionOperator.Greater:
      FBuildFilter(' gt ');
    TExpressionOperator.Less:
      FBuildFilter(' lt ');
    TExpressionOperator.GreaterEqual:
      FBuildFilter(' ge ');
    TExpressionOperator.LessEqual:
      FBuildFilter(' le ');
    TExpressionOperator.NotEqual:
      FBuildFilter(' ne ');
  end;
end;

procedure TXDataQueryCriterionVisitor.AppendValue(const Value: TValue; ValueType: TXDataType);
begin
  FBuildFilter(FBuilder.ValueLiteral(Value, ValueType));
end;

procedure TXDataQueryCriterionVisitor.AppendVariant(const Value: Variant; ValueType: TXDataType);

  procedure Check(Valid: Boolean);
  begin
    if not Valid then
      raise EXDataQueryBuilderException.CreateFmt(SQueryInvalidTypeCast,
        [TUtils.VariantToString(Value), ValueType.Name]);
  end;

  procedure Unsupported;
  begin
    raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedType, [ValueType.Name])
  end;

  procedure _AppendValue(const TargetValue: TValue);
  begin
    AppendValue(TargetValue, ValueType);
  end;

begin
  if ValueType = nil then
    raise EXDataQueryBuilderException.Create(SQueryUnknownType);

  case ValueType.TypeKind of
    TXTypeKind.xtUndefined:
      Unsupported;
    TXTypeKind.xtText:
      begin
        Check(VarIsStr(Value));
        _AppendValue(VarToStr(Value));
      end;
    TXTypeKind.xtInt32:
      begin
        Check(VarIsOrdinal(Value));
        _AppendValue(Integer(Value));
      end;
    TXTypeKind.xtInt64:
      begin
        Check(VarIsOrdinal(Value));
        _AppendValue(Int64(Value));
      end;
    TXTypeKind.xtDouble:
      begin
        Check(VarIsNumeric(Value));
        _AppendValue(Double(Value));
      end;
    TXTypeKind.xtBoolean:
      begin
        Check(VarType(Value) = varBoolean);
        _AppendValue(Boolean(Value));
      end;
    TXTypeKind.xtDateTime:
      begin
        Check(VarIsNumeric(Value) or (VarType(Value) = varDate));
        _AppendValue(TValue.From<TDateTime>(VarToDateTime(Value)));
      end;
    TXTypeKind.xtDate:
      begin
        Check(VarIsNumeric(Value) or (VarType(Value) = varDate));
        _AppendValue(TValue.From<TDate>(VarToDateTime(Value)));
      end;
    TXTypeKind.xtTime:
      begin
        Check(VarIsNumeric(Value) or (VarType(Value) = varDate));
        _AppendValue(TValue.From<TTime>(VarToDateTime(Value)));
      end;
    TXTypeKind.xtCurrency:
      begin
        Check(VarIsNumeric(Value));
        _AppendValue(Currency(Value));
      end;
    TXTypeKind.xtGuid:
      begin
        Check(VarIsStr(Value));
        _AppendValue(TValue.From<TGuid>(StringToGuid(Value)));
      end;
    TXTypeKind.xtVariant:
      Unsupported;
    TXTypeKind.xtBinary:
      Unsupported;
    TXTypeKind.xtStream:
      Unsupported;
    TXTypeKind.xtInt16:
      begin
        Check(VarIsOrdinal(Value));
        _AppendValue(SmallInt(Value));
      end;
    TXTypeKind.xtByte:
      begin
        Check(VarIsOrdinal(Value));
        _AppendValue(Byte(Value));
      end;
    TXTypeKind.xtSByte:
      Unsupported;
    TXTypeKind.xtEntity:
      Unsupported;
    TXTypeKind.xtEntityCollection:
      Unsupported;
    TXTypeKind.xtEnum:
      begin
        Check(VarIsOrdinal(Value));
        _AppendValue(Integer(Value));
      end;
    TXTypeKind.xtScalarCollection:
      Unsupported;
    TXTypeKind.xtRaw:
      Unsupported;
    TXTypeKind.xtInstance:
      Unsupported;
    TXTypeKind.xtInstanceCollection:
      Unsupported;
  else
    Unsupported;
  end;
end;

constructor TXDataQueryCriterionVisitor.Create(ABuilder: TXDataQueryBuilder;
  ABaseType: TXDataStructuredType; ABuildFilter: TBuildFilterFunction);
begin
  inherited Create;
  FBuilder := ABuilder;
  FBaseType := ABaseType;
  FBuildFilter := ABuildFilter;
  FProjectionVisitor := TXDataQueryProjectionVisitor.Create(FBuilder, FBaseType, FBuildFilter);
end;

destructor TXDataQueryCriterionVisitor.Destroy;
begin
  FProjectionVisitor.Free;
  inherited;
end;

procedure TXDataQueryCriterionVisitor.IdentifierEq(Expr: TIdentifierEqExpression);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Expr.ClassName]);
end;

procedure TXDataQueryCriterionVisitor.NotNull(Expr: TNotNullExpression);
begin
  FBuildFilter('(');
  Expr.Projection.Accept(FProjectionVisitor);
  AppendOper(TExpressionOperator.NotEqual);
  FBuildFilter('null)');
end;

procedure TXDataQueryCriterionVisitor.Null(Expr: TNullExpression);
begin
  FBuildFilter('(');
  Expr.Projection.Accept(FProjectionVisitor);
  AppendOper(TExpressionOperator.Equal);
  FBuildFilter('null)');
end;

procedure TXDataQueryCriterionVisitor.Projection(Expr: TProjectionExpression);
begin
  FBuildFilter('(');
  if Expr.LeftSide <> nil then
    Expr.LeftSide.Accept(FProjectionVisitor);
  AppendOper(Expr.Oper);
  if Expr.RightSide <> nil then
    Expr.RightSide.Accept(FProjectionVisitor);
  FBuildFilter(')');
end;

procedure TXDataQueryCriterionVisitor.Simple(Expr: TSimpleExpression);
begin
  FBuildFilter('(');
  Expr.Projection.Accept(FProjectionVisitor);
  AppendOper(Expr.Oper);
  AppendVariant(Expr.Value, FProjectionVisitor.ValueType);
  FBuildFilter(')');
end;

procedure TXDataQueryCriterionVisitor.Sql(Expr: TSqlCriterion);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Expr.ClassName]);
end;

procedure TXDataQueryCriterionVisitor.SqlDB(Expr: TSqlDBCriterion);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Expr.ClassName]);
end;

procedure TXDataQueryCriterionVisitor.SqlFunction(Expr: TSqlFunctionCriterion);
var
  I: Integer;
begin
  FBuildFilter(LowerCase(Expr.FunctionName) + '(');
  for I := 0 to Length(Expr.Args) - 1 do
  begin
    if I > 0 then
      FBuildFilter(',');
    Expr.Args[I].Accept(FProjectionVisitor);
  end;
  FBuildFilter(')');
end;

procedure TXDataQueryCriterionVisitor._And(Expr: TAndExpression);
begin
  FBuildFilter('(');
  Expr.LeftSide.Accept(Self);
  FBuildFilter(' and ');
  Expr.RightSide.Accept(Self);
  FBuildFilter(')');
end;

procedure TXDataQueryCriterionVisitor._In(Expr: TInExpression);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Expr.ClassName]);
end;

procedure TXDataQueryCriterionVisitor._Not(Expr: TNotExpression);
begin
  FBuildFilter('not ');
  Expr.Condition.Accept(Self);
end;

procedure TXDataQueryCriterionVisitor._Or(Expr: TOrExpression);
begin
  FBuildFilter('(');
  Expr.LeftSide.Accept(Self);
  FBuildFilter(' or ');
  Expr.RightSide.Accept(Self);
  FBuildFilter(')');
end;

{ TXDataQueryProjectionVisitor }

constructor TXDataQueryProjectionVisitor.Create(ABuilder: TXDataQueryBuilder;
  ABaseType: TXDataStructuredType; ABuildFilter: TBuildFilterFunction);
begin
  inherited Create;
  FBuilder := ABuilder;
  FBaseType := ABaseType;
  FBuildFilter := ABuildFilter;
end;

destructor TXDataQueryProjectionVisitor.Destroy;
begin
  inherited;
end;

function TXDataQueryProjectionVisitor.FindType(const TypeInfo: PTypeInfo): TXDataType;
begin
  Result := FBuilder.FindType(TypeInfo);
end;

procedure TXDataQueryProjectionVisitor.Aggregate(Proj: TAggregateProjection);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.Alias(Proj: TAliasedProjection);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.AppendValue(const Value: TValue; ValueType: TXDataType);
begin
  FBuildFilter(FBuilder.ValueLiteral(Value, ValueType));
end;

procedure TXDataQueryProjectionVisitor.Column(Proj: TColumnProjection);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.Conditional(Proj: TConditionalProjection);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.Constant(Proj: TConstantProjection);
begin
  AppendValue(Proj.Value, FindType(Proj.Value.TypeInfo));
end;

procedure TXDataQueryProjectionVisitor.Group(Proj: TGroupProjection);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.List(Proj: TProjectionList);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.Null(Proj: TNullProjection);
begin
  FBuildFilter('null');
end;

procedure TXDataQueryProjectionVisitor.Prop(Proj: TPropertyProjection);
var
  Path: string;
  TargetType: TXDataType;
begin
  FBuilder.Map(Proj.PropName, Path, TargetType);
  FBuildFilter(Path);
  FValueType := TargetType;
end;

procedure TXDataQueryProjectionVisitor.Sql(Proj: TSqlProjection);
begin
  raise EXDataQueryBuilderException.CreateFmt(SQueryUnsupportedFeature, [Proj.ClassName]);
end;

procedure TXDataQueryProjectionVisitor.SqlFunction(Proj: TSqlFunctionProjection);

  procedure AppendMath(const Oper: string);
  begin
    if Length(Proj.Args) <> 2 then
      raise EXDataQueryBuilderException.CreateFmt(SQueryFunctionWrongParamNumber,
        [Proj.FunctionName, Length(Proj.Args), 2]);

    FBuildFilter('(');
    Proj.Args[0].Accept(Self);
    FBuildFilter(' ');
    FBuildFilter(Oper);
    FBuildFilter(' ');
    Proj.Args[0].Accept(Self);
    FBuildFilter(')');
  end;

var
  FN: string;
  I: Integer;
begin
  FN := LowerCase(Proj.FunctionName);
  if (FN = 'add') or (FN = 'sub') then
    AppendMath(FN)
  else
  if FN = 'multiply' then
    AppendMath('mul')
  else
  if FN = 'divide' then
    AppendMath('div')
  else
  begin
    FBuildFilter(FN + '(');
    for I := 0 to Length(Proj.Args) - 1 do
    begin
      if I > 0 then
        FBuildFilter(',');
      Proj.Args[I].Accept(Self);
    end;
    FBuildFilter(')');
  end;

  FValueType := FindType(Proj.ResultType);
end;

end.
