Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit d86f94b

Browse files
authored
Merge pull request #590 from KevinHoward/master
Added 3 JSON Sql methods to the SQLServer 2016 Dialect Provider
2 parents e862b64 + f06100c commit d86f94b

File tree

7 files changed

+273
-3
lines changed

7 files changed

+273
-3
lines changed

src/ServiceStack.OrmLite.SqlServer.Converters/ServiceStack.OrmLite.SqlServer.Converters.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
</PropertyGroup>
3030

3131
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
32-
<PackageReference Include="Microsoft.SqlServer.Types" Version="11.0.2" />
32+
<PackageReference Include="Microsoft.SqlServer.Types" Version="14.0.314.76" />
3333
<ProjectReference Include="..\ServiceStack.OrmLite\ServiceStack.OrmLite.csproj" />
3434
<ProjectReference Include="..\ServiceStack.OrmLite.SqlServer\ServiceStack.OrmLite.SqlServer.csproj" />
3535
<Reference Include="..\..\lib\net45\ServiceStack.Text.dll" />
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Data;
4+
using System.Linq.Expressions;
5+
using System.Text;
6+
using ServiceStack.OrmLite;
7+
using ServiceStack.OrmLite.SqlServer.Converters;
8+
using ServiceStack.Text;
9+
10+
namespace ServiceStack.OrmLite.SqlServer
11+
{
12+
public class SqlServer2016Expression<T> : SqlServerExpression<T>
13+
{
14+
public SqlServer2016Expression(IOrmLiteDialectProvider dialectProvider)
15+
: base(dialectProvider) {}
16+
17+
protected override object VisitSqlMethodCall(MethodCallExpression m)
18+
{
19+
List<object> args = VisitInSqlExpressionList(m.Arguments);
20+
object quotedColName = args[0];
21+
args.RemoveAt(0);
22+
23+
string statement;
24+
25+
switch (m.Method.Name)
26+
{
27+
case nameof(Sql.In):
28+
statement = ConvertInExpressionToSql(m, quotedColName);
29+
break;
30+
case nameof(Sql.Desc):
31+
statement = $"{quotedColName} DESC";
32+
break;
33+
case nameof(Sql.As):
34+
statement = $"{quotedColName} AS {DialectProvider.GetQuotedColumnName(RemoveQuoteFromAlias(args[0].ToString()))}";
35+
break;
36+
case nameof(Sql.Sum):
37+
case nameof(Sql.Count):
38+
case nameof(Sql.Min):
39+
case nameof(Sql.Max):
40+
case nameof(Sql.Avg):
41+
statement = $"{m.Method.Name}({quotedColName}{(args.Count == 1 ? $",{args[0]}" : "")})";
42+
break;
43+
case nameof(Sql.CountDistinct):
44+
statement = $"COUNT(DISTINCT {quotedColName})";
45+
break;
46+
case nameof(Sql.AllFields):
47+
var argDef = m.Arguments[0].Type.GetModelMetadata();
48+
statement = DialectProvider.GetQuotedTableName(argDef) + ".*";
49+
break;
50+
case nameof(Sql.JoinAlias):
51+
statement = args[0] + "." + quotedColName.ToString().LastRightPart('.');
52+
break;
53+
case nameof(Sql.Custom):
54+
statement = quotedColName.ToString();
55+
break;
56+
case nameof(Sql2016.IsJson):
57+
statement = $"ISJSON({quotedColName})";
58+
break;
59+
case nameof(Sql2016.JsonValue):
60+
statement = $"JSON_VALUE({quotedColName}, '{args[0]}')";
61+
break;
62+
case nameof(Sql2016.JsonQuery):
63+
statement = $"JSON_QUERY({quotedColName}";
64+
if (DialectProvider is SqlServer2017OrmLiteDialectProvider && args.Count > 0)
65+
{
66+
statement += $", '{args[0]}'";
67+
}
68+
statement += ")";
69+
break;
70+
default:
71+
throw new NotSupportedException();
72+
}
73+
74+
return new PartialSqlString(statement);
75+
}
76+
}
77+
}
78+
79+
namespace ServiceStack.OrmLite
80+
{
81+
public static class Sql2016
82+
{
83+
/// <summary>Tests whether a string contains valid JSON.</summary>
84+
/// <param name="expression">The string to test.</param>
85+
/// <returns>Returns True if the string contains valid JSON; otherwise, returns False. Returns null if expression is null.</returns>
86+
/// <remarks>ISJSON does not check the uniqueness of keys at the same level.</remarks>
87+
/// <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/isjson-transact-sql"/>
88+
public static bool? IsJson(string expression) => null;
89+
90+
/// <summary>Extracts a scalar value from a JSON string.</summary>
91+
/// <param name="expression">
92+
/// An expression. Typically the name of a variable or a column that contains JSON text.<br/><br/>
93+
/// If <b>JSON_VALUE</b> finds JSON that is not valid in expression before it finds the value identified by <i>path</i>, the function returns an error. If <b>JSON_VALUE</b> doesn't find the value identified by <i>path</i>, it scans the entire text and returns an error if it finds JSON that is not valid anywhere in <i>expression</i>.
94+
/// </param>
95+
/// <param name="path">
96+
/// A JSON path that specifies the property to extract. For more info, see <see cref="https://docs.microsoft.com/en-us/sql/relational-databases/json/json-path-expressions-sql-server">JSON Path Expressions (SQL Server)</see>.<br/><br/>
97+
/// In SQL Server 2017 and in Azure SQL Database, you can provide a variable as the value of <i>path</i>.<br/><br/>
98+
/// If the format of path isn't valid, <b>JSON_VALUE</b> returns an error.<br/><br/>
99+
/// </param>
100+
/// <returns>
101+
/// Returns a single text value of type nvarchar(4000). The collation of the returned value is the same as the collation of the input expression.
102+
/// If the value is greater than 4000 characters: <br/><br/>
103+
/// <ul>
104+
/// <li>In lax mode, <b>JSON_VALUE</b> returns null.</li>
105+
/// <li>In strict mode, <b>JSON_VALUE</b> returns an error.</li>
106+
/// </ul>
107+
/// <br/>
108+
/// If you have to return scalar values greater than 4000 characters, use <b>OPENJSON</b> instead of <b>JSON_VALUE</b>. For more info, see <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql">OPENJSON (Transact-SQL)</see>.
109+
/// </returns>
110+
/// <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/json-value-transact-sql"/>
111+
public static T JsonValue<T>(string expression, string path) => default(T);
112+
113+
/// <summary>Extracts a scalar value from a JSON string.</summary>
114+
/// <param name="expression">
115+
/// An expression. Typically the name of a variable or a column that contains JSON text.<br/><br/>
116+
/// If <b>JSON_VALUE</b> finds JSON that is not valid in expression before it finds the value identified by <i>path</i>, the function returns an error. If <b>JSON_VALUE</b> doesn't find the value identified by <i>path</i>, it scans the entire text and returns an error if it finds JSON that is not valid anywhere in <i>expression</i>.
117+
/// </param>
118+
/// <param name="path">
119+
/// A JSON path that specifies the property to extract. For more info, see <see cref="https://docs.microsoft.com/en-us/sql/relational-databases/json/json-path-expressions-sql-server">JSON Path Expressions (SQL Server)</see>.<br/><br/>
120+
/// In SQL Server 2017 and in Azure SQL Database, you can provide a variable as the value of <i>path</i>.<br/><br/>
121+
/// If the format of path isn't valid, <b>JSON_VALUE</b> returns an error.<br/><br/>
122+
/// </param>
123+
/// <returns>
124+
/// Returns a single text value of type nvarchar(4000). The collation of the returned value is the same as the collation of the input expression.
125+
/// If the value is greater than 4000 characters: <br/><br/>
126+
/// <ul>
127+
/// <li>In lax mode, <b>JSON_VALUE</b> returns null.</li>
128+
/// <li>In strict mode, <b>JSON_VALUE</b> returns an error.</li>
129+
/// </ul>
130+
/// <br/>
131+
/// If you have to return scalar values greater than 4000 characters, use <b>OPENJSON</b> instead of <b>JSON_VALUE</b>. For more info, see <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql">OPENJSON (Transact-SQL)</see>.
132+
/// </returns>
133+
/// <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/json-value-transact-sql"/>
134+
public static string JsonValue(string expression, string path) =>
135+
$"JSON_VALUE({expression}, '{path}')";
136+
137+
/// <summary>
138+
/// Extracts an object or an array from a JSON string.<br/><br/>
139+
/// To extract a scalar value from a JSON string instead of an object or an array, see <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/json-value-transact-sql">JSON_VALUE(Transact-SQL)</see>.
140+
/// For info about the differences between <b>JSON_VALUE</b> and <b>JSON_QUERY</b>, see <see cref="https://docs.microsoft.com/en-us/sql/relational-databases/json/validate-query-and-change-json-data-with-built-in-functions-sql-server#JSONCompare">Compare JSON_VALUE and JSON_QUERY</see>.
141+
/// </summary>
142+
/// <typeparam name="T">Type of objects returned</typeparam>
143+
/// <param name="expression">
144+
/// An expression. Typically the name of a variable or a column that contains JSON text.<br/><br/>
145+
/// If <b>JSON_QUERY</b> finds JSON that is not valid in <i>expression</i> before it finds the value identified by <i>path</i>, the function returns an error. If <b>JSON_QUERY</b> doesn't find the value identified by <i>path</i>, it scans the entire text and returns an error if it finds JSON that is not valid anywhere in <i>expression</i>.
146+
/// </param>
147+
/// <param name="path">
148+
/// A JSON path that specifies the object or the array to extract.<br/><br/>
149+
/// In SQL Server 2017 and in Azure SQL Database, you can provide a variable as the value of <i>path</i>.<br/><br/>
150+
/// The JSON path can specify lax or strict mode for parsing.If you don't specify the parsing mode, lax mode is the default. For more info, see <see cref="https://docs.microsoft.com/en-us/sql/relational-databases/json/json-path-expressions-sql-server">JSON Path Expressions (SQL Server)</see>.<br/><br/>
151+
/// The default value for path is '$'. As a result, if you don't provide a value for path, <b>JSON_QUERY</b> returns the input <i>expression</i>.<br/><br/>
152+
/// If the format of <i>path</i> isn't valid, <b>JSON_QUERY</b> returns an error.
153+
/// </param>
154+
/// <returns>
155+
/// Returns a JSON fragment of type T. The collation of the returned value is the same as the collation of the input expression.<br/><br/>
156+
/// If the value is not an object or an array:
157+
/// <ul>
158+
/// <li>In lax mode, <b>JSON_QUERY</b> returns null.</li>
159+
/// <li>In strict mode, <b>JSON_QUERY</b> returns an error.</li>
160+
/// </ul>
161+
/// </returns>
162+
public static T JsonQuery<T>(string expression, string path = null) => default(T);
163+
164+
/// <summary>
165+
/// Extracts an object or an array from a JSON string.<br/><br/>
166+
/// To extract a scalar value from a JSON string instead of an object or an array, see <see cref="https://docs.microsoft.com/en-us/sql/t-sql/functions/json-value-transact-sql">JSON_VALUE(Transact-SQL)</see>.
167+
/// For info about the differences between <b>JSON_VALUE</b> and <b>JSON_QUERY</b>, see <see cref="https://docs.microsoft.com/en-us/sql/relational-databases/json/validate-query-and-change-json-data-with-built-in-functions-sql-server#JSONCompare">Compare JSON_VALUE and JSON_QUERY</see>.
168+
/// </summary>
169+
/// <typeparam name="T">Type of objects returned</typeparam>
170+
/// <param name="expression">
171+
/// An expression. Typically the name of a variable or a column that contains JSON text.<br/><br/>
172+
/// If <b>JSON_QUERY</b> finds JSON that is not valid in <i>expression</i> before it finds the value identified by <i>path</i>, the function returns an error. If <b>JSON_QUERY</b> doesn't find the value identified by <i>path</i>, it scans the entire text and returns an error if it finds JSON that is not valid anywhere in <i>expression</i>.
173+
/// </param>
174+
/// <param name="path">
175+
/// A JSON path that specifies the object or the array to extract.<br/><br/>
176+
/// In SQL Server 2017 and in Azure SQL Database, you can provide a variable as the value of <i>path</i>.<br/><br/>
177+
/// The JSON path can specify lax or strict mode for parsing.If you don't specify the parsing mode, lax mode is the default. For more info, see <see cref="https://docs.microsoft.com/en-us/sql/relational-databases/json/json-path-expressions-sql-server">JSON Path Expressions (SQL Server)</see>.<br/><br/>
178+
/// The default value for path is '$'. As a result, if you don't provide a value for path, <b>JSON_QUERY</b> returns the input <i>expression</i>.<br/><br/>
179+
/// If the format of <i>path</i> isn't valid, <b>JSON_QUERY</b> returns an error.
180+
/// </param>
181+
/// <returns>
182+
/// Returns a JSON fragment of type T. The collation of the returned value is the same as the collation of the input expression.<br/><br/>
183+
/// If the value is not an object or an array:
184+
/// <ul>
185+
/// <li>In lax mode, <b>JSON_QUERY</b> returns null.</li>
186+
/// <li>In strict mode, <b>JSON_QUERY</b> returns an error.</li>
187+
/// </ul>
188+
/// </returns>
189+
public static string JsonQuery(string expression, string path = null) =>
190+
(path.Contains("$"))
191+
? $"JSON_QUERY({expression}, '{path}')"
192+
: $"JSON_QUERY({expression})";
193+
}
194+
}

