-
-
Notifications
You must be signed in to change notification settings - Fork 496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SQL Server use FOR FOR JSON AUTO` #459
Comments
I've ran in to a similar problem and little experimenting in the playground resulted in the following: var compiler = new SqlServerCompiler();
var people = new Query( "Person" );
var contracts = new Query( "Contract" ).WhereColumns( "ContractId", "=", "Person.ContractId" );
var query = people
.Select( "Person.*" )
.SelectRaw( $"({compiler.Compile(contracts)} FOR JSON AUTO)"); To clean this up I created an extension method which seems to do the trick public static class QueryExtensions {
private static SqlServerCompiler compiler = new SqlServerCompiler();
public static Query SelectForJsonAuto( this Query @this, Query query )
=> @this.SelectRaw( $"( {compiler.Compile(query)} FOR JSON AUTO )" );
} Now going through the documentation I think it should technically be possible to create a create table words ( first text, second text, third text );
insert into words values
( 'apple', 'apricot', 'anaconda' ),
( 'bee', 'ball', 'boat' ),
( 'color', 'cheers', 'cactus' )
;
-- The output we're expecting
-- [{"first":"apple","second":"apricot","third":"anaconda"},{"first":"bee","second":"ball","third":"boat"},{"first":"color","second":"cheers","third":"cactus"}] SQLite is a little verbose as you have to build the entire object yourself: select
json_group_array(
json_object( 'first', first, 'second', second, 'third', third ) ) as words
from words; Postgres has a neat select json_agg( words ) from words; I'm sure Oracle, MySQL and other databases will have similar functions. |
A vacation and some time later, I worked on this a little more. For all intents and purposes this smells like a aggregation (and in PostgreSQL it is) but it seemed nearly impossible to implement it as such cleanly. I've looked at overriding To get it all to work though I've created a using SqlKata;
using SqlKata.Compilers;
using SqlKata.Extensions;
using SqlQuery = SqlKata.Query;
namespace Database
{
public class JsonColumn : QueryColumn
{
public override AbstractClause Clone()
{
return new JsonColumn
{
Engine = Engine,
Query = Query.Clone(),
Component = Component,
};
}
}
public class JsonPostgresCompiler : PostgresCompiler
{
public override string CompileColumn(SqlResult ctx, AbstractColumn column)
{
return column switch
{
JsonColumn c => CompileJsonColumn(ctx, c),
_ => base.CompileColumn(ctx, column)
};
string CompileJsonColumn(SqlResult ctx, JsonColumn column)
{
var alias = "";
if (!string.IsNullOrWhiteSpace(column.Query.QueryAlias))
{
alias = $" {ColumnAsKeyword}{WrapValue(column.Query.QueryAlias)}";
}
var subCtx = CompileSelectQuery(column.Query);
ctx.Bindings.AddRange(subCtx.Bindings);
return "json_agg(" + subCtx.RawSql + $"){alias}";
}
}
}
public class JsonSqlServerCompiler : SqlServerCompiler
{
public override string CompileColumn(SqlResult ctx, AbstractColumn column)
{
return column switch
{
JsonColumn c => CompileJsonColumn(ctx, c),
_ => base.CompileColumn(ctx, column)
};
string CompileJsonColumn(SqlResult ctx, JsonColumn column)
{
var alias = "";
if (!string.IsNullOrWhiteSpace(column.Query.QueryAlias))
{
alias = $" {ColumnAsKeyword}{WrapValue(column.Query.QueryAlias)}";
}
var subCtx = CompileSelectQuery(column.Query);
ctx.Bindings.AddRange(subCtx.Bindings);
return "(" + subCtx.RawSql + $" FOR JSON AUTO){alias}";
}
}
}
public static class PlusQueryExtensions
{
public static SqlQuery Plus(this SqlQuery @this, SqlQuery query, string alias)
{
@this.Method = "select";
@this.AddComponent("select", new JsonColumn
{
Query = query.Clone().As(alias)
});
return @this;
}
public static SqlQuery Plus(this SqlQuery @this, Func<SqlQuery, SqlQuery> callback, string alias)
=> @this.Plus(callback(@this.NewChild()), alias);
}
}
Edit: I've created a little sanity test for SQL server to make sure it works and have included two fixes in the orginal listing ( //
// The sanity test for SQL server
//
public class Plus
{
[Fact]
public void inlcude_subquery_as_json_blob()
{
const string expected =
"SELECT [A].*, (SELECT [B].* FROM [B] WHERE [A].[AId] = [B].[BId] FOR JSON AUTO) AS [Blob] FROM [A]";
var compiler = new JsonSqlServerCompiler();
var bQuery = (new SqlQuery())
.Select("B.*")
.From("B")
.WhereColumns("A.AId", "=", "B.BId");
var aQuery = (new SqlQuery())
.Select("A.*")
.From("A")
.Plus(bQuery, "Blob");
var sql = compiler.Compile(aQuery).RawSql;
Assert.Equal(expected, sql);
}
} |
Working on this some more I've created a little overload of the Dapper using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using SqlKata.Compilers;
using SqlQuery = SqlKata.Query;
using Dapper;
namespace Database
{
public static class SqlKata
{
public static void RegisterCompiler(Compiler compiler) => SqlKata.compiler = compiler;
private static Compiler compiler;
private static (string type, string member) SplitPath(string p)
{
var path = p.Split('.');
return (
String.Join('.', path.SkipLast(1)),
path.Last()
);
}
private static Func<object[], TReturn> Map<TReturn>(string[] paths) => objects =>
{
//
// We know a few things here:
// 1) The first element is TReturn, this is the element we want to fold in to
// 2) All models are a [Record] from the LanguageExt package
// 3) These are provided as a JSON blob we only have to deserialize
// 4) The max level we're going to look in to is the level of the result, the JSON deserizalizer will have to deal with nested blobs
//
IEnumerable<(string path, object value)> tuples = paths.Zip(objects);
var record = (TReturn) tuples.First().value;
var named = tuples
.Skip(1)
.Map(t =>
{
//
// As we'll eventually start looking to squeeze out the best performance I think most of the time is going to be spent
// deserializing the object. I am not worried about optimizing this to the max.
//
var tuple = ((string path, string blob)) t;
var path = SplitPath(tuple.path);
// There might be some win here, after this line memory usage peaks to ~90mb, I am not sure if this is coincidental
// or actually has anything to do with this line.
var type = Enumerable.Single(
from assembly in AppDomain.CurrentDomain.GetAssemblies()
from info in assembly.DefinedTypes
where path.type == info.FullName
select info
);
var member = type.GetMember(path.member).Single() switch
{
FieldInfo info => info.FieldType,
PropertyInfo info => info.PropertyType,
_ => throw new NotSupportedException($"Can't assign to [ {tuple.path} ]")
};
var value = System.Text.Json.JsonSerializer.Deserialize(tuple.blob, member);
return (path.member, value);
})
.ToLookup(
t => t.member,
t => t.value
);
//
// Reading the runtime code that goes in to this I am pretty sure if there is a win to be gained it would be here. It
// seems that if we could create a delegate for the with method and memoize that somewhere we can invoke it alot faster
// we'll have to figure something out for the variables though...
//
var with = typeof(TReturn).GetMethod("With");
var arguments = with
.GetParameters()
.Map(p => named.Contains(p.Name) ? named[p.Name] : null)
.ToArray();
return (TReturn) with.Invoke(record, arguments);
};
public static IEnumerable<TReturn> Query<TReturn>(this IDbConnection @this, SqlQuery sql, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
{
// You'd think this would work but it doesn't... It thows a cast exception somewhere inside SqlKata
//var columns = sql.GetComponents<JsonColumn>("select", compiler.EngineCode);
var columns =
from component in sql.GetComponents("select", compiler.EngineCode)
where component is JsonColumn
select (JsonColumn)component;
var compiled = compiler.Compile(sql);
return @this.Query(
sql: compiled.Sql,
types: new[] {typeof(TReturn)}
.Concat(from _ in columns select typeof(string))
.ToArray(),
map: Map<TReturn>(
new[] {typeof(TReturn).FullName}
.Concat(from c in columns select c.Query.QueryAlias)
.ToArray()
),
param: compiled.NamedBindings,
transaction,
buffered,
splitOn: String.Join(",", from c in columns select c.Query.QueryAlias),
commandTimeout,
commandType
);
}
}
} Edit: For all future readers, as mentioned I've been working with |
Interesting stuff here, it seems, System.Text.Json is not mature enough, will keep this open for now, to collect more feedback |
Hi everyone
So we are using the SQL builder to create query from OData, and we want to use the FOR AUTO JSON to have results directly wrapped in a correct JSON. To do that we have to use subqueries
For example the following OData "odata/contacts?$expand=Addresses,Car" should be translated to
SELECT Contacts.*, (SELECT Addresses.* FROM Addresses WHERE Addresses.ContactId = Contacts.Id FOR JSON AUTO) As Addresses, (SELECT Cars.* FROM Cars WHERE Cars.ContactId = Contacts.Id FOR JSON AUTO) As Car FROM Contacts FOR JSON AUTO
I'm able to construct each select requests (I can have the relation from the EDM model of OData) and I know I can use subqueries in SQLKata, but what I don't know is how to add the "FOR JSON AUTO" at the end of each query,
Is there a way to do it?
Thanks for your help
The text was updated successfully, but these errors were encountered: