ChilliCream / ChilliCream/graphql-platform

Add SQL Server temporal tables support

Open
#5,611 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

🌶️ hot chocolate Area: Data
Dominant language
C#
Stars
5.8k
Forks
810
Avg merge
15h 39m
Merged PRs (30d)
98

Description

Is your feature request related to a problem?

I had to to use sql temporal tables for a project.
First I tried mapping the initial iqueriable to a dto that includes the periodStart property.
Found that this was lagging on my queries to the database specially overfetching fields and relationships while using Projections. Decided to use typeExtensions and a resolver with a data loader for fetching the PeriodStart property in case this field is asked for.
This part looks like this:
`

 public class Query
    {
       
        [UsePaging]
        [UseProjection]
        [UseFiltering]
        [UseSorting]
        public  IQueryable<Foo> GetFoo(
            [ScopedService]ApplicationDbContext context 
    
        ) 
        {
            var foo= context.Foo;     
            return Foo;
        }
    }

`

`

public class FooTypeExtension : ObjectTypeExtension<Foo>
    {
        protected override void Configure(IObjectTypeDescriptor<Foo> descriptor)
        {
            descriptor.Field("timeStamp")
            .Type<DateTimeType>()
            
            .Resolve(async(context, ct)=>{
                Foo parent = context.Parent<Foo>();
                
                DateTime date =  await context.Service<FooTimeStampDataLoader>      ().LoadAsync(parent.ID, ct);
                return TimeZoneInfo.ConvertTimeFromUtc(date, TimeZoneInfo.Local);
            });
        }
    }

`

`

 public class FooTimeStampDataLoader : BatchDataLoader<Guid, DateTime>
    {
        private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;

        public FooTimeStampDataLoader (
            IBatchScheduler batchScheduler, 
            DataLoaderOptions options,
            IDbContextFactory<ApplicationDbContext> dbContextFactory)
            : base(batchScheduler, options)
        {
            _dbContextFactory = dbContextFactory ?? 
                throw new ArgumentNullException(nameof(dbContextFactory));
        }

        protected override async Task<IReadOnlyDictionary<Guid, DateTime>> LoadBatchAsync(
            IReadOnlyList<Guid> keys, 
            CancellationToken cancellationToken)
        {
            await using ApplicationDbContext dbContext = 
                _dbContextFactory.CreateDbContext();
   

            var p = await dbContext.Foo
            .Where(s => keys.Contains(s.ID))
            .Select(x => new {
                ID = x.ID,
                TimeStamp = EF.Property<DateTime>(x, "PeriodStart")})
                .
                ToDictionaryAsync(x => x.ID, x => x.TimeStamp)
                ;
            return p;
        }
    }

`

After finding out this works by executing two separate querys to the db in case timeStamp is asked I wanted to add filtering and order by timeStamp as well. For sorting I did by going into the hotchocolate 12.15.2 source code and copying QueryableDefaultSortFieldHandler and modified it like so:

`

public class TimeStampSortHandler
    : SortFieldHandler<QueryableSortContext, QueryableSortOperation>
{
    public override bool CanHandle(
        ITypeCompletionContext context,
        ISortInputTypeDefinition typeDefinition,
        ISortFieldDefinition fieldDefinition) =>
        fieldDefinition.Member is not null;

    public override bool TryHandleEnter(
        QueryableSortContext context,
        ISortField field,
        ObjectFieldNode node,
        [NotNullWhen(true)] out ISyntaxVisitorAction? action)
    {
        if (node.Value.IsNull())
        {
            context.ReportError(
                ErrorHelper.CreateNonNullError(field, node.Value, context));

            action = SyntaxVisitor.Skip;
            return true;
        }
        IExtendedType runtime = new ExtendedType(typeof(DateTime));
        // if (field.RuntimeType is null)
        // {
        //     Console.WriteLine("isnull");
        //     action = null;
        //     return false;
        // }

        if (!(context.GetInstance() is QueryableFieldSelector lastFieldSelector))
        {
            throw ThrowHelper.Sorting_InvalidState_ParentIsNoFieldSelector(field);
        }

        Expression lastSelector = lastFieldSelector.Selector;
        Expression<Func<Foo, DateTime>> expression = _ => EF.Property<DateTime>(_, "PeriodStart");
        Expression nextSelector = Expression.Invoke(expression, lastSelector);
        // Expression nextSelector = field.Member switch
        // {
        //     PropertyInfo i => Expression.Property(lastSelector, i),
        //     MethodInfo i => Expression.Call(lastSelector, i),
        //     { } i => throw ThrowHelper.QueryableSorting_MemberInvalid(i, field),
        //     null => throw ThrowHelper.QueryableSorting_NoMemberDeclared(field),
        // };

        if (context.InMemory)
        {
            nextSelector = SortExpressionBuilder.IfNullThenDefault(
                lastSelector,
                nextSelector,
                Expression.Default(runtime.Source));
        }

        context.PushInstance(lastFieldSelector.WithSelector(nextSelector));
        context.RuntimeTypes.Push(runtime);
        action = SyntaxVisitor.Continue;
        return true;
    }

    public override bool TryHandleLeave(
        QueryableSortContext context,
        ISortField field,
        ObjectFieldNode node,
        [NotNullWhen(true)] out ISyntaxVisitorAction? action)
    {
        // if (field.RuntimeType is null)
        // {
        //     action = null;
        //     return false;
        // }

        // Deque last
        context.PopInstance();
        context.RuntimeTypes.Pop();

        action = SyntaxVisitor.Continue;
        return true;
    }

`

and added a sortInputType that uses this handler:

`

    public class FooSortType : SortInputType<Foo>
{
    protected override void Configure(ISortInputTypeDescriptor<Foo> descriptor)
    {
        
        descriptor.BindFieldsImplicitly();
        descriptor.Field("timeStamp").Type<DefaultSortEnumType>()
         .Extend()
            .OnBeforeCreate(_ => _.Handler = new TimeStampSortHandler());
    }
}


}

`

this works for sorting by more than one field including timeStamp.
Would you do this any differently, is there another way to implement temporalTables with hotchocolate?

The solution you'd like

Would like to not use the dataLoader and let it select foo.PeriodStart as TimeStamp directly on the extended type.
As well as integration for sorting and filtering.
Are you planning for adding support for sql server temporal tables on 13.0?
Thank you.

Product

Hot Chocolate

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the QueryableDefaultSortFieldHandler pattern alongside the provided FooTypeExtension, FooTimeStampDataLoader, TimeStampSortHandler, and FooSortType examples. Define the scope for selecting PeriodStart without the data loader and for integrating temporal-table fields with sorting and filtering; done should provide the requested Hot Chocolate support or a documented design and tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, graphql, sql
Domain
api, database
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.