src/ServiceStack.OrmLite.SqlServer/SqlServer2016OrmLiteDialectProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ namespace ServiceStack.OrmLite.SqlServer
88
public class SqlServer2016OrmLiteDialectProvider : SqlServer2014OrmLiteDialectProvider
99
{
1010
public new static SqlServer2016OrmLiteDialectProvider Instance = new SqlServer2016OrmLiteDialectProvider();
11+
12+
public override SqlExpression<T> SqlExpression<T>() => new SqlServer2016Expression<T>(this);
1113
}
1214
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Data;
3+
using System.Text;
4+
using ServiceStack.Text;
5+
6+
namespace ServiceStack.OrmLite.SqlServer
7+
{
8+
public class SqlServer2017OrmLiteDialectProvider : SqlServer2016OrmLiteDialectProvider
9+
{
10+
public new static SqlServer2017OrmLiteDialectProvider Instance = new SqlServer2017OrmLiteDialectProvider();
11+
}
12+
}

src/ServiceStack.OrmLite.SqlServer/SqlServerDialect.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ public static class SqlServer2016Dialect
2121
{
2222
public static IOrmLiteDialectProvider Provider => SqlServer2016OrmLiteDialectProvider.Instance;
2323
}
24+
25+
public static class SqlServer2017Dialect
26+
{
27+
public static IOrmLiteDialectProvider Provider => SqlServer2017OrmLiteDialectProvider.Instance;
28+
}
2429
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Data;
3+
using System.Linq;
4+
using NUnit.Framework;
5+
6+
namespace ServiceStack.OrmLite.SqlServerTests.Expressions
7+
{
8+
public class JsonExpressionsTest : ExpressionsTestBase
9+
{
10+
[Test]
11+
public void Can_select_json_scalar_value()
12+
{
13+
OrmLiteConfig.DialectProvider = SqlServer2016Dialect.Provider;
14+
15+
OpenDbConnection().CreateTableIfNotExists<TestType>();
16+
OpenDbConnection().DeleteAll<TestType>();
17+
18+
var obj = new { Address = new Address { Line1 = "1234 Main Street", Line2 = "Apt. 404", City = "Las Vegas", State = "NV" } };
19+
var stringValue = obj.ToJson(); //"{ \"Address\": { \"Line1\": \"1234 Main Street\", \"Line2\": \"Apt. 404\", \"City\": \"Las Vegas\", \"State\": \"NV\" } }"
20+
21+
OpenDbConnection().Insert(new TestType() { StringColumn = stringValue });
22+
23+
var actual = OpenDbConnection().Select<TestType>(q =>
24+
Sql2016.JsonValue(q.StringColumn, "$.address.state") == "NV");
25+
26+
Assert.AreEqual(obj.Address.State, actual);
27+
}
28+
29+
[Test]
30+
public void Can_select_json_object_value()
31+
{
32+
OrmLiteConfig.DialectProvider = SqlServer2016Dialect.Provider;
33+
34+
OpenDbConnection().CreateTableIfNotExists<TestType>();
35+
OpenDbConnection().DeleteAll<TestType>();
36+
37+
var expected = new Address { Line1 = "1234 Main Street", Line2 = "Apt. 404", City = "Las Vegas", State = "NV" };
38+
var obj = new { Address = expected };
39+
var stringValue = obj.ToJson(); //"{ \"Address\": { \"Line1\": \"1234 Main Street\", \"Line2\": \"Apt. 404\", \"City\": \"Las Vegas\", \"State\": \"NV\" } }"
40+
41+
OpenDbConnection().Insert(new TestType() { StringColumn = stringValue });
42+
43+
var address = OpenDbConnection().From<TestType>().Select(q =>
44+
Sql2016.JsonQuery<Address>(q.StringColumn, "$.address")).ConvertTo<Address>();
45+
46+
Assert.AreEqual(obj.Address, address);
47+
}
48+
49+
internal class Address
50+
{
51+
public string Line1 { get; set; }
52+
public string Line2 { get; set; }
53+
public string City { get; set; }
54+
public string State { get; set; }
55+
}
56+
}
57+
}

src/ServiceStack.OrmLite.SqlServerTests/ServiceStack.OrmLite.SqlServerTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</PropertyGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="NUnit" Version="3.6.1" />
17+
<PackageReference Include="NUnit" Version="3.7.1" />
1818
<Reference Include="..\..\lib\pcl\ServiceStack.Interfaces.dll" />
1919
</ItemGroup>
2020

@@ -23,7 +23,7 @@
2323
</PropertyGroup>
2424

2525
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
26-
<PackageReference Include="Microsoft.SqlServer.Types" Version="11.0.2" />
26+
<PackageReference Include="Microsoft.SqlServer.Types" Version="14.0.314.76" />
2727
<Reference Include="System" />
2828
<Reference Include="System.Configuration" />
2929
<Reference Include="System.Threading" />

0 commit comments

Comments
 (0)