From 90734af070012c3ff1e2eadf5c570dfc3518906a Mon Sep 17 00:00:00 2001 From: bchavez Date: Wed, 11 May 2016 00:18:14 -0700 Subject: [PATCH] Fixes #49 also better fixes #39 with proper ConvertPseudoTypes usage. --- HISTORY.md | 2 + .../ReQL/BinaryTests.cs | 2 +- .../ReQL/DateAndTimeTests.cs | 48 +++++++++++++++---- .../ReQL/GitHubIssues.cs | 47 ++++++++++++++++++ Source/RethinkDb.Driver/Ast/TopLevel.cs | 46 ++++++++++++++++++ Source/RethinkDb.Driver/Ast/Util.cs | 44 +++-------------- .../Generated/Model/TopLevel.cs | 2 +- Source/RethinkDb.Driver/Net/Connection.cs | 12 +++-- Source/RethinkDb.Driver/Net/Converter.cs | 4 +- Source/RethinkDb.Driver/Net/Cursor.cs | 3 +- .../JsonConverters/ReqlDateTimeConverter.cs | 9 +++- .../RethinkDb.Driver/RethinkDb.Driver.csproj | 1 + .../Templates/CodeGen/TopLevelTemplate.cshtml | 2 +- .../CodeGen/TopLevelTemplate.generated.cs | 31 ++++++------ 14 files changed, 179 insertions(+), 74 deletions(-) create mode 100644 Source/RethinkDb.Driver/Ast/TopLevel.cs diff --git a/HISTORY.md b/HISTORY.md index d8b7463..76288ee 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,8 @@ * BREAKING: Issue 39 - Pseudo types are now converted by default in JToken types (JObject, JArray). * You'll need to specify .Run*(conn, new { time_format: `raw` }) to keep raw types * from being converted. Other raw types: binary_format and group_format. +* BREAKING: Issue 49 - Handle DateTime and DateTimeOffset with ReqlDateTimeConverter +* instead of Iso8601 AST term. ## v2.3.1-beta-1 * Compatibility with RethinkDB 2.3 and new user/pass authentication system. diff --git a/Source/RethinkDb.Driver.Tests/ReQL/BinaryTests.cs b/Source/RethinkDb.Driver.Tests/ReQL/BinaryTests.cs index 8e63c64..d87a80d 100644 --- a/Source/RethinkDb.Driver.Tests/ReQL/BinaryTests.cs +++ b/Source/RethinkDb.Driver.Tests/ReQL/BinaryTests.cs @@ -21,7 +21,7 @@ public void binary_echo() [Test] public void can_get_raw_binary_type() { - JObject reqlType = R.binary(new byte[] { 1, 2, 3 }).Run(conn); + JObject reqlType = R.binary(new byte[] { 1, 2, 3 }).Run(conn, new {binary_format = "raw"}); reqlType[Converter.PseudoTypeKey].ToString().Should().Be("BINARY"); } diff --git a/Source/RethinkDb.Driver.Tests/ReQL/DateAndTimeTests.cs b/Source/RethinkDb.Driver.Tests/ReQL/DateAndTimeTests.cs index 7537e22..1391b4d 100644 --- a/Source/RethinkDb.Driver.Tests/ReQL/DateAndTimeTests.cs +++ b/Source/RethinkDb.Driver.Tests/ReQL/DateAndTimeTests.cs @@ -50,28 +50,61 @@ public void datetime_expr_utctime() } [Test] - public void unspecified_date_time() + public void unspecified_date_time_should_come_back_local_by_default() { var date = new DateTime(2015, 11, 14, 1, 2, 3, DateTimeKind.Unspecified); - //ISO 8601 string has no time zone, and no default time zone was provided. + var result = R.Expr(date).RunResult(conn); + + var expected = new DateTime(2015, 11, 14, 1, 2, 3, DateTimeKind.Local); + + result.Should().BeCloseTo(expected, (int)TimeSpan.FromMinutes(30).TotalMilliseconds); + } + [Test] + public void should_be_able_to_use_datetime_with_iso8601() + { + var date = new DateTime(2015, 11, 14, 1, 2, 3, DateTimeKind.Unspecified); + + //ISO 8601 string has no time zone, and no default time zone was provided. Action action = () => - { - DateTime result = R.Expr(date).Run(conn); - }; + { + DateTime result = R.Iso8601(date).Run(conn); + }; action.ShouldThrow("DateTime unspecified timezone should not be ISO8601 valid."); } + [Test] + public void can_convert_utc_datetime_to_epochtime() + { + var date = new DateTime(2015, 11, 14, 1, 2, 3, DateTimeKind.Utc); + + var result = R.EpochTime(date).RunResult(conn); + + result.Should().BeCloseTo(date, (int)TimeSpan.FromMinutes(30).TotalMilliseconds); + } + + [Test] + public void can_convert_local_datetime_to_epochtime() + { + var date = new DateTime(2015, 11, 14, 1, 2, 3, DateTimeKind.Local); + + var result = R.EpochTime(date).RunResult(conn); + + result.Should().BeCloseTo(date.ToUniversalTime(), (int)TimeSpan.FromMinutes(30).TotalMilliseconds); + } + [Test] public void unspecified_date_time_with_default_timezone() { var date = new DateTime(2015, 11, 14, 1, 2, 3, DateTimeKind.Unspecified); + var datestr = date.ToString("o"); + var default_timezone = new {default_timezone = "-09:00"}; - DateTime result = (R.Expr(date) as Iso8601)[default_timezone].Run(conn); + DateTime result = R.Iso8601(datestr)[default_timezone].Run(conn); var dateTimeUtc = new DateTime(2015, 11, 14, 1, 2, 3) + TimeSpan.FromHours(9); @@ -81,13 +114,12 @@ public void unspecified_date_time_with_default_timezone() DateTime result2 = R.Iso8601(withoutTimezone)[default_timezone].Run(conn); result2.ToUniversalTime().Should().BeCloseTo(dateTimeUtc, 1); - } [Test] public void use_raw_object() { - JObject result = R.Now().Run(conn); + JObject result = R.Now().Run(conn, new {time_format="raw"}); //ten minute limit for clock drift. result["$reql_type$"].ToString().Should().Be("TIME"); diff --git a/Source/RethinkDb.Driver.Tests/ReQL/GitHubIssues.cs b/Source/RethinkDb.Driver.Tests/ReQL/GitHubIssues.cs index e74d5ca..90fec7b 100644 --- a/Source/RethinkDb.Driver.Tests/ReQL/GitHubIssues.cs +++ b/Source/RethinkDb.Driver.Tests/ReQL/GitHubIssues.cs @@ -172,6 +172,53 @@ public void issue_41_ensure_run_helpers_throw_error_first_before_direct_conversi action.ShouldThrow(); } + + public class Issue49 + { + [JsonProperty("id")] + public int Id = 1; + public DateTime BigBang { get; set; } + } + + [Test] + public void issue_49_use_reqldatetimeconverter_for_dates_in_ast() + { + ClearDefaultTable(); + + var mindate = DateTime.MinValue.ToUniversalTime(); + + var insertResult = R + .Db(DbName) + .Table(TableName) + .Insert(new Issue49 {BigBang = mindate}) + .RunResult(conn); + + insertResult.Errors.Should().Be(0); + + var updateResult = R + .Db(DbName) + .Table(TableName) + .Get(1) + .Update(orig => + { + var unchanged = orig["BigBang"].Eq(mindate); + return R.Error(unchanged.CoerceTo("string")); + }) + .OptArg("return_changes", true) + .RunResult(conn); + + updateResult.Errors.Should().Be(1); + updateResult.FirstError.Should().Be("true"); + + var filter = + R.Db(DbName) + .Table(TableName) + .Filter(x => x["BigBang"].Eq(mindate)) + .RunResult>(conn); + + filter.Count.Should().Be(1); + filter[0].BigBang.Should().Be(mindate); + } } } diff --git a/Source/RethinkDb.Driver/Ast/TopLevel.cs b/Source/RethinkDb.Driver/Ast/TopLevel.cs new file mode 100644 index 0000000..2f40756 --- /dev/null +++ b/Source/RethinkDb.Driver/Ast/TopLevel.cs @@ -0,0 +1,46 @@ +#pragma warning disable 1591 // Missing XML comment for publicly visible type or member + +using System; +using RethinkDb.Driver.Net.JsonConverters; + +namespace RethinkDb.Driver.Ast +{ + public partial class TopLevel + { + /// + /// Type-safe helper method for R.Iso8601 + /// + public Iso8601 Iso8601(DateTime? datetime) + { + var str = datetime?.ToString("o"); + return Ast.Iso8601.FromString(str); + } + /// + /// Type-safe helper method for R.Iso8601 + /// + public Iso8601 Iso8601(DateTimeOffset? datetime) + { + var str = datetime?.ToString("o"); + return Ast.Iso8601.FromString(str); + } + + /// + /// Type-safe helper for R.EpochTime + /// + public EpochTime EpochTime(DateTime? datetime) + { + var ticks = datetime?.ToUniversalTime().Ticks; + var epoch = ReqlDateTimeConverter.ToUnixTime(ticks.Value); + return EpochTime(epoch); + } + /// + /// Type-safe helper for R.EpochTime + /// + public EpochTime EpochTime(DateTimeOffset? datetime) + { + var ticks = datetime?.UtcTicks; + var epoch = ReqlDateTimeConverter.ToUnixTime(ticks.Value); + return EpochTime(epoch); + } + } +} \ No newline at end of file diff --git a/Source/RethinkDb.Driver/Ast/Util.cs b/Source/RethinkDb.Driver/Ast/Util.cs index d5cf43c..ca40e00 100644 --- a/Source/RethinkDb.Driver/Ast/Util.cs +++ b/Source/RethinkDb.Driver/Ast/Util.cs @@ -39,38 +39,10 @@ private static ReqlAst ToReqlAst(object val, int remainingDepth) return ast; } - //GitHub Issue #21 - Request - Allow Inserting JObject var token = val as JToken; if( token != null ) { - //First way to do it is to convert the whole thing into JSON and have - //the server parse it. One thing we'd need to watch out for is - //the DateTime conversion. ReqlDateTimeConverter would need to change - //so that it directly serializes to $reql_type$:TIME instead of an AST - //Iso8601(string) with [49,...]. It's probably more proper to serialize - //to $reql_type$ than a ReQL Iso8601(string)[49, ...]. - //var json = token.ToString(Formatting.None, Converter.Serializer.Converters.ToArray()); - //return new Json(json); return new Poco(token); - - //Another way to do it: De-construct the JObject like we do to an IDictionary... - //Pro: More complete - //Con: Easy to get wrong, lots more code, code-duplication with IDictionary - // - //if (token.Type == JTokenType.Object) - //{ - // var jobj = val as IDictionary; - // var obj = new Dictionary(); - // foreach (var t in jobj) - // { - // obj[t.Key] = ToReqlAst(t.Value); - // } - // return MakeObj.fromMap(obj); - //} - //else if (token.Type == JTokenType.Bytes) .. and for each supported JSON native type. - //{ - // - //} } var lst = val as IList; @@ -107,21 +79,17 @@ private static ReqlAst ToReqlAst(object val, int remainingDepth) return Func.FromLambda(del); } - - if( val is DateTime ) + var dt = val as DateTime?; + if (dt != null) { - var dt = (DateTime)val; - var isoStr = dt.ToString("o"); - return Iso8601.FromString(isoStr); + return new Poco(dt); } - if( val is DateTimeOffset ) + var dto = val as DateTimeOffset?; + if (dto != null) { - var dt = (DateTimeOffset)val; - var isoStr = dt.ToString("o"); - return Iso8601.FromString(isoStr); + return new Poco(dto); } - var @int = val as int?; if( @int != null ) { diff --git a/Source/RethinkDb.Driver/Generated/Model/TopLevel.cs b/Source/RethinkDb.Driver/Generated/Model/TopLevel.cs index 18ca837..1ba3dcd 100644 --- a/Source/RethinkDb.Driver/Generated/Model/TopLevel.cs +++ b/Source/RethinkDb.Driver/Generated/Model/TopLevel.cs @@ -20,7 +20,7 @@ using RethinkDb.Driver.Ast; namespace RethinkDb.Driver.Ast { - public class TopLevel { + public partial class TopLevel { public ReqlExpr Expr(Object value){ return Util.ToReqlExpr(value); diff --git a/Source/RethinkDb.Driver/Net/Connection.cs b/Source/RethinkDb.Driver/Net/Connection.cs index 26c6970..c4b0455 100644 --- a/Source/RethinkDb.Driver/Net/Connection.cs +++ b/Source/RethinkDb.Driver/Net/Connection.cs @@ -278,7 +278,8 @@ protected virtual async Task RunQueryAtomAsync(Query query, CancellationTo if( typeof(T).IsJToken() ) { var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); - return (T)Converter.ConvertPseudoTypes(res.Data[0], fmt); + Converter.ConvertPseudoTypes(res.Data[0], fmt); + return (T)(object)res.Data[0]; //ugh ugly. find a better way to do this. } return res.Data[0].ToObject(Converter.Serializer); @@ -310,7 +311,8 @@ private async Task RunQueryResultAsync(Query query, CancellationToken canc if( typeof(T).IsJToken() ) { var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); - return (T)Converter.ConvertPseudoTypes(res.Data[0], fmt); + Converter.ConvertPseudoTypes(res.Data[0], fmt); + return (T)(object)res.Data[0]; //ugh ugly. find a better way to do this. } return res.Data[0].ToObject(Converter.Serializer); } @@ -324,7 +326,8 @@ private async Task RunQueryResultAsync(Query query, CancellationToken canc if( typeof(T).IsJToken() ) { var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); - return (T)Converter.ConvertPseudoTypes(res.Data, fmt); + Converter.ConvertPseudoTypes(res.Data, fmt); + return (T)(object)res.Data; //ugh ugly. find a better way to do this. } return res.Data.ToObject(Converter.Serializer); } @@ -381,7 +384,8 @@ protected async Task RunQueryAsync(Query query, CancellationToken ca if( typeof(T).IsJToken() ) { var fmt = FormatOptions.FromOptArgs(query.GlobalOptions); - return Converter.ConvertPseudoTypes(res.Data[0], fmt); + Converter.ConvertPseudoTypes(res.Data[0], fmt); + return res.Data[0]; } return res.Data[0].ToObject(typeof(T), Converter.Serializer); } diff --git a/Source/RethinkDb.Driver/Net/Converter.cs b/Source/RethinkDb.Driver/Net/Converter.cs index 941d866..fca9929 100644 --- a/Source/RethinkDb.Driver/Net/Converter.cs +++ b/Source/RethinkDb.Driver/Net/Converter.cs @@ -101,7 +101,7 @@ public static void InitializeDefault() /// /// Method for converting pseudo types in JToken (JObjects) /// - public static object ConvertPseudoTypes(JToken data, FormatOptions fmt) + public static void ConvertPseudoTypes(JToken data, FormatOptions fmt) { var reqlTypes = data.SelectTokens("$..$reql_type$").ToList(); @@ -143,8 +143,6 @@ public static object ConvertPseudoTypes(JToken data, FormatOptions fmt) pesudoObject.Replace(convertedValue); } - - return data; } private static object GetTime(JObject value) diff --git a/Source/RethinkDb.Driver/Net/Cursor.cs b/Source/RethinkDb.Driver/Net/Cursor.cs index a4f1077..bb2a3c0 100644 --- a/Source/RethinkDb.Driver/Net/Cursor.cs +++ b/Source/RethinkDb.Driver/Net/Cursor.cs @@ -214,7 +214,8 @@ T Convert(JToken token) { if( typeof(T).IsJToken() ) { - return (T)Converter.ConvertPseudoTypes(token, fmt); + Converter.ConvertPseudoTypes(token, fmt); + return (T)(object)token; //ugh ugly. find a better way to do this. } return token.ToObject(Converter.Serializer); } diff --git a/Source/RethinkDb.Driver/Net/JsonConverters/ReqlDateTimeConverter.cs b/Source/RethinkDb.Driver/Net/JsonConverters/ReqlDateTimeConverter.cs index 165b991..1eeb139 100644 --- a/Source/RethinkDb.Driver/Net/JsonConverters/ReqlDateTimeConverter.cs +++ b/Source/RethinkDb.Driver/Net/JsonConverters/ReqlDateTimeConverter.cs @@ -31,9 +31,14 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } //http://stackoverflow.com/questions/2883576/how-do-you-convert-epoch-time-in-c - public double ToUnixTime(DateTimeOffset date) + public static double ToUnixTime(DateTimeOffset date) { - return (date.UtcTicks - 621355968000000000) / 10000000.0; + return ToUnixTime(date.UtcTicks); + } + + public static double ToUnixTime(long utcTicks) + { + return (utcTicks - 621355968000000000) / 10000000.0; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) diff --git a/Source/RethinkDb.Driver/RethinkDb.Driver.csproj b/Source/RethinkDb.Driver/RethinkDb.Driver.csproj index 1aa7a5d..ecf2310 100644 --- a/Source/RethinkDb.Driver/RethinkDb.Driver.csproj +++ b/Source/RethinkDb.Driver/RethinkDb.Driver.csproj @@ -55,6 +55,7 @@ + diff --git a/Source/Templates/CodeGen/TopLevelTemplate.cshtml b/Source/Templates/CodeGen/TopLevelTemplate.cshtml index 0c9443a..6844b77 100644 --- a/Source/Templates/CodeGen/TopLevelTemplate.cshtml +++ b/Source/Templates/CodeGen/TopLevelTemplate.cshtml @@ -41,7 +41,7 @@ using RethinkDb.Driver.Model; using RethinkDb.Driver.Ast; namespace RethinkDb.Driver.Ast { - public class TopLevel { + public partial class TopLevel { public ReqlExpr Expr(Object value){ return Util.ToReqlExpr(value); diff --git a/Source/Templates/CodeGen/TopLevelTemplate.generated.cs b/Source/Templates/CodeGen/TopLevelTemplate.generated.cs index 38c5d5e..500b0e1 100644 --- a/Source/Templates/CodeGen/TopLevelTemplate.generated.cs +++ b/Source/Templates/CodeGen/TopLevelTemplate.generated.cs @@ -154,21 +154,22 @@ public override void Execute() "ragma warning disable 1591 // Missing XML comment for publicly visible type or m" + "ember\r\n// ReSharper disable CheckNamespace\r\n\r\nusing System;\r\nusing System.Linq;\r" + "\nusing System.Collections;\r\nusing RethinkDb.Driver.Model;\r\nusing RethinkDb.Drive" + -"r.Ast;\r\n\r\nnamespace RethinkDb.Driver.Ast {\r\n public class TopLevel {\r\n\r\n " + -" public ReqlExpr Expr(Object value){\r\n return Util.ToReqlExpr(value" + -");\r\n\r\n }\r\n internal ReqlExpr expr(Object value){\r\n retu" + -"rn Expr(value);\r\n }\r\n\r\n\r\n public ReqlExpr Row(params object[] valu" + -"es) {\r\n throw new ReqlDriverError(\"r.row is not implemented in the C#" + -" driver. Use lambda syntax instead.\");\r\n }\r\n internal ReqlExpr row" + -"(params object[] values) {\r\n return Row(values);\r\n }\r\n\r\n " + -" public MapObject HashMap(object key, object val) {\r\n return new Map" + -"Object().With(key, val);\r\n }\r\n internal MapObject hashMap(object k" + -"ey, object val) {\r\n return new MapObject().With(key, val);\r\n }" + -"\r\n\r\n public MapObject HashMap()\r\n {\r\n return new MapObj" + -"ect();\r\n }\r\n internal MapObject hashMap()\r\n {\r\n " + -"return HashMap();\r\n }\r\n\r\n\r\n public IList Array(params object[] val" + -"ues){\r\n return values.ToList();\r\n }\r\n internal IList ar" + -"ray(params object[] values){\r\n return Array(values);\r\n }\r\n\r\n"); +"r.Ast;\r\n\r\nnamespace RethinkDb.Driver.Ast {\r\n public partial class TopLevel {\r" + +"\n\r\n public ReqlExpr Expr(Object value){\r\n return Util.ToReqlEx" + +"pr(value);\r\n\r\n }\r\n internal ReqlExpr expr(Object value){\r\n " + +" return Expr(value);\r\n }\r\n\r\n\r\n public ReqlExpr Row(params objec" + +"t[] values) {\r\n throw new ReqlDriverError(\"r.row is not implemented i" + +"n the C# driver. Use lambda syntax instead.\");\r\n }\r\n internal Reql" + +"Expr row(params object[] values) {\r\n return Row(values);\r\n }\r\n" + +"\r\n public MapObject HashMap(object key, object val) {\r\n return" + +" new MapObject().With(key, val);\r\n }\r\n internal MapObject hashMap(" + +"object key, object val) {\r\n return new MapObject().With(key, val);\r\n " + +" }\r\n\r\n public MapObject HashMap()\r\n {\r\n return ne" + +"w MapObject();\r\n }\r\n internal MapObject hashMap()\r\n {\r\n " + +" return HashMap();\r\n }\r\n\r\n\r\n public IList Array(params obje" + +"ct[] values){\r\n return values.ToList();\r\n }\r\n internal " + +"IList array(params object[] values){\r\n return Array(values);\r\n " + +" }\r\n\r\n");