Search code examples
c#sqlfluent

Recursion in Fluent API


I am designing a fluent API for writing SQL. Keep in mind one of my goals is to have API not suggest functions that can't be called in that part of the chain. For instance if you just got done defining a field in the select clause you can't call Where until you called From first. A simple query looks like this:

        string sql = SelectBuilder.Create()
            .Select()
                .Fld("field1")
            .From("table1")
            .Where()
                .Whr("field1 > field2")
                .Whr("CURRENT_TIMESTAMP > field3")
            .Build()
            .SQL;

My problem comes with recursion in SQL code. Say you wanted to have a field contain another SQL statement like below:

        string sql = SelectBuilder.Create()
            .Select()
                .Fld("field1")
                .SQLFld()
                    .Select
                        .Count("field6")
                    .From("other table")
                .EndSQLFld()
                .FLd("field2")
            .From("table1")
            .Where()
                .Whr("field1 > field2")
                .Whr("CURRENT_TIMESTAMP > field3")
            .Build()
            .SQL;

I am using method chaining to build my fluent API. It many ways it is a state machine strewn out across many classes which represent each state. To add this functionality I would need to copy essentially every state I already have and wrap them around the two SQLFld and EndSQLFld states. I would need yet another copy if you were one more level down and were embedding a SQL statement in to a field of the already embedded SQL statement. This goes on to infinity, so with an infinitely deep embedded SQL query I would need an infinite number of classes to represent the infinite states.

I thought about writing a SelectBuilder query that was taken to the point of the Build method and then embedding that SelectBuilder in to another SelectBuilder and that fixes my infinity problem, but it is not very elegant and that is the point of this API.

I could also throw out the idea that the API only offers functions when they are appropriate but I would really hate to do that. I feel like that helps you best discover how to use the API. In many fluent APIs it doesn't matter which order you call what, but I want the API to appear as close to the actual SQL statement as possible and enforce its syntax.

Anyone have any idea how to solve this issue?


Solution

  • Glad to see you are trying fluent interfaces, I think they are a very elegant and expressive.

    The builder pattern is not the only implementation for fluent interfaces. Consider this design, and let us know what you think =)

    This is an example and I leave to you the details of your final implementation.

    Interface design example:

     public class QueryDefinition
        {
            // The members doesn't need to be strings, can be whatever you use to handle the construction of the query.
            private string select;
            private string from;
            private string where;
    
            public QueryDefinition AddField(string select)
            {
                this.select = select;
                return this;
            }
    
            public QueryDefinition From(string from)
            {
                this.from = from;
                return this;
            }
    
            public QueryDefinition Where(string where)
            {
                this.where = where;
                return this;
            }
    
            public QueryDefinition AddFieldWithSubQuery(Action<QueryDefinition> definitionAction)
            {
                var subQueryDefinition = new QueryDefinition(); 
                definitionAction(subQueryDefinition);
    
                // Add here any action needed to consider the sub query, which should be defined in the object subQueryDefinition.
    
                return this;
            }
    

    Example usage:

    static void Main(string[] args)
            {
                // 1 query deep
                var def = new QueryDefinition();
                def
                    .AddField("Field1")
                    .AddField("Filed2")
                    .AddFieldWithSubQuery(subquery =>
                    {
                        subquery
                            .AddField("InnerField1")
                            .AddField("InnerFiled2")
                            .From("InnerTable")
                            .Where("<InnerCondition>");
                    })
                    .From("Table")
                    .Where("<Condition>");
    
    
                // 2 queries deep
                var def2 = new QueryDefinition();
                def2
                    .AddField("Field1")
                    .AddField("Filed2")
                    .AddFieldWithSubQuery(subquery =>
                    {
                        subquery
                            .AddField("InnerField1")
                            .AddField("InnerField2")
                            .AddFieldWithSubQuery(subsubquery =>
                                {
                                    subsubquery
                                        .AddField("InnerInnerField1")
                                        .AddField("InnerInnerField2")
                                        .From("InnerInnerTable")
                                        .Where("<InnerInnerCondition>");
                                })
                            .From("InnerInnerTable")
                            .Where("<InnerCondition>");
                    })
                    .From("Table")
                    .Where("<Condition>");
            }