diff --git a/.gitignore b/.gitignore index cc6d6f85..8a05467e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +**/.DS_Store + # test coverage tests/EdgeDB.Tests.Integration/coveragereports CoverageReport @@ -380,3 +382,6 @@ MigrationBackup/ FodyWeavers.xsd src/EdgeDB.ExampleApp/dump.db docs/tmp/* + +# EdgeDB.Net CLI watcher info file +edgeql.dotnet.watcher.process \ No newline at end of file diff --git a/EdgeDB.Net.sln b/EdgeDB.Net.sln index f5b63898..0f7a8940 100644 --- a/EdgeDB.Net.sln +++ b/EdgeDB.Net.sln @@ -31,7 +31,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Examples.ExampleTODO EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "EdgeDB.Examples.FSharp", "examples\EdgeDB.Examples.FSharp\EdgeDB.Examples.FSharp.fsproj", "{F25AA805-163F-46B4-942E-B1A5EBE8383C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdgeDB.DocGenerator", "tools\EdgeDB.DocGenerator\EdgeDB.DocGenerator.csproj", "{776EAE34-5A30-45B0-9277-C399CC2ABA53}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.DocGenerator", "tools\EdgeDB.DocGenerator\EdgeDB.DocGenerator.csproj", "{776EAE34-5A30-45B0-9277-C399CC2ABA53}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Net.CLI", "src\EdgeDB.Net.CLI\EdgeDB.Net.CLI.csproj", "{AC6DAA7B-5510-47BF-9759-1A16565F24A8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Examples.GenerationExample", "examples\EdgeDB.Examples.GenerationExample\EdgeDB.Examples.GenerationExample.csproj", "{EAA9EF1A-0F10-4F74-B2F4-1C5BBC505866}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeGen", "CodeGen", "{C9B0F9DB-E508-4AC1-87E5-BD82A8AB007A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Generated", "examples\EdgeDB.Examples.GenerationExample\EdgeDB.Generated\EdgeDB.Generated.csproj", "{DE74C2DA-4649-44E9-83E9-1E897F61ED6F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -83,6 +91,18 @@ Global {776EAE34-5A30-45B0-9277-C399CC2ABA53}.Debug|Any CPU.Build.0 = Debug|Any CPU {776EAE34-5A30-45B0-9277-C399CC2ABA53}.Release|Any CPU.ActiveCfg = Release|Any CPU {776EAE34-5A30-45B0-9277-C399CC2ABA53}.Release|Any CPU.Build.0 = Release|Any CPU + {AC6DAA7B-5510-47BF-9759-1A16565F24A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC6DAA7B-5510-47BF-9759-1A16565F24A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC6DAA7B-5510-47BF-9759-1A16565F24A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC6DAA7B-5510-47BF-9759-1A16565F24A8}.Release|Any CPU.Build.0 = Release|Any CPU + {EAA9EF1A-0F10-4F74-B2F4-1C5BBC505866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAA9EF1A-0F10-4F74-B2F4-1C5BBC505866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAA9EF1A-0F10-4F74-B2F4-1C5BBC505866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAA9EF1A-0F10-4F74-B2F4-1C5BBC505866}.Release|Any CPU.Build.0 = Release|Any CPU + {DE74C2DA-4649-44E9-83E9-1E897F61ED6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE74C2DA-4649-44E9-83E9-1E897F61ED6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE74C2DA-4649-44E9-83E9-1E897F61ED6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE74C2DA-4649-44E9-83E9-1E897F61ED6F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -99,6 +119,10 @@ Global {E38429C6-53A5-4311-8189-1F78238666DC} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B} {F25AA805-163F-46B4-942E-B1A5EBE8383C} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B} {776EAE34-5A30-45B0-9277-C399CC2ABA53} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA} + {AC6DAA7B-5510-47BF-9759-1A16565F24A8} = {025AAADF-16AF-4367-9C3D-9E60EDED832F} + {EAA9EF1A-0F10-4F74-B2F4-1C5BBC505866} = {C9B0F9DB-E508-4AC1-87E5-BD82A8AB007A} + {C9B0F9DB-E508-4AC1-87E5-BD82A8AB007A} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B} + {DE74C2DA-4649-44E9-83E9-1E897F61ED6F} = {C9B0F9DB-E508-4AC1-87E5-BD82A8AB007A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4E90C94F-D693-4411-82F3-2051DE1BE052} diff --git a/edgedb.toml b/edgedb.toml index e9ced650..257ffb51 100644 --- a/edgedb.toml +++ b/edgedb.toml @@ -1,2 +1,2 @@ [edgedb] -server-version = "nightly" +server-version = "3.0" diff --git a/examples/EdgeDB.Examples.CSharp/Examples/AbstractTypes.cs b/examples/EdgeDB.Examples.CSharp/Examples/AbstractTypes.cs index 5dfec391..bf3e6e21 100644 --- a/examples/EdgeDB.Examples.CSharp/Examples/AbstractTypes.cs +++ b/examples/EdgeDB.Examples.CSharp/Examples/AbstractTypes.cs @@ -43,6 +43,8 @@ public class OtherThing : AbstractThing public async Task ExecuteAsync(EdgeDBClient client) { + var aa = await client.QueryAsync("select 'Hello world'"); + // select the abstract type from the schema. // Note that the type builder will 'discover' the types that inherit // our C# abstract type. diff --git a/examples/EdgeDB.Examples.CSharp/Examples/JsonResults.cs b/examples/EdgeDB.Examples.CSharp/Examples/JsonResults.cs index 32099667..46e662a7 100644 --- a/examples/EdgeDB.Examples.CSharp/Examples/JsonResults.cs +++ b/examples/EdgeDB.Examples.CSharp/Examples/JsonResults.cs @@ -25,7 +25,7 @@ public async Task ExecuteAsync(EdgeDBClient client) { var result = await client.QueryJsonAsync("select Person {name, email}"); - var people = JsonConvert.DeserializeObject(result)!; + var people = JsonConvert.DeserializeObject(result!)!; Logger!.LogInformation("People from json: {@People}", people); } diff --git a/examples/EdgeDB.Examples.CSharp/Examples/Range.cs b/examples/EdgeDB.Examples.CSharp/Examples/Range.cs index 7681b03d..40d8f2e2 100644 --- a/examples/EdgeDB.Examples.CSharp/Examples/Range.cs +++ b/examples/EdgeDB.Examples.CSharp/Examples/Range.cs @@ -1,4 +1,4 @@ -using EdgeDB.DataTypes; +using EdgeDB.DataTypes; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -15,8 +15,6 @@ internal class RangeExample : IExample public async Task ExecuteAsync(EdgeDBClient client) { var range = await client.QuerySingleAsync>("select range(1, 10)"); - - } } } diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj new file mode 100644 index 00000000..c27cc284 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj @@ -0,0 +1,19 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs new file mode 100644 index 00000000..99bb3002 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs @@ -0,0 +1,68 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:B9692A5CA0A9992246361197BEACBDE398A5A30C5DCCC83BCACD8C80D5842FEB +// Generated on 2023-07-24T15:15:21.8729863Z +#nullable enable +using EdgeDB; +using EdgeDB.DataTypes; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EdgeDB.Generated; + +public static class CreateUser +{ + /// + /// The string containing the query defined in C:\Users\lynch\source\repos\EdgeDB\examples\EdgeDB.Examples.GenerationExample\Scripts\CreateUser.edgeql. + /// + public static readonly string Query; + private static readonly string _queryHex = "494E5345525420506572736F6E207B0D0A20206E616D65203A3D203C7374723E246E616D652C0D0A2020656D61696C203A3D203C7374723E24656D61696C0D0A7D0D0A554E4C45535320434F4E464C494354204F4E202E656D61696C200D0A454C5345202853454C45435420506572736F6E29"; + + + static CreateUser() + { + Query = string.Join("", Regex.Split(_queryHex, "(?<=\\G..)(?!$)").Select(x => (char)Convert.ToByte(x, 16))); + } + + /// + /// Executes the CreateUser query, defined as: + /// + /// INSERT Person { + /// name := <str>$name, + /// email := <str>$email + /// } + /// UNLESS CONFLICT ON .email + /// ELSE (SELECT Person) + /// + /// + /// The client to execute the query on. + /// The name parameter in the query. + /// The email parameter in the query. + public static Task ExecuteAsync(IEdgeDBQueryable client, String name, String email, CancellationToken token = default) + => client.QueryRequiredSingleAsync( + Query, new Dictionary() { { "name", name }, { "email", email } }, + capabilities: Capabilities.Modifications, token: token + ); + + /// + /// Executes the CreateUser query, defined as: + /// + /// INSERT Person { + /// name := <str>$name, + /// email := <str>$email + /// } + /// UNLESS CONFLICT ON .email + /// ELSE (SELECT Person) + /// + /// + /// The client to execute the query on. + /// The name parameter in the query. + /// The email parameter in the query. + public static Task CreateUserAsync(this IEdgeDBQueryable client, String name, String email, CancellationToken token = default) + => ExecuteAsync(client, name, email, token: token); +} +#nullable restore \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs new file mode 100644 index 00000000..4d98091a --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs @@ -0,0 +1,58 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:8F4282FFB3ADCA549B2F18362FAE328971409954B3CFA1B28AD5CCE1F74156AC +// Generated on 2023-07-24T15:15:21.9231397Z +#nullable enable +using EdgeDB; +using EdgeDB.DataTypes; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EdgeDB.Generated; + +public static class DeleteUser +{ + /// + /// The string containing the query defined in C:\Users\lynch\source\repos\EdgeDB\examples\EdgeDB.Examples.GenerationExample\Scripts\DeleteUser.edgeql. + /// + public static readonly string Query; + private static readonly string _queryHex = "44454C45544520506572736F6E200D0A46494C544552202E656D61696C203D203C7374723E24656D61696C"; + + + static DeleteUser() + { + Query = string.Join("", Regex.Split(_queryHex, "(?<=\\G..)(?!$)").Select(x => (char)Convert.ToByte(x, 16))); + } + + /// + /// Executes the DeleteUser query, defined as: + /// + /// DELETE Person + /// FILTER .email = <str>$email + /// + /// + /// The client to execute the query on. + /// The email parameter in the query. + public static Task ExecuteAsync(IEdgeDBQueryable client, String email, CancellationToken token = default) + => client.QuerySingleAsync( + Query, new Dictionary() { { "email", email } }, + capabilities: Capabilities.Modifications, token: token + ); + + /// + /// Executes the DeleteUser query, defined as: + /// + /// DELETE Person + /// FILTER .email = <str>$email + /// + /// + /// The client to execute the query on. + /// The email parameter in the query. + public static Task DeleteUserAsync(this IEdgeDBQueryable client, String email, CancellationToken token = default) + => ExecuteAsync(client, email, token: token); +} +#nullable restore \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj new file mode 100644 index 00000000..3b379e8f --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + 11.0 + enable + + + + + + + + + + + diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs new file mode 100644 index 00000000..2fe40f8f --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs @@ -0,0 +1,62 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:C2792288AE6845E4B2E4B3324315537B82E7715A9114BEFF57A1DCC479448C78 +// Generated on 2023-07-24T15:15:21.9473062Z +#nullable enable +using EdgeDB; +using EdgeDB.DataTypes; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EdgeDB.Generated; + +public static class GetUser +{ + /// + /// The string containing the query defined in C:\Users\lynch\source\repos\EdgeDB\examples\EdgeDB.Examples.GenerationExample\Scripts\GetUser.edgeql. + /// + public static readonly string Query; + private static readonly string _queryHex = "53454C45435420506572736F6E207B0D0A096E616D652C20656D61696C0D0A7D0D0A46494C544552202E656D61696C203D203C7374723E24656D61696C"; + + + static GetUser() + { + Query = string.Join("", Regex.Split(_queryHex, "(?<=\\G..)(?!$)").Select(x => (char)Convert.ToByte(x, 16))); + } + + /// + /// Executes the GetUser query, defined as: + /// + /// SELECT Person { + /// name, email + /// } + /// FILTER .email = <str>$email + /// + /// + /// The client to execute the query on. + /// The email parameter in the query. + public static Task ExecuteAsync(IEdgeDBQueryable client, String email, CancellationToken token = default) + => client.QuerySingleAsync( + Query, new Dictionary() { { "email", email } }, + capabilities: Capabilities.ReadOnly, token: token + ); + + /// + /// Executes the GetUser query, defined as: + /// + /// SELECT Person { + /// name, email + /// } + /// FILTER .email = <str>$email + /// + /// + /// The client to execute the query on. + /// The email parameter in the query. + public static Task GetUserAsync(this IEdgeDBQueryable client, String email, CancellationToken token = default) + => ExecuteAsync(client, email, token: token); +} +#nullable restore \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/CreateUserResult.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/CreateUserResult.g.cs new file mode 100644 index 00000000..d828db98 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/CreateUserResult.g.cs @@ -0,0 +1,19 @@ +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +namespace EdgeDB.Generated; + +[EdgeDBType] +public sealed class CreateUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } + +} + +#nullable restore +#pragma warning restore CS8618 \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/DeleteUserResult.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/DeleteUserResult.g.cs new file mode 100644 index 00000000..217a1c19 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/DeleteUserResult.g.cs @@ -0,0 +1,19 @@ +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +namespace EdgeDB.Generated; + +[EdgeDBType] +public sealed class DeleteUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } + +} + +#nullable restore +#pragma warning restore CS8618 \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/GetUserResult.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/GetUserResult.g.cs new file mode 100644 index 00000000..a74792e2 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/GetUserResult.g.cs @@ -0,0 +1,25 @@ +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +namespace EdgeDB.Generated; + +[EdgeDBType] +public sealed class GetUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } + + [EdgeDBProperty("name")] + public String Name { get; set; } + + [EdgeDBProperty("email")] + public String Email { get; set; } + +} + +#nullable restore +#pragma warning restore CS8618 \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/UpdateUserResult.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/UpdateUserResult.g.cs new file mode 100644 index 00000000..a06fa3d4 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/Results/UpdateUserResult.g.cs @@ -0,0 +1,19 @@ +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +namespace EdgeDB.Generated; + +[EdgeDBType] +public sealed class UpdateUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } + +} + +#nullable restore +#pragma warning restore CS8618 \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs new file mode 100644 index 00000000..3a09bb96 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs @@ -0,0 +1,76 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:B3B320FF0722D3FB0AACD80281B50A8709A2A23491715978860B6D76FFE85BBD +// Generated on 2023-07-24T15:15:21.9619097Z +#nullable enable +using EdgeDB; +using EdgeDB.DataTypes; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace EdgeDB.Generated; + +public static class UpdateUser +{ + /// + /// The string containing the query defined in C:\Users\lynch\source\repos\EdgeDB\examples\EdgeDB.Examples.GenerationExample\Scripts\UpdateUser.edgeql. + /// + public static readonly string Query; + private static readonly string _queryHex = "57495448200D0A202020206E65775F6E616D65203A3D203C6F7074696F6E616C207374723E246E616D652C0D0A202020206E65775F656D61696C203A3D203C6F7074696F6E616C207374723E24656D61696C0D0A55504441544520506572736F6E0D0A46494C544552202E6964203D203C757569643E2469640D0A534554207B0D0A202020206E616D65203A3D206E65775F6E616D6520494620455849535453206E65775F6E616D6520454C5345202E6E616D652C0D0A20202020656D61696C203A3D206E65775F656D61696C20494620455849535453206E65775F656D61696C20454C5345202E656D61696C0D0A7D"; + + + static UpdateUser() + { + Query = string.Join("", Regex.Split(_queryHex, "(?<=\\G..)(?!$)").Select(x => (char)Convert.ToByte(x, 16))); + } + + /// + /// Executes the UpdateUser query, defined as: + /// + /// WITH + /// new_name := <optional str>$name, + /// new_email := <optional str>$email + /// UPDATE Person + /// FILTER .id = <uuid>$id + /// SET { + /// name := new_name IF EXISTS new_name ELSE .name, + /// email := new_email IF EXISTS new_email ELSE .email + /// } + /// + /// + /// The client to execute the query on. + /// The id parameter in the query. + /// The name parameter in the query. + /// The email parameter in the query. + public static Task ExecuteAsync(IEdgeDBQueryable client, Guid id, String? name = null, String? email = null, CancellationToken token = default) + => client.QuerySingleAsync( + Query, new Dictionary() { { "id", id }, { "name", name }, { "email", email } }, + capabilities: Capabilities.Modifications, token: token + ); + + /// + /// Executes the UpdateUser query, defined as: + /// + /// WITH + /// new_name := <optional str>$name, + /// new_email := <optional str>$email + /// UPDATE Person + /// FILTER .id = <uuid>$id + /// SET { + /// name := new_name IF EXISTS new_name ELSE .name, + /// email := new_email IF EXISTS new_email ELSE .email + /// } + /// + /// + /// The client to execute the query on. + /// The id parameter in the query. + /// The name parameter in the query. + /// The email parameter in the query. + public static Task UpdateUserAsync(this IEdgeDBQueryable client, Guid id, String? name = null, String? email = null, CancellationToken token = default) + => ExecuteAsync(client, id, name, email, token: token); +} +#nullable restore \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Program.cs b/examples/EdgeDB.Examples.GenerationExample/Program.cs new file mode 100644 index 00000000..251eff64 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Program.cs @@ -0,0 +1,18 @@ +using EdgeDB; +using EdgeDB.Generated; + +// create a client +var client = new EdgeDBClient(); + +// create a user +var result1 = await client.CreateUserAsync(name: "example", email: "example@example.com"); + +// Get a user based on email +var result2 = await client.GetUserAsync(email: "example@example.com"); + +// update a users name +var result3 = await client.UpdateUserAsync(id: result2!.Id, name: "new name", email: null); + +// delete a user +var result4 = await client.DeleteUserAsync(email: "example@example.com"); + diff --git a/examples/EdgeDB.Examples.GenerationExample/Scripts/CreateUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scripts/CreateUser.edgeql new file mode 100644 index 00000000..e65e9a37 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scripts/CreateUser.edgeql @@ -0,0 +1,6 @@ +INSERT Person { + name := $name, + email := $email +} +UNLESS CONFLICT ON .email +ELSE (SELECT Person) \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Scripts/DeleteUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scripts/DeleteUser.edgeql new file mode 100644 index 00000000..1f4de7a5 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scripts/DeleteUser.edgeql @@ -0,0 +1,2 @@ +DELETE Person +FILTER .email = $email \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Scripts/GetUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scripts/GetUser.edgeql new file mode 100644 index 00000000..13d0aecd --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scripts/GetUser.edgeql @@ -0,0 +1,4 @@ +SELECT Person { + name, email +} +FILTER .email = $email \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Scripts/UpdateUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scripts/UpdateUser.edgeql new file mode 100644 index 00000000..e641e3f4 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scripts/UpdateUser.edgeql @@ -0,0 +1,9 @@ +WITH + new_name := $name, + new_email := $email +UPDATE Person +FILTER .id = $id +SET { + name := new_name IF EXISTS new_name ELSE .name, + email := new_email IF EXISTS new_email ELSE .email +} \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs b/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs new file mode 100644 index 00000000..11175d8d --- /dev/null +++ b/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs @@ -0,0 +1,100 @@ +using CommandLine; +using EdgeDB.CLI.Utils; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Arguments +{ + public class ConnectionArguments : LogArgs + { + [Option("dsn", HelpText = "DSN for EdgeDB to connect to (overrides all other options except password)")] + public string? DSN { get; set; } + + [Option("credentials-file", HelpText = "Path to JSON file to read credentials from")] + public string? CredentialsFile { get; set; } + + [Option('I', "instance", HelpText = "Local instance name created with edgedb instance create to connect to (overrides host and port)")] + public string? Instance { get; set; } + + [Option('H', "host", HelpText = "Host of the EdgeDB instance")] + public string? Host { get; set; } + + [Option('P', "port", HelpText = "Port to connect to EdgeDB")] + public int? Port { get; set; } + + [Option('d', "database", HelpText = "Database name to connect to")] + public string? Database { get; set; } + + [Option('u', "user", HelpText = "User name of the EdgeDB user")] + public string? User { get; set; } + + [Option("password", HelpText = "Ask for password on the terminal (TTY)")] + public bool Password { get; set; } + + [Option("password-from-stdin", HelpText = "Read the password from stdin rather than TTY (useful for scripts)")] + public bool PasswordFromSTDIN { get; set; } + + [Option("tls-ca-file", HelpText = "Certificate to match server against\n\nThis might either be full self-signed server certificate or certificate authority (CA) certificate that server certificate is signed with.")] + public string? TLSCAFile { get; set; } + + [Option("tls-security", HelpText = "Specify the client-side TLS security mode.")] + public TLSSecurityMode? TLSSecurity { get; set; } + + [Option("raw-connection", Hidden = true)] + public string? ConnectionJson { get; set; } + + public EdgeDBConnection GetConnection() + { + if (ConnectionJson is not null) + return JsonConvert.DeserializeObject(ConnectionJson) ?? throw new Exception("Cannot decode connection args"); + + if (DSN is not null) + return EdgeDBConnection.FromDSN(DSN); + + if (Instance is not null) + return EdgeDBConnection.FromInstanceName(Instance); + + if (CredentialsFile is not null) + return JsonConvert.DeserializeObject(File.ReadAllText(CredentialsFile)) + ?? throw new NullReferenceException($"The file '{CredentialsFile}' didn't contain a valid credential definition"); + + // create the resolved connection + var resolved = EdgeDBConnection.ResolveEdgeDBTOML(); + + if (Host is not null) + resolved.Hostname = Host; + + if (Port.HasValue) + resolved.Port = Port.Value; + + if (Database is not null) + resolved.Database = Database; + + if (User is not null) + resolved.Username = User; + + if (Password) + { + // read password from console + Console.Write($"Password for '{resolved.Database}': "); + + resolved.Password = ConsoleUtils.ReadSecretInput(); + } + + if (PasswordFromSTDIN) + resolved.Password = Console.ReadLine(); + + if (TLSCAFile is not null) + resolved.TLSCertificateAuthority = TLSCAFile; + + if (TLSSecurity.HasValue) + resolved.TLSSecurity = TLSSecurity.Value; + + return resolved; + } + } +} diff --git a/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs b/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs new file mode 100644 index 00000000..b72ed64a --- /dev/null +++ b/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs @@ -0,0 +1,23 @@ +using CommandLine; +using Microsoft.Extensions.Logging; +using Serilog.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Arguments +{ + /// + /// A class containing logger arguments. + /// + public class LogArgs + { + /// + /// Gets or sets the log level for the default logger. + /// + [Option("loglevel", HelpText = "Configure the log level")] + public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; + } +} diff --git a/src/EdgeDB.Net.CLI/Commands/FileWatch.cs b/src/EdgeDB.Net.CLI/Commands/FileWatch.cs new file mode 100644 index 00000000..f4f7aa86 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Commands/FileWatch.cs @@ -0,0 +1,405 @@ +using CommandLine; +using EdgeDB.CLI; +using EdgeDB.CLI.Arguments; +using EdgeDB.CLI.Utils; +using EdgeDB.Generator; +using EdgeDB.Generator.TypeGenerators; +using Newtonsoft.Json; +using Serilog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EdgeDB.CLI; + +/// +/// A class representing the watch command. +/// +[Verb("watch", HelpText = "Configure the file watcher")] +public class FileWatch : ConnectionArguments, ICommand +{ + private enum Operation + { + Created, + Updated, + Deleted, + Renamed + } + + private sealed record WatchInstruction(string Source, string? Destination, Operation Operation, int Attempts = 0); + + /// + /// Gets or sets the directory to watch for edgeql files. + /// + [Option('t', "directory", HelpText = "The directory to watch for .edgeql files.")] + public string? Directory { get; set; } + + /// + /// Gets or sets whether or not to kill a already running watcher. + /// + [Option('k', "kill", SetName = "functions", HelpText = "Kill the current running watcher for the project")] + public bool Kill { get; set; } + + [Option('b', "background", SetName = "functions", HelpText = "Runs the watcher in a background process")] + public bool Background { get; set; } + + /// + /// Gets or sets the output directory to place the generated source files. + /// + [Option('o', "output", HelpText = "The output directory for the generated source to be placed.")] + public string? OutputDirectory { get; set; } + + /// + /// Gets or sets the project name/namespace of generated files. + /// + [Option('n', "project-name", HelpText = "The name of the generated project and namespace of generated files.")] + public string GeneratedProjectName { get; set; } = "EdgeDB.Generated"; + + [Option("max-retries", HelpText = "The max number of attempts per failed operation.")] + public int MaxOperationRetries { get; set; } = 10; + + private readonly FileSystemWatcher _watcher = new(); + private readonly Dictionary _latestGenerations = new(); + private readonly SemaphoreSlim _mutex = new(1, 1); + private readonly ConcurrentStack _instructionQueue = new(); + + private TaskCompletionSource _instructionDispatcher = new(); + private string? _watchDirectory; + private ILogger? _logger; + + public async Task ExecuteAsync(ILogger logger) + { + _logger = logger; + + var connection = GetConnection(); + + var generator = new CodeGenerator(connection); + + _watchDirectory = Directory ?? (ProjectUtils.TryGetProjectRoot(out var root) ? root : Environment.CurrentDirectory); + OutputDirectory ??= Path.Combine(Environment.CurrentDirectory, GeneratedProjectName); + + // check if a watcher is already running + var watcher = ProjectUtils.GetWatcherProcess(_watchDirectory); + + if (Kill) + { + if (watcher is null) + { + logger.Error("No watcher process could be found for the target directory {@dir}", _watchDirectory); + return; + } + else + { + watcher.Kill(); + logger.Information("Watcher process {@pid} killed", watcher.Id); + return; + } + } + else if (watcher is not null) + { + logger.Error("A watcher is already running for the target directory {@dir}. PID: {@pid}", _watchDirectory, watcher.Id); + return; + } + + // is this watcher suppost to run in the background? + if (Background) + { + var pid = ProjectUtils.StartBackgroundWatchProcess(connection, _watchDirectory, OutputDirectory!, GeneratedProjectName); + logger.Information("Background process started with id {@pid}", pid); + return; + } + + // init generator will full structure generation + var context = new GeneratorContext(OutputDirectory, GeneratedProjectName); + var typeGenerator = await generator.GetTypeGeneratorAsync(default); + + await RunFullGenerationAsync(_watchDirectory, generator, context, typeGenerator); + + CleanFiles(context.OutputDirectory, generator, typeGenerator); + + _watcher.Path = _watchDirectory; + _watcher.Filter = "*.edgeql"; + _watcher.IncludeSubdirectories = true; + + _watcher.Error += _watcher_Error; + + _watcher.Changed += (o, e) => FileCreatedOrUpdated(o, e, false); + _watcher.Created += (o, e) => FileCreatedOrUpdated(o, e, true); + _watcher.Deleted += FileDeleted; + _watcher.Renamed += FileRenamed; + + _watcher.NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Security + | NotifyFilters.Size; + + _watcher.EnableRaisingEvents = true; + + ProjectUtils.RegisterProcessAsWatcher(_watchDirectory); + + logger.Information("Watcher started for {@Dir}", _watchDirectory); + + + + while (true) + { + _logger!.Debug("Waiting for dispatch"); + await _instructionDispatcher.Task; + + while (_instructionQueue.TryPop(out var step)) + { + _logger.Debug("Popped instruction {@Step}", step); + + try + { + switch (step.Operation) + { + case Operation.Updated or Operation.Created: + { + var info = GeneratorTargetInfo.FromFile(step.Source); + + if (_latestGenerations.TryGetValue(info.Path!, out var hash) && hash == info.Hash) + _logger.Information("Skipping {@file}: already at latest generation ({@hash})", info.Path, info.Hash[..7]); + else + { + if (string.IsNullOrEmpty(info.EdgeQL)) + { + _logger.Information("Skipping {@File}, contents is empty", info.Path); + return; + } + + using var tokenSource = new CancellationTokenSource(10000); + + await GenerateAndCleanAsync(info, generator, context, typeGenerator, tokenSource.Token); + + _logger.Information("Calling post-process generation task..."); + + await typeGenerator.PostProcessAsync(context); + + _latestGenerations[info.Path!] = info.Hash!; + + _logger.Debug("Generation complete for {@Target}", info.FileName); + + } + } + break; + case Operation.Deleted: + { + if (step.Attempts > 0) + { + _logger.Information("Attempt retry backoff..."); + await Task.Delay(1000); + } + + // TODO + } + break; + case Operation.Renamed: + { + if (step.Destination is null) + { + _logger.Fatal("Unknown rename destination"); + return; + } + + if (step.Attempts > 0) + { + _logger.Information("Attempt retry backoff..."); + await Task.Delay(1000); + } + + var oldGeneratedFile = Path.Combine(context.OutputDirectory, $"{Path.GetFileNameWithoutExtension(step.Source)}.g.cs"); + + if (!RetryingDelete(oldGeneratedFile, step)) + break; + + // TODO + + + + } + break; + } + } + catch(Exception x) + { + _logger.Fatal(x, "Unhandled exception in dispatcher"); + } + } + + _logger!.Debug("Reseting dispatcher..."); + _instructionDispatcher = new(); + } + } + + private static async Task RunFullGenerationAsync(string projectRoot, CodeGenerator generator, GeneratorContext context, ITypeGenerator typeGenerator) + { + // find edgeql files + var edgeqlFiles = ProjectUtils.GetTargetEdgeQLFiles(projectRoot).ToArray(); + + // error if any are the same name + var groupFileNames = edgeqlFiles.GroupBy(x => Path.GetFileNameWithoutExtension(x)); + if (groupFileNames.Any(x => x.Count() > 1)) + { + foreach (var conflict in groupFileNames.Where(x => x.Count() > 1)) + { + Serilog.Log.ForContext().Fatal($"{{@Count}} files contain the same name ({string.Join(" - ", conflict.Select(x => x))})", conflict.Count()); + } + + return; + } + + await generator.GenerateAsync(edgeqlFiles, context, default, true, typeGenerator); + } + + private bool RetryingDelete(string file, WatchInstruction step) + { + try + { + if(File.Exists(file)) + File.Delete(file); + + return true; + } + catch (Exception x) + { + if (step.Attempts > MaxOperationRetries) + { + _logger!.Fatal(x, "Failed to delete file {@File} after {@Retries} attempts", file, step.Attempts); + return false; + } + + _logger!.Warning(x, "Failed to delete file {@File}, attempt {@Current}/{@Max}, requeuing task", file, step.Attempts, MaxOperationRetries); + + _instructionQueue.Push(step with + { + Attempts = step.Attempts + 1 + }); + + return false; + } + } + + private static bool IsValidFile(string path) + => !path.Contains(Path.Combine("dbschema", "migrations")); + + internal async Task GenerateAndCleanAsync( + GeneratorTargetInfo info, CodeGenerator generator, GeneratorContext context, + ITypeGenerator typeGenerator, CancellationToken token, bool force = false) + { + await _mutex.WaitAsync(token).ConfigureAwait(false); + + _logger!.Debug("Entering generation step for {@file}. Hash: {@hash}", info.Path, info.Hash); + try + { + // check if the file is a valid file + if (!IsValidFile(info.Path!)) + { + _logger.Information("Skipping {@file}: invalid target", info.Path); + return; + } + + _logger.Debug("Generating {@file}...", info.Path); + + try + { + await generator.GenerateAsync(info, context, token, typeGenerator, force); + + CleanFiles(context.OutputDirectory, generator, typeGenerator); + + _logger!.Information("Completed {@info}", info.GetGenerationFileName(context)); + + } + catch (EdgeDBErrorException err) + { + // error with file + _logger!.Error(err, "Failed to generate {@file}", info.Path); + } + } + catch (Exception x) + { + _logger!.Fatal(x, "Failed to generate {@file}", info.Path); + } + finally + { + _mutex.Release(); + } + } + + private void CleanFiles(string outputDir, CodeGenerator generator, ITypeGenerator typeGenerator) + { + var files = System.IO.Directory.GetFiles(outputDir, "*.g.cs", new EnumerationOptions { RecurseSubdirectories = true }); + var generatedFiles = typeGenerator.GetGeneratedFiles(); + + var targets = files.Where(x => !generatedFiles.Any(y => y.GeneratedPath == x) && !generator.GeneratedFiles.Any(y => y == x)); + + foreach(var target in targets) + { + _logger!.Debug("Removing stale generated file {@File}", target); + File.Delete(target); + } + } + + private void FileCreatedOrUpdated(object sender, FileSystemEventArgs e, bool created) + { + if (!FileUtils.WaitForHotFile(e.FullPath)) + return; + + // wait an extra second to make sure the file is fully written + //Thread.Sleep(1000); + + var operation = created ? Operation.Created : Operation.Updated; + + _logger!.Information("[I] -> {@Op} {@file}", operation, Path.GetFileNameWithoutExtension(e.FullPath)); + + PushInstruction(operation, e.FullPath); + } + + private void FileDeleted(object sender, FileSystemEventArgs e) + { + if (!IsValidFile(e.FullPath)) + return; + + _logger!.Information("[I] -> Deleted {@file}", Path.GetFileNameWithoutExtension(e.FullPath)); + + PushInstruction(Operation.Deleted, e.FullPath); + } + + private void FileRenamed(object sender, RenamedEventArgs e) + { + if (!IsValidFile(e.FullPath)) + return; + + _logger!.Information("[I] -> Updated {@file}", Path.GetFileNameWithoutExtension(e.FullPath)); + + PushInstruction(Operation.Renamed, e.OldFullPath, e.FullPath); + } + + private void _watcher_Error(object sender, ErrorEventArgs e) + { + _logger!.Error(e.GetException(), "A file watch error occured"); + } + + private void PushInstruction(Operation operation, string source, string? destination = null) + { + _instructionQueue.Push(new(source, destination, operation)); + + var dispatchResult = _instructionDispatcher.TrySetResult(); + + _logger!.Debug("Dispatch result: {@r}", dispatchResult); + + if (!dispatchResult) + { + _logger!.Debug("Got unsuccessful dispatch result for {@Op}, ignoring", operation); + } + } +} diff --git a/src/EdgeDB.Net.CLI/Commands/Generate.cs b/src/EdgeDB.Net.CLI/Commands/Generate.cs new file mode 100644 index 00000000..d7d1ebfc --- /dev/null +++ b/src/EdgeDB.Net.CLI/Commands/Generate.cs @@ -0,0 +1,111 @@ +using CommandLine; +using EdgeDB.Binary; +using EdgeDB.CLI.Arguments; +using EdgeDB.CLI.Utils; +using EdgeDB.Generator; +using Newtonsoft.Json; +using Serilog; +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace EdgeDB.CLI; + +/// +/// A class representing the generate command. +/// +[Verb("generate", HelpText = "Generate or updates csharp classes from .edgeql files.")] +public class Generate : ConnectionArguments, ICommand +{ + /// + /// Gets or sets whether or not a class library should be generated. + /// + [Option('p', "project", HelpText = "Whether or not to create the default class library that will contain the generated source code. Enabled by default.")] + public bool GenerateProject { get; set; } = true; + + /// + /// Gets or sets the output directory the generated source files will be placed. + /// + [Option('o', "output", HelpText = "The output directory for the generated source to be placed. When generating a project, source files will be placed in that projects directory. Default is the current directory")] + public string? OutputDirectory { get; set; } + + /// + /// Gets or sets the project name/namespace. + /// + [Option('n', "project-name", HelpText = "The name of the generated project and namespace of generated files.")] + public string GeneratedProjectName { get; set; } = "EdgeDB.Generated"; + + /// + /// Gets or sets whether or not to start a watch process post-generate. + /// + [Option('w', "watch", HelpText = "Listens for any changes or new edgeql files and (re)generates them automatically.")] + public bool Watch { get; set; } + + /// + /// Gets or sets whether or not to force regenerate. + /// + [Option('f', "force", HelpText = "Force regeneration of all query files.")] + public bool Force { get; set; } + + /// + public async Task ExecuteAsync(ILogger logger) + { + // get connection info + var connection = GetConnection(); + + var projectRoot = ProjectUtils.GetProjectRoot(); + + OutputDirectory ??= Environment.CurrentDirectory; + + Directory.CreateDirectory(OutputDirectory); + + if (GenerateProject && !Directory.Exists(Path.Combine(OutputDirectory, GeneratedProjectName))) + { + logger.Information("Creating project {@ProjectName}...", GeneratedProjectName); + await ProjectUtils.CreateGeneratedProjectAsync(OutputDirectory, GeneratedProjectName); + } + + if(GenerateProject) + OutputDirectory = Path.Combine(OutputDirectory, GeneratedProjectName); + + if (Watch) + { + var existing = ProjectUtils.GetWatcherProcess(projectRoot); + + if (existing is not null) + { + logger.Warning("Watching already running"); + return; + } + + logger.Information("Starting file watcher..."); + var pid = ProjectUtils.StartBackgroundWatchProcess(connection, projectRoot, OutputDirectory, GeneratedProjectName); + logger.Information("File watcher process started, PID: {@PID}", pid); + return; + } + + // find edgeql files + var edgeqlFiles = ProjectUtils.GetTargetEdgeQLFiles(projectRoot).ToArray(); + + // error if any are the same name + var groupFileNames = edgeqlFiles.GroupBy(x => Path.GetFileNameWithoutExtension(x)); + if(groupFileNames.Any(x => x.Count() > 1)) + { + foreach(var conflict in groupFileNames.Where(x => x.Count() > 1)) + { + logger.Fatal($"{{@Count}} files contain the same name ({string.Join(" - ", conflict.Select(x => x))})", conflict.Count()); + } + + return; + } + + var generator = new CodeGenerator(connection); + var context = new GeneratorContext(OutputDirectory, GeneratedProjectName); + + logger.Information("Generating {@FileCount} files...", edgeqlFiles.Length); + + await generator.GenerateAsync(edgeqlFiles, context, default, Force); + + logger.Information("Generation complete!"); + } +} diff --git a/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj b/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj new file mode 100644 index 00000000..8e246aa5 --- /dev/null +++ b/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj @@ -0,0 +1,40 @@ + + + + Exe + net7.0 + enable + enable + EdgeDB.Net.CLI + EdgeDB + A CLI tool to generate C# files from edgeql files + edgedb-net + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/EdgeDB.Net.CLI/Extensions/LoggerExtensions.cs b/src/EdgeDB.Net.CLI/Extensions/LoggerExtensions.cs new file mode 100644 index 00000000..ae542cd0 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Extensions/LoggerExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; +using Serilog.Extensions.Logging; +using System; +namespace EdgeDB +{ + public static class LoggerExtensions + { + public static ILogger CreateClientLogger(this Serilog.ILogger logger) + { + return new SerilogLoggerFactory(logger).CreateLogger(); + } + } +} + diff --git a/src/EdgeDB.Net.CLI/Extensions/TypeExtensions.cs b/src/EdgeDB.Net.CLI/Extensions/TypeExtensions.cs new file mode 100644 index 00000000..990e4aff --- /dev/null +++ b/src/EdgeDB.Net.CLI/Extensions/TypeExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Text.RegularExpressions; + +namespace EdgeDB +{ + public static class TypeExtensions + { + public static string ToFormattedString(this Type type, bool includeParents = false) + { + var name = type.Name switch + { + "String" => "string", + "Int16" => "short", + "Int32" => "int", + "Int64" => "long", + "Boolean" => "bool", + "Object" => "object", + "Void" => "void", + "Byte" => "byte", + "SByte" => "sbyte", + "UInt16" => "ushort", + "UInt32" => "uint", + "UInt64" => "ulong", + "Char" => "char", + "Decimal" => "decimal", + "Double" => "double", + "Single" => "float", + _ => includeParents ? type.FullName ?? type.Name : type.Name + }; + + return Regex.Replace(name, @"`\d+", m => $"<{string.Join(", ", type.GetGenericArguments().Select(t => t.ToFormattedString()))}>"); + } + } +} + diff --git a/src/EdgeDB.Net.CLI/Generator/CodeGenerator.cs b/src/EdgeDB.Net.CLI/Generator/CodeGenerator.cs new file mode 100644 index 00000000..318ef98b --- /dev/null +++ b/src/EdgeDB.Net.CLI/Generator/CodeGenerator.cs @@ -0,0 +1,343 @@ +using CommandLine; +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol; +using EdgeDB.CLI; +using EdgeDB.CLI.Utils; +using EdgeDB.Generator.TypeGenerators; +using EdgeDB.Utils; +using Serilog; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EdgeDB.Generator +{ + internal class CodeGenerator + { + public IEnumerable GeneratedFiles + => _generatedFilesMap.Values; + + private readonly Dictionary _generatedFilesMap = new(); + + private readonly EdgeDBClient _client; + private readonly ILogger _logger; + + public CodeGenerator(EdgeDBConnection connection) + { + _logger = Serilog.Log.ForContext(); + + _client = new EdgeDBClient(connection, new() { Logger = _logger.CreateClientLogger() }); + } + + public bool TryGetGeneratedFile(string edgeqlPath, [MaybeNullWhen(false)] out string generatedFile) + => _generatedFilesMap.TryGetValue(edgeqlPath, out generatedFile); + + public async Task ParseAsync(GeneratorTargetInfo target, CancellationToken token) + { + await using var client = await _client.GetOrCreateClientAsync(token); + + return await client.ProtocolProvider.ParseQueryAsync(new Binary.Protocol.QueryParameters( + target.EdgeQL, + null, + Capabilities.All, + Cardinality.Many, + IOFormat.Binary, + false + ), token); + } + + public async Task GenerateAsync( + IEnumerable targets, GeneratorContext context, CancellationToken token, + bool force = false, ITypeGenerator? typeGenerator = null) + { + var targetInfos = new List(); + + foreach (var target in targets) + { + if (!File.Exists(target)) + { + _logger.Warning("Target file {@File} doesn't exist, skipping", target); + continue; + } + + string edgeql; + try + { + edgeql = File.ReadAllText(target); + } + catch (Exception x) + { + _logger.Error(x, "Failed to read target file {@File}", target); + continue; + } + + var hash = HashUtils.HashEdgeQL(edgeql); + + targetInfos.Add(new GeneratorTargetInfo(edgeql, Path.GetFileNameWithoutExtension(target), target, hash)); + } + + typeGenerator ??= await GetTypeGeneratorAsync(token); + + _logger.Information("Chosing type generator {@Generator}", typeGenerator); + + + foreach (var target in targetInfos) + { + try + { + await GenerateAsync(target, context, token, typeGenerator, force); + } + catch(Exception x) + { + _logger.Error(x, "Type generation failed for {@Target}", target); + } + } + + _logger.Information("Running post-process..."); + await typeGenerator.PostProcessAsync(context); + } + + public async Task GenerateAsync( + GeneratorTargetInfo target, GeneratorContext context, + CancellationToken token, ITypeGenerator? typeGenerator = null, + bool force = false) + { + var isOwnGenerator = typeGenerator is null; + + typeGenerator ??= await GetTypeGeneratorAsync(token); + + await ValidateOrGenerateAsync(target, context, typeGenerator, force, token); + + if (isOwnGenerator) + await typeGenerator.PostProcessAsync(context); + } + + public async Task GetTypeGeneratorAsync(CancellationToken token) + { + var protocolVersion = await GetClientProtocolVersionAsync(token); + + return protocolVersion switch + { + (1, >= 0) => new V1TypeGenerator(), + (2, >= 0) => new V2TypeGenerator(), + _ => throw new NotSupportedException($"Cannot determine the generator to use for the protocol version {protocolVersion}") + }; + } + + private async Task ValidateOrGenerateAsync( + GeneratorTargetInfo target, GeneratorContext context, + ITypeGenerator typeGenerator, bool force, + CancellationToken token) + { + var outputPath = target.GetGenerationFileName(context); + + if (!force) + { + + if (File.Exists(outputPath) && await IsUpToDateAsync(target, context)) + { + _logger.Information("Skipping {@File}, up to date version exists: {@UpToDate}", target.FileName, outputPath); + return; + } + } + + _logger.Information("Parsing {@Target}...", target.FileName); + + + var parsed = await ParseAsync(target, token); + + var resultTypeDef = await typeGenerator.GetTypeAsync(parsed.OutCodecInfo.Codec, target, context); + + var code = await GenerateCSharpContentAsync(resultTypeDef, parsed, target, context, typeGenerator); + + _logger.Information("Writing {@target} to {@Path}", target.FileName, outputPath); + + _logger.Verbose("Codec structure:\n{@Codec}", CodecFormatter.Format(parsed.OutCodecInfo.Codec).ToString()); + _logger.Verbose("Generated Code:\n{Code}", code); + + await File.WriteAllTextAsync(outputPath, code, token); + + if (!_generatedFilesMap.TryAdd(target.Path, outputPath)) + _generatedFilesMap[target.Path] = outputPath; + } + + private async Task GenerateCSharpContentAsync( + string typeRef, ParseResult parseResult, GeneratorTargetInfo target, + GeneratorContext context, ITypeGenerator typeGenerator) + { + var resultType = parseResult.Cardinality switch + { + Cardinality.NoResult => null, + Cardinality.AtMostOne => $"{typeRef}?", + Cardinality.One => typeRef, + _ => $"IReadOnlyCollection<{typeRef}?>" + }; + + var method = parseResult.Cardinality switch + { + Cardinality.NoResult => "ExecuteAsync", + Cardinality.AtMostOne => "QuerySingleAsync", + Cardinality.One => "QueryRequiredSingleAsync", + _ => "QueryAsync" + }; + + var methodParametersArray = Array.Empty(); + var dictParamsArray = Array.Empty(); + + if (parseResult.InCodecInfo.Codec is ObjectCodec objCodec) + { + methodParametersArray = new string[objCodec.InnerCodecs.Length]; + dictParamsArray = new string[objCodec.InnerCodecs.Length]; + + // order by cardinality + var oderedProps = objCodec.Properties.OrderByDescending(x => x.Cardinality, CardinalityComparer.Instance).ToArray(); + + for (int i = 0; i != oderedProps.Length; i++) + { + var codec = oderedProps[i].Codec; + var name = oderedProps[i].Name; + + var type = await typeGenerator.GetTypeAsync(codec, target, context); + + methodParametersArray[i] = $"{ApplyCardinality(type, oderedProps[i].Cardinality)} {TextUtils.ToCamelCase(name)}"; + + if (oderedProps[i].Cardinality is Cardinality.AtMostOne) + methodParametersArray[i] += " = null"; + + dictParamsArray[i] = $"{{ \"{name}\", {TextUtils.ToCamelCase(name)} }}"; + } + } + + var methodParameters = methodParametersArray.Length > 0 + ? $", {string.Join(", ", methodParametersArray)}" + : string.Empty; + + var dictParameters = dictParamsArray.Length > 0 + ? $", new Dictionary() {{ {string.Join(", ", dictParamsArray)} }}" + : string.Empty; + + var paramSummarys = methodParametersArray.Length > 0 + ? $"\n /// {string.Join("\n /// ", methodParametersArray.Select(x => $"The {x.Split(' ')[1]} parameter in the query."))}" + : string.Empty; + + return +$$"""" +// AUTOGENERATED: DO NOT MODIFY +// edgeql:{{target.Hash}} +// Generated on {{DateTime.UtcNow:O}} +#nullable enable +using EdgeDB; +using EdgeDB.DataTypes; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace {{context.GenerationNamespace}}; + +public static class {{target.FileName}} +{ + /// + /// The string containing the query defined in {{target.Path}}. + /// + public static readonly string Query; + private static readonly string _queryHex = "{{HexConverter.ToHex(Encoding.UTF8.GetBytes(target.EdgeQL))}}"; + + + static {{target.FileName}}() + { + Query = string.Join("", Regex.Split(_queryHex, "(?<=\\G..)(?!$)").Select(x => (char)Convert.ToByte(x, 16))); + } + + /// + /// Executes the {{target.FileName}} query, defined as: + /// + /// {{TextUtils.EscapeToXMLComment(Regex.Replace(target.EdgeQL!, @"(\n)", m => $"{m.Groups[1].Value}{"",4}/// "))}} + /// + /// + /// The client to execute the query on.{{paramSummarys}} + public static Task<{{resultType}}> ExecuteAsync(IEdgeDBQueryable client{{methodParameters}}, CancellationToken token = default) + => client.{{method}}<{{typeRef}}>( + Query{{dictParameters}}, + capabilities: Capabilities.{{parseResult.Capabilities}}, token: token + ); + + /// + /// Executes the {{target.FileName}} query, defined as: + /// + /// {{TextUtils.EscapeToXMLComment(Regex.Replace(target.EdgeQL!, @"(\n)", m => $"{m.Groups[1].Value}{"",4}/// "))}} + /// + /// + /// The client to execute the query on.{{paramSummarys}} + public static Task<{{resultType}}> {{target.FileName}}Async(this IEdgeDBQueryable client{{methodParameters}}, CancellationToken token = default) + => ExecuteAsync(client{{(methodParametersArray.Length > 0 ? $", {string.Join(", ", methodParametersArray.Select(x => x.Split(' ')[1]))}" : string.Empty)}}, token: token); +} +#nullable restore +""""; + } + + internal async Task IsUpToDateAsync(GeneratorTargetInfo target, GeneratorContext context) + { + if (!File.Exists(target.GetGenerationFileName(context))) + return false; + + using var stream = File.OpenRead(target.GetGenerationFileName(context)); + using var reader = new StreamReader(stream); + + // header + if (await reader.ReadLineAsync() is null) + return false; + + var fileHashHeader = await reader.ReadLineAsync(); + + if (fileHashHeader is null) + return false; + + var match = Regex.Match(fileHashHeader, @"^\/\/ edgeql:([0-9a-fA-F]{64})$"); + + return match.Success && match.Groups[1].Value == target.Hash; + } + + private async Task GetClientProtocolVersionAsync(CancellationToken token) + { + await using var binaryClient = await _client.GetOrCreateClientAsync(token); + + await binaryClient.SyncAsync(token); + + return binaryClient.ProtocolProvider.Version; + } + + public static string ApplyCardinality(string type, Cardinality cardinality) + { + if (cardinality is Cardinality.AtMostOne or Cardinality.Many or Cardinality.NoResult) + type += "?"; + + return type; + } + + private readonly struct CardinalityComparer : IComparer + { + public static readonly CardinalityComparer Instance = new(); + + private static readonly Dictionary _map = new() + { + {Cardinality.NoResult, 0 }, + {Cardinality.AtMostOne, 1 }, + {Cardinality.One, 2 }, + {Cardinality.AtLeastOne, 3 }, + {Cardinality.Many, 4 } + }; + + public int Compare(Cardinality x, Cardinality y) + => _map[x] - _map[y]; + } + } +} diff --git a/src/EdgeDB.Net.CLI/Generator/GeneratorContext.cs b/src/EdgeDB.Net.CLI/Generator/GeneratorContext.cs new file mode 100644 index 00000000..93cf4db7 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Generator/GeneratorContext.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Generator +{ + internal sealed record GeneratorContext( + string OutputDirectory, + string GenerationNamespace + ); +} diff --git a/src/EdgeDB.Net.CLI/Generator/GeneratorTargetInfo.cs b/src/EdgeDB.Net.CLI/Generator/GeneratorTargetInfo.cs new file mode 100644 index 00000000..504f0cdc --- /dev/null +++ b/src/EdgeDB.Net.CLI/Generator/GeneratorTargetInfo.cs @@ -0,0 +1,34 @@ +using EdgeDB.CLI.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Generator +{ + internal record GeneratorTargetInfo( + string EdgeQL, + string FileName, + string Path, + string Hash + ) + { + public string GetGenerationFileName(GeneratorContext context) + => System.IO.Path.Combine(context.OutputDirectory, $"{FileName}.g.cs"); + + public static GeneratorTargetInfo FromFile(string target) + { + if (!File.Exists(target)) + { + throw new FileNotFoundException("Failed to find file", target); + } + + var edgeql = File.ReadAllText(target); + + var hash = HashUtils.HashEdgeQL(edgeql); + + return new(edgeql, System.IO.Path.GetFileNameWithoutExtension(target), target, hash); + } + } +} diff --git a/src/EdgeDB.Net.CLI/Generator/TypeGenerators/ITypeGenerator.cs b/src/EdgeDB.Net.CLI/Generator/TypeGenerators/ITypeGenerator.cs new file mode 100644 index 00000000..83809764 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Generator/TypeGenerators/ITypeGenerator.cs @@ -0,0 +1,35 @@ +using EdgeDB.Binary.Codecs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Generator.TypeGenerators +{ + internal interface ITypeGenerator + { + ValueTask GetTypeAsync(ICodec codec, GeneratorTargetInfo? target, GeneratorContext context); + + ValueTask PostProcessAsync(GeneratorContext context); + + IEnumerable GetGeneratedFiles(); + + void RemoveGeneratedReferences(IEnumerable references); + } + + internal sealed class GeneratedFileInfo + { + public string GeneratedPath { get; } + public List EdgeQLReferences { get; } + + public GeneratedFileInfo(string generatedPath, string? edgeqlPath) + { + GeneratedPath = generatedPath; + EdgeQLReferences = new(); + + if (edgeqlPath is not null) + EdgeQLReferences.Add(edgeqlPath); + } + } +} diff --git a/src/EdgeDB.Net.CLI/Generator/TypeGenerators/V1TypeGenerator.cs b/src/EdgeDB.Net.CLI/Generator/TypeGenerators/V1TypeGenerator.cs new file mode 100644 index 00000000..71621b79 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Generator/TypeGenerators/V1TypeGenerator.cs @@ -0,0 +1,155 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.CLI.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Generator.TypeGenerators +{ + internal sealed class V1TypeGenerator : ITypeGenerator + { + private readonly List _generatedFiles; + + private int _count; + + public V1TypeGenerator() + { + _generatedFiles = new(); + } + + public IEnumerable GetGeneratedFiles() + => _generatedFiles; + + public void RemoveGeneratedReferences(IEnumerable references) + { + foreach(var item in references) + { + _generatedFiles.Remove(item); + } + } + + private void RegisterFile(string path, string? workingFile) + { + var existing = _generatedFiles.FirstOrDefault(x => x.GeneratedPath == path); + + if (existing is not null && workingFile is not null) + existing.EdgeQLReferences.Add(workingFile); + + if(existing is null) + { + _generatedFiles.Add(new(path, workingFile)); + } + } + + private string GetSubtypeName(string? name = null) + { + return name is not null + ? $"{name}SubType{_count++}" + : $"GenericSubType{_count++}"; + } + + public ValueTask GetTypeAsync(ICodec codec, GeneratorTargetInfo? target, GeneratorContext context) + { + var rootName = target is null ? $"{codec.GetHashCode()}Result" : $"{target.FileName}Result"; + + return GetTypeAsync(codec, rootName, target?.Path, context); + } + + private async ValueTask GetTypeAsync(ICodec codec, string? name, string? workingFile, GeneratorContext context, bool nullable = false) + { + switch (codec) + { + case SparceObjectCodec: + throw new InvalidOperationException("Cannot parse sparce object codec"); + case ObjectCodec obj: + return await GenerateResultType(obj, name, workingFile, context); + case TupleCodec tuple: + var elements = new string[tuple.InnerCodecs.Length]; + for (var i = 0; i != elements.Length; i++) + elements[i] = await GetTypeAsync(codec, $"{name ?? GetSubtypeName()}TupleElement{i}", workingFile, context); + return $"({string.Join(", ", elements)})"; + case IWrappingCodec wrapping when codec.GetType().IsGenericType: + if (codec.GetType().GetGenericTypeDefinition() == typeof(ArrayCodec<>)) + return $"{await GetTypeAsync(wrapping.InnerCodec, name, workingFile, context)}[]"; + else if (codec.GetType().GetGenericTypeDefinition() == typeof(RangeCodec<>)) + return $"Range<{await GetTypeAsync(wrapping.InnerCodec, name, workingFile, context)}>"; + else if (codec.GetType().GetGenericTypeDefinition() == typeof(SetCodec<>)) + return $"List<{await GetTypeAsync(wrapping.InnerCodec, name, workingFile, context)}>"; + else + throw new EdgeDBException($"Unknown wrapping codec {wrapping}"); + case IScalarCodec scalar: + var sc = scalar.ConverterType.Name; + if (nullable) + sc += "?"; + + return sc; + case NullCodec: + return "object?"; + default: + throw new InvalidOperationException($"Unknown type parse path for codec {codec}"); + } + } + + private async ValueTask GenerateResultType(ObjectCodec obj, string? name, string? workingFile, GeneratorContext context) + { + var properties = new (string, string)[obj.Properties.Length]; + + for(var i = 0; i != properties.Length; i++) + { + var property = obj.Properties[i]; + var codec = obj.InnerCodecs[i]; + + var type = await GetTypeAsync( + codec, + name: null, + workingFile, + context, + property.Cardinality is Cardinality.AtMostOne or Cardinality.Many or Cardinality.NoResult + ); + + properties[i] = (property.Name, $"public {type} {TextUtils.ToPascalCase(property.Name)} {{ get; set; }}"); + } + + var tname = name ?? GetSubtypeName(); + + await GenerateTypeCodeAsync(properties, tname, workingFile, context); + + return tname; + } + + private async Task GenerateTypeCodeAsync((string, string)[] props, string name, string? workingFile, GeneratorContext context) + { + Directory.CreateDirectory(Path.Combine(context.OutputDirectory, "Results")); + + var code = +$$"""" +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +namespace {{context.GenerationNamespace}}; + +[EdgeDBType] +public sealed class {{name}} +{ +{{string.Join("\n", props.Select(x => $" [EdgeDBProperty(\"{x.Item1}\")]\n {x.Item2}\n"))}} +} + +#nullable restore +#pragma warning restore CS8618 +""""; + var path = Path.Combine(context.OutputDirectory, "Results", $"{name}.g.cs"); + + RegisterFile(path, workingFile); + + await File.WriteAllTextAsync(path, code); + } + + public ValueTask PostProcessAsync(GeneratorContext _) => ValueTask.CompletedTask; + } +} diff --git a/src/EdgeDB.Net.CLI/Generator/TypeGenerators/V2TypeGenerator.cs b/src/EdgeDB.Net.CLI/Generator/TypeGenerators/V2TypeGenerator.cs new file mode 100644 index 00000000..b5d09d45 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Generator/TypeGenerators/V2TypeGenerator.cs @@ -0,0 +1,363 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.CLI.Utils; +using Serilog; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Generator.TypeGenerators +{ + internal abstract class ObjectGenerationInfo + { + public abstract string GeneratedFilePath { get; } + + public string CleanTypeName + => TypeName.Contains("::") ? TypeName.Split("::")[1] : TypeName; + + public ObjectGenerationInfo? Parent { get; set; } + + public string TypeName { get; } + + public Dictionary PropertyTypeMap { get; } + + public string? FilePath { get; set; } + + public ObjectGenerationInfo(string typename, Dictionary propertyMap, ObjectGenerationInfo? parent = null) + { + TypeName = typename; + PropertyTypeMap = propertyMap; + Parent = parent; + } + } + + internal sealed class ClassGenerationInfo : ObjectGenerationInfo + { + public override string GeneratedFilePath + => Path.Combine("Results", $"{CleanTypeName}.g.cs"); + + public string? EdgeQLSourceFile { get; } + public ObjectCodec Codec { get; } + + public ClassGenerationInfo( + string typename, ObjectCodec codec, Dictionary propertyTypeMap, + string? edgeqlSourceFile, + ObjectGenerationInfo? parent = null) + : base(typename, propertyTypeMap, parent) + { + EdgeQLSourceFile = edgeqlSourceFile; + Codec = codec; + } + + public override string ToString() => $"class {CleanTypeName} ({TypeName})"; + } + + internal sealed class InterfaceGenerationInfo : ObjectGenerationInfo + { + public override string GeneratedFilePath + => Path.Combine("Interfaces", $"{TypeName}.g.cs"); + + public InterfaceGenerationInfo( + string typename, + Dictionary propertyMap, + ObjectGenerationInfo? parent = null) + : base(typename, propertyMap, parent) + { + } + + public override string ToString() => $"interface {CleanTypeName}"; + } + + internal sealed class V2TypeGenerator : ITypeGenerator + { + private readonly Dictionary> _resultShapes; + private readonly List _generatedTypes; + + private int _count; + + public V2TypeGenerator() + { + _resultShapes = new(); + _generatedTypes = new(); + } + + public IEnumerable GetGeneratedFiles() + => _generatedTypes.Select(x => + { + return x switch + { + ClassGenerationInfo cls => new GeneratedFileInfo(cls.FilePath!, cls.EdgeQLSourceFile), + InterfaceGenerationInfo iface => new GeneratedFileInfo(iface.FilePath!, null), + _ => throw new InvalidOperationException($"Unknown target info {x}") + }; + }); + + public void RemoveGeneratedReferences(IEnumerable references) + { + foreach (var item in references) + { + _generatedTypes.RemoveAll(x => x.FilePath == item.GeneratedPath); + } + } + + private string GetSubtypeName(string? name = null) + { + return name is not null + ? $"{name}SubType{_count++}" + : $"GenericSubType{_count++}"; + } + + public ValueTask GetTypeAsync(ICodec codec, GeneratorTargetInfo? target, GeneratorContext context) + { + var rootName = target is null ? $"{codec.GetHashCode()}Result" : target.FileName; + + return GetTypeAsync(codec, rootName, target?.Path, context); + } + + private async ValueTask GetTypeAsync(ICodec codec, string? name, string? workingFile, GeneratorContext context) + { + switch (codec) + { + case SparceObjectCodec: + throw new InvalidOperationException("Cannot parse sparce object codec"); + case ObjectCodec obj: + return await GenerateResultType(obj, name, workingFile, context); + case TupleCodec tuple: + var elements = new string[tuple.InnerCodecs.Length]; + for (var i = 0; i != elements.Length; i++) + elements[i] = await GetTypeAsync(codec, $"{name ?? GetSubtypeName()}TupleElement{i}", workingFile, context); + return $"({string.Join(", ", elements)})"; + case CompilableWrappingCodec: + case IWrappingCodec when codec.GetType().IsGenericType: + var inner = codec is CompilableWrappingCodec c ? c.InnerCodec : codec is IWrappingCodec w ? w.InnerCodec : throw new Exception("This isn't possible"); + if (codec.GetType().GetGenericTypeDefinition() == typeof(ArrayCodec<>)) + return $"{await GetTypeAsync(inner, name, workingFile, context)}[]"; + else if (codec.GetType().GetGenericTypeDefinition() == typeof(RangeCodec<>)) + return $"Range<{await GetTypeAsync(inner, name, workingFile, context)}>"; + else if (codec.GetType().GetGenericTypeDefinition() == typeof(SetCodec<>)) + return $"List<{await GetTypeAsync(inner, name, workingFile, context)}>"; + else + throw new EdgeDBException($"Unknown wrapping codec {inner}"); + case IScalarCodec scalar: + return scalar.ConverterType.Name; + case NullCodec: + return "object"; + default: + throw new InvalidOperationException($"Unknown type parse path for codec {codec}"); + } + } + + private async ValueTask GenerateResultType(ObjectCodec obj, string? name, string? workingFile, GeneratorContext context) + { + if (name is not null) + name = $"{name}{TextUtils.NameWithoutModule(obj.Metadata?.SchemaName) ?? "Result"}"; + else + name = $"{GetSubtypeName(name)}{TextUtils.CleanTypeName(obj.Metadata?.SchemaName) ?? "Result"}"; + + var map = new Dictionary(); + + for(var i = 0; i != obj.Properties.Length; i++) + { + var propType = await GetTypeAsync(obj.InnerCodecs[i], name: null, workingFile, context); + map.Add(obj.Properties[i].Name, propType); + } + + var info = new ClassGenerationInfo(name, obj, map, workingFile); + _generatedTypes.Add(info); + + info.FilePath = Path.Combine(context.OutputDirectory, info.GeneratedFilePath); + + _resultShapes.TryAdd(obj.Id, new()); + _resultShapes[obj.Id].Add(info); + + + return info.CleanTypeName; + } + + public async ValueTask PostProcessAsync(GeneratorContext context) + { + var groups = _resultShapes.Values + .SelectMany(x => x) + .GroupBy(x => x.Codec.Metadata is null ? x.Codec.Id.ToString() : x.Codec.Metadata.SchemaName ?? GetSubtypeName(x.TypeName)); + + foreach(var group in groups) + { + var iname = group.Key.Contains("::") ? group.Key.Split("::")[1] : group.Key; + + var map = await GenerateInterfaceAsync( + group.Key, + iname, + group, + context, + group.Select(x => x.CleanTypeName), + prop => group.Where(x => x.PropertyTypeMap.ContainsKey(prop)).DistinctBy(x => x.CleanTypeName).Select(x => x.CleanTypeName) + ); + + foreach (var type in group) + { + var propsMap = type.PropertyTypeMap.Select(x => + { + var property = type.Codec.Properties.First(y => y.Name == x.Key); + + return $"[EdgeDBProperty(\"{property.Name}\")]\n public {CodeGenerator.ApplyCardinality(x.Value, property.Cardinality)} {TextUtils.ToPascalCase(x.Key)} {{ get; set; }}"; + }); + + var ifaceStrMap = string.Empty; + + var ifaceMap = map + .Where(x => !group.All(y => y.PropertyTypeMap.ContainsKey(x.Key))) + .Select(x => + { + var isContained = type.PropertyTypeMap.ContainsKey(x.Key); + + var typename = CodeGenerator.ApplyCardinality(x.Value.Item1.Type, x.Value.Item1.Value.Cardinality); + + return !isContained + ? $"Optional<{typename}> I{iname}.{TextUtils.ToPascalCase(x.Key)} => Optional<{typename}>.Unspecified;" + : $"Optional<{typename}> I{iname}.{TextUtils.ToPascalCase(x.Key)} => {TextUtils.ToPascalCase(x.Key)};"; + }); + + ifaceStrMap = ifaceMap.Any() + ? $"// I{iname}\n {string.Join("\n ", ifaceMap)}" + : string.Empty; + + var path = Path.Combine(context.OutputDirectory, "Results", $"{type.CleanTypeName}.g.cs"); + + type.FilePath = path; + + var code = +$$""" +// source: {{(type.EdgeQLSourceFile is not null ? Path.GetRelativePath(path, type.EdgeQLSourceFile) : "unknown")}} +// parent: {{Path.Combine("..", "..", "Interfaces", $"I{iname}.g.cs")}} +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +namespace {{context.GenerationNamespace}}; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +public class {{type.CleanTypeName}} : I{{iname}} +{ + {{string.Join("\n\n ", propsMap)}} + + {{ifaceStrMap}} +} + +#nullable restore +#pragma warning restore CS8618 +"""; + + await File.WriteAllTextAsync(path, code); + } + } + } + + private async Task> GenerateInterfaceAsync( + string schemaName, string name, + IEnumerable subtypes, GeneratorContext context, + IEnumerable decendants, Func> getAvailability) + { + var props = subtypes.Select(x => + x.PropertyTypeMap.Select(y => + (TypeName: y.Value, Property: x.Codec.Properties.First(z => z.Name == y.Key)) + ).ToArray()) + .SelectMany(x => x) + .DistinctBy(x => x.Property.Name) + .ToDictionary(x => x, x => subtypes.All(y => y.Codec.Properties.Contains(x.Property, PropertyComparator.Instance))); + + var interfaceInfo = new InterfaceGenerationInfo($"I{name}", props.ToDictionary(x => x.Key.Property.Name, x => + { + var type = CodeGenerator.ApplyCardinality(x.Key.TypeName, x.Key.Property.Cardinality); + + if (!x.Value) + type = $"Optional<{type}>"; + + return type; + })); + + foreach(var subtype in subtypes) + { + subtype.Parent = interfaceInfo; + } + + _generatedTypes.Add(interfaceInfo); + + var propStr = + string.Join("\n ", props.Select(x => + { + var type = CodeGenerator.ApplyCardinality(x.Key.TypeName, x.Key.Property.Cardinality); + + if (!x.Value) + type = $"Optional<{type}>"; + + var summary = $"/// \n /// The {x.Key.Property.Name} property available on "; + + if (x.Value) + { + summary += "all descendants of this interface.\n"; + } + else + { + var availableOn = getAvailability(x.Key.Property.Name); + summary += $"the following types:
\n /// {string.Join("
\n /// ", availableOn.Select(x => $""))}\n"; + } + + summary += " ///
\n "; + + return $"{summary}{type} {TextUtils.ToPascalCase(x.Key.Property.Name)} {{ get; }}\n"; + })); + + var code = +$$""" +// descendants: {{string.Join("|", subtypes.Select(x => Path.Combine("..", "..", "Results", $"{x.CleanTypeName}.g.cs")))}} +// {{schemaName}} abstraction +using EdgeDB; +using EdgeDB.DataTypes; +using System; + +#nullable enable +#pragma warning disable CS8618 // nullablility is controlled by EdgeDB + +namespace {{context.GenerationNamespace}}; + +/// +/// Represents the schema type {{schemaName}} with properties that are shared across the following types:
+/// {{string.Join("
\n/// ", decendants.Select(x => $""))}} +///
+public interface I{{name}} +{ + {{propStr}} +} +#nullable restore +#pragma warning restore CS8618 + +"""; + Directory.CreateDirectory(Path.Combine(context.OutputDirectory, "Interfaces")); + + var path = Path.Combine(context.OutputDirectory, "Interfaces", $"I{name}.g.cs"); + + interfaceInfo.FilePath = path; + + await File.WriteAllTextAsync(path, code); + + return props.ToDictionary(x => x.Key.Property.Name, x => (x.Key, x.Value)); + } + + private readonly struct PropertyComparator : IEqualityComparer + { + public static readonly PropertyComparator Instance = new(); + + public bool Equals(ObjectProperty x, ObjectProperty y) + => x.Name == y.Name && x.Cardinality == y.Cardinality; + + public int GetHashCode([DisallowNull] ObjectProperty obj) + => obj.GetHashCode(); + } + } +} diff --git a/src/EdgeDB.Net.CLI/ICommand.cs b/src/EdgeDB.Net.CLI/ICommand.cs new file mode 100644 index 00000000..3d1986ad --- /dev/null +++ b/src/EdgeDB.Net.CLI/ICommand.cs @@ -0,0 +1,18 @@ +using Serilog; + +namespace EdgeDB.CLI; + +/// +/// Represents a generic command that can be executed. +/// +interface ICommand +{ + /// + /// Executes the command, awaiting its completion. + /// + /// The logger for the command. + /// + /// A task that represents the execution flow of the command. + /// + Task ExecuteAsync(ILogger logger); +} \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/Program.cs b/src/EdgeDB.Net.CLI/Program.cs new file mode 100644 index 00000000..caa2fd1c --- /dev/null +++ b/src/EdgeDB.Net.CLI/Program.cs @@ -0,0 +1,75 @@ +using CommandLine; +using CommandLine.Text; +using EdgeDB.CLI; +using EdgeDB.CLI.Arguments; +using Serilog; + +// intialize our logger +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Console() + .CreateLogger(); + +// find all types that extend the 'ICommand' interface. +var commands = typeof(Program).Assembly.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(ICommand))); + +// create our command line arg parser with no default help writer. +var parser = new Parser(x => +{ + x.HelpWriter = null; +}); + +// parse the 'args'. +ParserResult result; + +try +{ + result = parser.ParseArguments(args, commands.ToArray()); +} +catch (Exception x) +{ + Log.Logger.Error(x, "Failed to parse args: {@arg}", args); + return; +} + +try +{ + // execute the parsed result if it is a command. + var commandResult = await result.WithParsedAsync(x => + { + // if the command supports log args, change the log level for our logger. + if(x is LogArgs logArgs) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(logArgs.LogLevel) + .WriteTo.Console() + .CreateLogger(); + } + + // execute the command with the logger. + return x.ExecuteAsync(Log.Logger); + }); + + // if the result was not parsed to a valid command. + result.WithNotParsed(err => + { + // build the help text. + var helpText = HelpText.AutoBuild(commandResult, h => + { + h.AdditionalNewLineAfterOption = true; + h.Heading = "EdgeDB.Net CLI"; + h.Copyright = "Copyright (c) 2022 EdgeDB"; + + return h; + }, e => e, verbsIndex: true); + + // write out the help text. + Console.WriteLine(helpText); + }); + +} +catch (Exception x) +{ + // log the root exception. + Log.Logger.Fatal(x, "Critical error"); +} diff --git a/src/EdgeDB.Net.CLI/Properties/launchSettings.json b/src/EdgeDB.Net.CLI/Properties/launchSettings.json new file mode 100644 index 00000000..a20800e8 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "profiles": { + "EdgeDB.Net.CLI": { + "commandName": "Project", + "commandLineArgs": "watch --loglevel Debug", + "workingDirectory": "C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample" + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs b/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs new file mode 100644 index 00000000..2bcd71e3 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EdgeDB.CLI +{ + /// + /// A utility class for writing code. + /// + internal class CodeWriter + { + /// + /// The content of the code writer. + /// + public readonly StringBuilder Content = new(); + + /// + /// Gets the indentation level of the current code writer. + /// + public int IndentLevel { get; private set; } + + /// + /// The scope tracker providing an implementation of IDisposable. + /// + private readonly ScopeTracker _scopeTracker; // We only need one. It can be reused. + + /// + /// Creates a new . + /// + public CodeWriter() + { + _scopeTracker = new(this); + } + + /// + /// Appends a string to the current code writer. + /// + /// The line to append + public void Append(string line) + => Content.Append(line); + + /// + /// Appends a line to the current code writer, respecting + /// and ending in a line terminator. + /// + /// The line to append + public void AppendLine(string line) + => Content.Append(new string(' ', IndentLevel)).AppendLine(line); + + /// + /// Appends an empty line to the current code writer, adding a line terminator to the end. + /// + public void AppendLine() + => Content.AppendLine(); + + /// + /// Begins a new scope with the specified line. + /// + /// The line to append. + /// An representing the scope returned. + public IDisposable BeginScope(string line) + { + AppendLine(line); + return BeginScope(); + } + + /// + /// Begins a new scope, incrementing the indent level until the scope is disposed. + /// + /// An representing the scope returned. + public IDisposable BeginScope() + { + Content.Append(new string(' ', IndentLevel)).AppendLine("{"); + IndentLevel += 4; + return _scopeTracker; + } + + /// + /// Ends a scope, decrementing the indent level. + /// + public void EndScope() + { + IndentLevel -= 4; + Content.Append(new string(' ', IndentLevel)).AppendLine("}"); + } + + /// + /// Converts the current code writer to a . + /// + /// A string representing the code written to the code writer. + public override string ToString() + => Content.ToString(); + + /// + /// An implementation of responsible for scope decrementing. + /// + class ScopeTracker : IDisposable + { + /// + /// Gets the That created this . + /// + public CodeWriter Parent { get; } + + /// + /// Constructs a new . + /// + /// The parent that created the . + public ScopeTracker(CodeWriter parent) + { + Parent = parent; + } + + /// + /// Disposes and ends the scope of this . + /// + public void Dispose() + { + Parent.EndScope(); + } + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs b/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs new file mode 100644 index 00000000..8ed422cf --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to the console. + /// + internal class ConsoleUtils + { + /// + /// Reads a secret input from STDIN. + /// + /// + /// The entered input received from STDIN. + /// + public static string ReadSecretInput() + { + string input = ""; + ConsoleKey key; + do + { + var keyInfo = Console.ReadKey(true); + key = keyInfo.Key; + + if (key == ConsoleKey.Backspace) + input = input.Length > 0 ? input[..^1] : ""; + else if(!char.IsControl(keyInfo.KeyChar)) + input += keyInfo.KeyChar; + } + while (key != ConsoleKey.Enter); + + return input; + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/FileUtils.cs b/src/EdgeDB.Net.CLI/Utils/FileUtils.cs new file mode 100644 index 00000000..aef28823 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/FileUtils.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to file operations. + /// + internal static class FileUtils + { + /// + /// Waits synchronously for a file to be released. + /// + /// The file path. + /// The timeout. + /// if the file was released; otherwise . + public static bool WaitForHotFile(string path, int timeout = 5000) + { + var start = DateTime.UtcNow; + while (true) + { + try + { + using var fs = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + fs.Close(); + return true; + } + catch + { + if ((DateTime.UtcNow - start).TotalMilliseconds >= timeout) + return false; + + Thread.Sleep(200); + } + } + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/HashUtils.cs b/src/EdgeDB.Net.CLI/Utils/HashUtils.cs new file mode 100644 index 00000000..60716046 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/HashUtils.cs @@ -0,0 +1,27 @@ +using EdgeDB.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to hashes. + /// + internal static class HashUtils + { + /// + /// Hashes edgeql for an autogenerated file header. + /// + /// The edgeql to hash. + /// The hashed edgeql as hex. + public static string HashEdgeQL(string edgeql) + { + using var algo = SHA256.Create(); + return HexConverter.ToHex(algo.ComputeHash(Encoding.UTF8.GetBytes(edgeql))); + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs b/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs new file mode 100644 index 00000000..25980ebf --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs @@ -0,0 +1,189 @@ +using CliWrap; +using EdgeDB.Utils; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods realted to edgedb projects. + /// + internal static class ProjectUtils + { + public static string EdgeQLNetDataDir + => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), + "edgeql-net" + ); + + static ProjectUtils() + { + Directory.CreateDirectory(EdgeQLNetDataDir); + } + + /// + /// Gets the edgedb project root from the current directory. + /// + /// The project root directory. + /// The project could not be found. + public static string GetProjectRoot() + { + var directory = Environment.CurrentDirectory; + bool foundRoot = false; + + while (!foundRoot) + { + if ( + !(foundRoot = Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly).Any(x => x.EndsWith($"{Path.DirectorySeparatorChar}edgedb.toml"))) && + (directory = Directory.GetParent(directory!)?.FullName) is null) + throw new FileNotFoundException("Could not find edgedb.toml in the current and parent directories"); + } + + return directory; + } + + public static bool TryGetProjectRoot([MaybeNullWhen(false)] out string root) + { + root = null; + + try + { + root = GetProjectRoot(); + return true; + } + catch (FileNotFoundException) + { + return false; + } + } + + /// + /// Starts a watcher process. + /// + /// The connection info for the watcher process. + /// The project root directory. + /// The output directory for files the watcher generates to place. + /// The namespace for generated files. + /// The started watcher process id. + public static int StartBackgroundWatchProcess(EdgeDBConnection connection, string root, string outputDir, string @namespace) + { + var current = Process.GetCurrentProcess(); + var connString = JsonConvert.SerializeObject(connection).Replace("\"", "\\\""); + + return Process.Start(new ProcessStartInfo + { + FileName = current.MainModule!.FileName, + Arguments = $"watch --raw-connection \"{connString}\" -t \"{root}\" -o \"{outputDir}\" -n \"{@namespace}\"", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WindowStyle = ProcessWindowStyle.Hidden + })!.Id; + } + + /// + /// Gets the watcher process for the provided root directory and/or its parents. + /// + /// The project root. + /// + /// The watcher or if not found. + /// + public static Process? GetWatcherProcess(string root) + { + string? path = root; + Process? proc = null; + while(path is not null) + { + var target = GetWatcherProcessTargetPath(path); + if (File.Exists(target)) + { + var rawPid = File.ReadAllText(target); + + if (int.TryParse(rawPid, out var pid) && (proc = Process.GetProcesses().FirstOrDefault(x => x.Id == pid)) is not null) + return proc; + } + + path = Directory.GetParent(path)?.FullName; + } + + return null; + } + + /// + /// Registers the current process as the watcher project for the given project root. + /// + /// The project root. + public static void RegisterProcessAsWatcher(string root) + { + var id = Environment.ProcessId; + + if(GetWatcherProcess(root) is not null) + throw new ArgumentException("Watch directory already has a watcher", nameof(root)); + + File.WriteAllText(GetWatcherProcessTargetPath(root), id.ToString()); + } + + private static string GetWatcherProcessTargetPath(string root) + { + using var hash = SHA256.Create(); + + var name = HexConverter.ToHex(hash.ComputeHash(Encoding.UTF8.GetBytes(root))); + + return Path.Combine(EdgeQLNetDataDir, name); + } + + /// + /// Creates a dotnet project. + /// + /// The target directory. + /// The name of the project + /// The project failed to be created. + public static async Task CreateGeneratedProjectAsync(string root, string name) + { + var result = await Cli.Wrap("dotnet") + .WithArguments($"new classlib --framework \"net6.0\" -n {name}") + .WithWorkingDirectory(root) + .WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError())) + .WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput())) + .ExecuteAsync(); + + if (result.ExitCode != 0) + throw new IOException($"Failed to create new project"); + + result = await Cli.Wrap("dotnet") + .WithArguments("add package EdgeDB.Net.Driver") + .WithWorkingDirectory(Path.Combine(root, name)) + .WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError())) + .WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput())) + .ExecuteAsync(); + + if (result.ExitCode != 0) + throw new IOException($"Failed to create new project"); + + // remove default file + if(File.Exists(Path.Combine(root, name, "Class1.cs"))) + File.Delete(Path.Combine(root, name, "Class1.cs")); + } + + /// + /// Gets a list of edgeql file paths for the provided root directory. + /// + /// + /// migration files are ignored. + /// + /// The root directory to scan for edgeql files. + /// + /// An that enumerates a collection of files ending in .edgeql. + /// + public static IEnumerable GetTargetEdgeQLFiles(string root) + => Directory.GetFiles(root, "*.edgeql", SearchOption.AllDirectories).Where(x => !x.Contains(Path.Combine("dbschema", "migrations"))); + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/TextUtils.cs b/src/EdgeDB.Net.CLI/Utils/TextUtils.cs new file mode 100644 index 00000000..66c4c722 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/TextUtils.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to text operations. + /// + internal static class TextUtils + { + /// + /// The current culture info. + /// + private static CultureInfo? _cultureInfo; + + /// + /// Converts the given string to pascal case. + /// + /// The string to convert to pascal case. + /// A pascal-cased version of the input string. + public static string ToPascalCase(string input) + { + _cultureInfo ??= CultureInfo.CurrentCulture; + var t = Regex.Replace(input, @"[^^]([A-Z])", m => $"{m.Value[0]} {m.Groups[1].Value}"); + + return _cultureInfo.TextInfo.ToTitleCase(t.Replace("_", " ")).Replace(" ", ""); + } + + /// + /// Converts the given string to camel case. + /// + /// The string to convert to pascal case. + /// A camel-cased version of the input string. + public static string ToCamelCase(string input) + { + var p = ToPascalCase(input); + return $"{p[0].ToString().ToLower()}{p[1..]}"; + } + + public static string EscapeToSourceCode(string x, bool isExactStr = false) + { + return x.Replace("\"", isExactStr ? "\"\"" : "\\\"").ReplaceLineEndings(string.Empty); + } + + public static string EscapeToXMLComment(string x) + { + return x.Replace(">", ">").Replace("<", "<"); + } + + public static string? NameWithoutModule(string? s) + { + if (string.IsNullOrEmpty(s)) + return s; + + return s.Contains("::") ? s.Split("::")[1] : s; + } + + public static string? CleanTypeName(string? s) + { + if (string.IsNullOrEmpty(s)) + return s; + + if (!s.Contains("::")) + return s; + + return ToPascalCase(s.Replace("::", "_")); + } + } +} diff --git a/src/EdgeDB.Net.Driver/AssemblyInfo.cs b/src/EdgeDB.Net.Driver/AssemblyInfo.cs index 2c4eae45..c2b2a32a 100644 --- a/src/EdgeDB.Net.Driver/AssemblyInfo.cs +++ b/src/EdgeDB.Net.Driver/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("EdgeDB.Net.QueryBuilder")] +[assembly: InternalsVisibleTo("EdgeDB.Net.CLI")] [assembly: InternalsVisibleTo("EdgeDB.Runtime")] [assembly: InternalsVisibleTo("EdgeDB.ExampleApp")] [assembly: InternalsVisibleTo("EdgeDB.DotnetTool")] diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/CodecBuilder.cs b/src/EdgeDB.Net.Driver/Binary/Builders/CodecBuilder.cs index f4bdecb8..d2c48f04 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/CodecBuilder.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/CodecBuilder.cs @@ -1,4 +1,5 @@ using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol; using Newtonsoft.Json.Linq; using System; using System.Collections.Concurrent; @@ -7,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -24,22 +26,24 @@ public CodecInfo(Guid id, ICodec codec) } } - internal sealed class CodecBuilder + internal sealed class CodecCache { - public static ICollection CachedCodecs - => CodecCache.Values; - /// /// The codec cache mapped to result types. /// - public static readonly ConcurrentDictionary CodecCache = new(); + public readonly ConcurrentDictionary Cache = new(); /// /// The codec cached mapped to the type of codec, storing instances. /// - public static readonly ConcurrentDictionary CodecInstanceCache = new(); + public readonly ConcurrentDictionary CodecInstanceCache = new(); + + public readonly ConcurrentDictionary CompiledCodecCache = new(); + } - public static readonly ConcurrentDictionary CompiledCodecCache = new(); + internal sealed class CodecBuilder + { + public static readonly ConcurrentDictionary CodecCaches = new(); public static readonly Guid NullCodec = Guid.Empty; public static readonly Guid InvalidCodec = Guid.Parse("ffffffffffffffffffffffffffffffff"); @@ -54,35 +58,17 @@ static CodecBuilder() _scalarCodecs = new(); _scalarCodecMap = new(); - var codecs = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(x => x - .GetInterfaces() - .Any(x => x.Name == "ICodec") && !(x.IsAbstract || x.IsInterface) && x.Name != "RuntimeCodec`1" && x.Name != "RuntimeScalarCodec`1"); - - var scalars = new List(codecs - .Where(x => x.IsAssignableTo(typeof(IScalarCodec)) && x.IsAssignableTo(typeof(ICacheableCodec))) - .Select(x => (IScalarCodec)Activator.CreateInstance(x)!) - ); - - CodecInstanceCache = new(scalars.ToDictionary(x => x.GetType(), x => (ICodec)x)); - - foreach(var complexCodecs in scalars.Where(x => x is IComplexCodec).Cast().ToArray()) - { - complexCodecs.BuildRuntimeCodecs(); - - foreach(var scalarComplex in complexCodecs.RuntimeCodecs - .Where(x => x is IScalarCodec) - .Cast()) - { - _scalarCodecs.Add(scalarComplex); - } - } + var scalars = _scalarCodecTypeMap + .Where(x => x.Value.IsAssignableTo(typeof(IScalarCodec))) + .Select(x => (IScalarCodec)Activator.CreateInstance(x.Value, args: new object?[] { null })!); _scalarCodecMap = new(scalars.ToDictionary(x => x.ConverterType, x => x)); _scalarCodecs.AddRange(scalars); } + public static CodecCache GetProviderCache(IProtocolProvider provider) + => CodecCaches.GetOrAdd(provider, p => new()); + public static bool ContainsScalarCodec(Type type) => _scalarCodecs.Any(x => x.ConverterType == type); @@ -95,10 +81,14 @@ public static bool TryGetScalarCodec(Type type, [MaybeNullWhen(false)] out IScal public static ulong GetCacheHashKey(string query, Cardinality cardinality, IOFormat format) => unchecked(CalculateKnuthHash(query) * (ulong)cardinality * (ulong)format); - public static bool TryGetCodecs(ulong hash, + public static bool TryGetCodecs( + IProtocolProvider provider, + ulong hash, [MaybeNullWhen(false)] out CodecInfo inCodecInfo, [MaybeNullWhen(false)] out CodecInfo outCodecInfo) { + var providerCache = GetProviderCache(provider); + inCodecInfo = null; outCodecInfo = null; @@ -106,8 +96,8 @@ public static bool TryGetCodecs(ulong hash, ICodec? outCodec; if (_codecKeyMap.TryGetValue(hash, out var codecIds) - && (CodecCache.TryGetValue((codecIds.InCodec), out inCodec) || ((inCodec = GetScalarCodec(codecIds.InCodec)) != null)) - && (CodecCache.TryGetValue((codecIds.OutCodec), out outCodec) || ((outCodec = GetScalarCodec(codecIds.OutCodec)) != null))) + && (providerCache.Cache.TryGetValue((codecIds.InCodec), out inCodec) || ((inCodec = GetScalarCodec(provider, codecIds.InCodec)) != null)) + && (providerCache.Cache.TryGetValue((codecIds.OutCodec), out outCodec) || ((outCodec = GetScalarCodec(provider, codecIds.OutCodec)) != null))) { inCodecInfo = new(codecIds.InCodec, inCodec); outCodecInfo = new(codecIds.OutCodec, outCodec); @@ -120,8 +110,8 @@ public static bool TryGetCodecs(ulong hash, public static void UpdateKeyMap(ulong hash, Guid inCodec, Guid outCodec) => _codecKeyMap[hash] = (inCodec, outCodec); - public static ICodec? GetCodec(Guid id) - => CodecCache.TryGetValue(id, out var codec) ? codec : GetScalarCodec(id); + public static ICodec? GetCodec(IProtocolProvider provider, in Guid id) + => GetProviderCache(provider).Cache.TryGetValue(id, out var codec) ? codec : GetScalarCodec(provider, id); public static ICodec BuildCodec(EdgeDBBinaryClient client, Guid id, byte[] buff) { @@ -132,71 +122,101 @@ public static ICodec BuildCodec(EdgeDBBinaryClient client, Guid id, byte[] buff) public static ICodec BuildCodec(EdgeDBBinaryClient client, Guid id, ref PacketReader reader) { if (id == NullCodec) - return GetOrCreateCodec(); - - List codecs = new(); + return GetOrCreateCodec(client.ProtocolProvider); + var providerCache = GetProviderCache(client.ProtocolProvider); + + List descriptorsList = new(); while (!reader.Empty) { var start = reader.Position; - var typeDescriptor = ITypeDescriptor.GetDescriptor(ref reader); + var typeDescriptor = client.ProtocolProvider.GetDescriptor(ref reader); var end = reader.Position; - client.Logger.TraceTypeDescriptor(typeDescriptor, typeDescriptor.Id, end - start, $"{end}/{reader.Data.Length}".PadRight(reader.Data.Length.ToString().Length *2 + 2)); + client.Logger.TraceTypeDescriptor( + typeDescriptor.GetType().Name, + typeDescriptor.Id, + $"{(end - start)}b".PadRight(reader.Data.Length.ToString().Length), + $"{end}/{reader.Data.Length}".PadRight(reader.Data.Length.ToString().Length * 2 + 2) + ); + + descriptorsList.Add(typeDescriptor); + } + + var descriptors = descriptorsList.ToArray(); + var codecs = new ICodec?[descriptors.Length]; + + for(var i = 0; i != descriptors.Length; i++) + { + ref var descriptor = ref descriptors[i]; - if (!CodecCache.TryGetValue(typeDescriptor.Id, out var codec)) - codec = GetScalarCodec(typeDescriptor.Id); + if (!providerCache.Cache.TryGetValue(descriptor.Id, out var codec)) + codec = GetScalarCodec(client.ProtocolProvider, descriptor.Id); if (codec is not null) - codecs.Add(codec); + codecs[i] = codec; else { - codec = typeDescriptor switch + codec = client.ProtocolProvider.BuildCodec( + in descriptor, + (in int i) => ref codecs[i], + (in int i) => ref descriptors[i] + ); + + codecs[i] = codec; + + if(codec is not null) { - EnumerationTypeDescriptor enumeration => GetOrCreateCodec(), - NamedTupleTypeDescriptor namedTuple => new ObjectCodec(namedTuple, codecs), - ObjectShapeDescriptor @object => new ObjectCodec(@object, codecs), - InputShapeDescriptor input => new SparceObjectCodec(input, codecs), - TupleTypeDescriptor tuple => new TupleCodec(tuple.ElementTypeDescriptorsIndex.Select(x => codecs[x]).ToArray()), - RangeTypeDescriptor range => new CompilableWrappingCodec(typeDescriptor.Id, codecs[range.TypePos], typeof(RangeCodec<>)), // (ICodec)Activator.CreateInstance(typeof(RangeCodec<>).MakeGenericType(codecs[range.TypePos].ConverterType), codecs[range.TypePos])!, - ArrayTypeDescriptor array => new CompilableWrappingCodec(typeDescriptor.Id, codecs[array.TypePos], typeof(ArrayCodec<>)), //(ICodec)Activator.CreateInstance(typeof(Array<>).MakeGenericType(codecs[array.TypePos].ConverterType), codecs[array.TypePos])!, - SetTypeDescriptor set => new CompilableWrappingCodec(typeDescriptor.Id, codecs[set.TypePos], typeof(SetCodec<>)), //(ICodec)Activator.CreateInstance(typeof(Set<>).MakeGenericType(codecs[set.TypePos].ConverterType), codecs[set.TypePos])!, - BaseScalarTypeDescriptor scalar => throw new MissingCodecException($"Could not find the scalar type {scalar.Id}. Please file a bug report with your query that caused this error."), - _ => throw new MissingCodecException($"Could not find a type descriptor with type {typeDescriptor.Id}. Please file a bug report with your query that caused this error.") - }; - - codecs.Add(codec); - - if (!CodecCache.TryAdd(typeDescriptor.Id, codec)) - client.Logger.CodecCouldntBeCached(codec, id); - else - client.Logger.CodecAddedToCache(id, codec); + if (!providerCache.Cache.TryAdd(descriptor.Id, codec)) + client.Logger.CodecCouldntBeCached(codec, id); + else + client.Logger.CodecAddedToCache(id, codec); + } } } - var finalCodec = codecs.Last(); + ICodec? finalCodec = null; + + for(var i = 1; i != codecs.Length + 1 && finalCodec is null; i++) + finalCodec = codecs[^i]; + - client.Logger.TraceCodecBuilderResult(finalCodec, codecs.Count, CodecCache.Count); + if (finalCodec is null) + throw new MissingCodecException("Failed to find end tail of codec tree"); + + client.Logger.TraceCodecBuilderResult(CodecFormatter.Format(finalCodec).ToString(), codecs.Length, providerCache.Cache.Count); return finalCodec; } - public static ICodec? GetScalarCodec(Guid typeId) + public static ICodec? GetScalarCodec(IProtocolProvider provider, Guid typeId) { - if (_defaultCodecs.TryGetValue(typeId, out var codecType)) + if (_scalarCodecTypeMap.TryGetValue(typeId, out var codecType)) { - var codec = CodecInstanceCache.GetOrAdd(codecType, t => (ICodec)Activator.CreateInstance(t)!); + if (_scalarCodecMap.TryGetValue(codecType, out var scalar)) + return scalar; + + var codec = GetProviderCache(provider) + .CodecInstanceCache + .GetOrAdd( + codecType, + t => (ICodec)Activator.CreateInstance(t, args: new object?[] { null })! + ); - CodecCache[typeId] = codec; + GetProviderCache(provider).Cache[typeId] = codec; return codec; } return null; } - private static ICodec GetOrCreateCodec() + internal static ICodec GetOrCreateCodec(IProtocolProvider provider) where T : ICodec, new() - => CodecInstanceCache.GetOrAdd(typeof(T), _ => new T()); + => GetProviderCache(provider).CodecInstanceCache.GetOrAdd(typeof(T), _ => new T()); + + internal static ICodec GetOrCreateCodec(IProtocolProvider provider, Func factory) + where T : ICodec + => GetProviderCache(provider).CodecInstanceCache.GetOrAdd(typeof(T), factory); private static ulong CalculateKnuthHash(string content) { @@ -209,7 +229,7 @@ private static ulong CalculateKnuthHash(string content) return hashedValue; } - private static readonly Dictionary _defaultCodecs = new() + private static readonly Dictionary _scalarCodecTypeMap = new() { { NullCodec, typeof(NullCodec) }, { new Guid("00000000-0000-0000-0000-000000000100"), typeof(Codecs.UUIDCodec) }, diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs b/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs index 0c89ed35..0a185f45 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/ObjectBuilder.cs @@ -1,4 +1,3 @@ -using EdgeDB.Binary.Packets; using EdgeDB.Binary.Codecs; using EdgeDB.DataTypes; using System.Collections; @@ -11,32 +10,48 @@ namespace EdgeDB { internal sealed class ObjectBuilder { - private static readonly Dictionary _codecVisitorStateTable = new(); + private static readonly Dictionary _codecVisitorStateTable = new(); private static readonly object _visitorLock = new(); - public static TType? BuildResult(EdgeDBBinaryClient client, ICodec codec, ref Data data) + public static TType? BuildResult(EdgeDBBinaryClient client, ICodec codec, in ReadOnlyMemory data) { // TO INVESTIGATE: since a codec can only be "visited" or "mutated" for // one type at a time, we have to ensure that the codec is ready to deserialize // TType, we can store the states of the codecs here for building result // to achieve this. + var typeCodec = codec; + bool wasSkipped = false; lock(_visitorLock) { - // if TType is object, we *have* to walk since external factors (such as config changes) can influence - // the overall behaviour of the type visitor, therefor we cant guarantee the cached state is correct. - wasSkipped = typeof(TType) != typeof(object) && _codecVisitorStateTable.TryGetValue(codec.GetHashCode(), out var typeId) && typeId == typeof(TType).GUID; + // Since the supplied codec could be a template, we walk the codec and cache based on the supplied type + // if the codec hasn't been walked OR the result type is an object we walk it and cache it if its not an + // object codec. + if (typeof(TType) != typeof(object) && _codecVisitorStateTable.TryGetValue(typeof(TType), out var info)) + { + if(codec.GetHashCode() == info.Version) + { + typeCodec = info.Codec; + wasSkipped = true; + } + } if (!wasSkipped) { + var version = codec.GetHashCode(); + var visitor = new TypeVisitor(client); visitor.SetTargetType(typeof(TType)); visitor.Visit(ref codec); - if(typeof(TType) != typeof(object)) - _codecVisitorStateTable[codec.GetHashCode()] = typeof(TType).GUID; + if (typeof(TType) != typeof(object)) + _codecVisitorStateTable[typeof(TType)] = (version, codec); + + typeCodec = codec; + + client.Logger.ObjectDeserializationPrep(CodecFormatter.Format(typeCodec).ToString()); } } @@ -46,12 +61,12 @@ internal sealed class ObjectBuilder } - if (codec is ObjectCodec objectCodec) + if (typeCodec is ObjectCodec objectCodec) { - return (TType?)TypeBuilder.BuildObject(client, typeof(TType), objectCodec, ref data); + return (TType?)TypeBuilder.BuildObject(client, typeof(TType), objectCodec, in data); } - var value = codec.Deserialize(client, data.PayloadBuffer); + var value = typeCodec.Deserialize(client, in data); return (TType?)ConvertTo(typeof(TType), value); } diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/ObjectEnumerator.cs b/src/EdgeDB.Net.Driver/Binary/Builders/ObjectEnumerator.cs index 8e92a6a9..26beafe2 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/ObjectEnumerator.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/ObjectEnumerator.cs @@ -17,24 +17,25 @@ public ref struct ObjectEnumerator { internal static readonly Type RefType = typeof(ObjectEnumerator).MakeByRefType(); - public int Length => _names.Length; - internal PacketReader Reader; - internal readonly ICodec[] Codecs; - private readonly string[] _names; + public readonly int Length + => Properties.Length; + + internal readonly ObjectProperty[] Properties; + private readonly int _numElements; private readonly CodecContext _context; + + internal PacketReader Reader; private int _pos; internal ObjectEnumerator( - ref Span data, + in ReadOnlySpan data, int position, - string[] names, - ICodec[] codecs, + ObjectProperty[] properties, CodecContext context) { - Reader = new PacketReader(ref data, position); - Codecs = codecs; - _names = names; + Reader = new PacketReader(in data, position); + Properties = properties; _pos = 0; _context = context; @@ -86,7 +87,7 @@ public bool Next([NotNullWhen(true)] out string? name, out object? value) if (length == -1) { - name = _names[_pos]; + name = Properties[_pos].Name; value = null; _pos++; return true; @@ -94,11 +95,12 @@ public bool Next([NotNullWhen(true)] out string? name, out object? value) Reader.ReadBytes(length, out var buff); - var innerReader = new PacketReader(ref buff); - name = _names[_pos]; - var codec = Codecs[_pos]; + var innerReader = new PacketReader(in buff); + ref var property = ref Properties[_pos]; + + name = property.Name; + value = property.Codec.Deserialize(ref innerReader, _context); - value = codec.Deserialize(ref innerReader, _context); _pos++; return true; } diff --git a/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs b/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs index a3d8fd79..b3685a80 100644 --- a/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs +++ b/src/EdgeDB.Net.Driver/Binary/Builders/TypeBuilder.cs @@ -1,4 +1,3 @@ -using EdgeDB.Binary.Packets; using EdgeDB.Binary.Codecs; using System.Collections; using System.Collections.Concurrent; @@ -143,7 +142,7 @@ internal static bool TryGetTypeDeserializerInfo(Type type, [MaybeNullWhen(false) return info is not null; } - internal static object? BuildObject(EdgeDBBinaryClient client, Type type, Binary.Codecs.ObjectCodec codec, ref Data data) + internal static object? BuildObject(EdgeDBBinaryClient client, Type type, Binary.Codecs.ObjectCodec codec, in ReadOnlyMemory data) { if (!IsValidObjectType(type)) throw new InvalidOperationException($"Cannot deserialize data to {type.Name}"); @@ -157,7 +156,7 @@ internal static bool TryGetTypeDeserializerInfo(Type type, [MaybeNullWhen(false) if (codec is not TypeInitializedObjectCodec typeCodec) codec = codec.GetOrCreateTypeCodec(type); - var reader = new PacketReader(data.PayloadBuffer); + var reader = new PacketReader(data.Span); return codec.Deserialize(ref reader, client.CodecContext); } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/ArrayCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/ArrayCodec.cs index 9d864707..bd098d9e 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/ArrayCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/ArrayCodec.cs @@ -1,3 +1,5 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class ArrayCodec @@ -14,7 +16,8 @@ internal sealed class ArrayCodec internal ICodec InnerCodec; - public ArrayCodec(ICodec innerCodec) + public ArrayCodec(in Guid id, ICodec innerCodec, CodecMetadata? metadata = null) + : base(in id, metadata) { InnerCodec = innerCodec; } @@ -47,7 +50,7 @@ public ArrayCodec(ICodec innerCodec) { var elementLength = reader.ReadInt32(); reader.ReadBytes(elementLength, out var innerData); - array[i] = InnerCodec.Deserialize(context, innerData); + array[i] = InnerCodec.Deserialize(context, in innerData); } return array; @@ -84,6 +87,9 @@ public override void Serialize(ref PacketWriter writer, T?[]? value, CodecContex } } + public override string ToString() + => "std::array"; + ICodec IWrappingCodec.InnerCodec { get => InnerCodec; diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/BigIntCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/BigIntCodec.cs index 635f14de..ecc3fbc0 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/BigIntCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/BigIntCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using EdgeDB.Utils; using System.Numerics; @@ -6,8 +7,14 @@ namespace EdgeDB.Binary.Codecs internal sealed class BigIntCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000108"); + public static readonly BigInteger Base = 10000; + public BigIntCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override BigInteger Deserialize(ref PacketReader reader, CodecContext context) { var numDigits = reader.ReadUInt16(); @@ -69,5 +76,8 @@ public override void Serialize(ref PacketWriter writer, BigInteger value, CodecC for (int i = digits.Count - 1; i >= 0; i--) writer.Write(digits[i]); } + + public override string ToString() + => "std::bigint"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/BoolCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/BoolCodec.cs index dff358eb..c3c95640 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/BoolCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/BoolCodec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class BoolCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000109"); + + public BoolCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override bool Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadBoolean(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, bool value, CodecContext { writer.Write(value); } + + public override string ToString() + => "std::bool"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/BytesCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/BytesCodec.cs index b618a69f..e4f2aeae 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/BytesCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/BytesCodec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class BytesCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000102"); + + public BytesCodec(CodecMetadata? metadata = null) + : base (in Id, metadata) + { } + public override byte[] Deserialize(ref PacketReader reader, CodecContext context) { return reader.ConsumeByteArray(); @@ -13,5 +21,8 @@ public override void Serialize(ref PacketWriter writer, byte[]? value, CodecCont if (value is not null) writer.Write(value); } + + public override string ToString() + => "std::bytes"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/CodecFormatter.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/CodecFormatter.cs new file mode 100644 index 00000000..75089063 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/CodecFormatter.cs @@ -0,0 +1,163 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using EdgeDB.Binary.Protocol.V2._0.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Codecs +{ + internal static class CodecFormatter + { + public static StringBuilder Format(ICodec codec, int spacing = 2) + { + StringBuilder sb = new(); + + var depth = 0; + + Process(codec, ref sb, in spacing, ref depth); + + return sb; + } + + private static string Padding(in int depth, string s) + => "".PadLeft(depth) + s; + + private static void Process( + ICodec codec, + scoped ref StringBuilder builder, + scoped in int spacing, + scoped ref int depth) + { + switch (codec) + { + case IWrappingCodec wrapping: + builder.AppendLine(Padding(in depth, $"<{codec.Id}> {codec} {{")); + depth += spacing; + builder.Append(FormatMetadata(codec.Metadata, ref depth, in spacing)); + builder.AppendLine(Padding(in depth, "child: {")); + depth += spacing; + Process(wrapping.InnerCodec, ref builder, in spacing, ref depth); + depth -= spacing; + builder.AppendLine(Padding(in depth, "}")); + depth -= spacing; + builder.AppendLine(Padding(in depth, "}")); + break; + case IMultiWrappingCodec multiWrapping: + var names = multiWrapping switch + { + ObjectCodec obj => obj.Properties.Select(x => x.Name), + SparceObjectCodec sparse => sparse.FieldNames, + _ => null + }; + + builder.AppendLine(Padding(in depth, $"<{codec.Id}> {codec} {{")); + + depth += spacing; + builder.Append(FormatMetadata(codec.Metadata, ref depth, in spacing)); + + if(multiWrapping.InnerCodecs.Length == 0) + { + builder.AppendLine(Padding(in depth, "children: []")); + + if(codec.Metadata is not null) + { + depth -= spacing; + + builder.AppendLine(Padding(in depth, "}")); + } + } + else + { + builder.AppendLine(Padding(in depth, "children: [")); + depth += spacing; + for (int i = 0; i != multiWrapping.InnerCodecs.Length; i++) + { + var pos = builder.Length; + Process(multiWrapping.InnerCodecs[i], ref builder, in spacing, ref depth); + + if (names is not null) + builder.Insert(pos + depth, $"{names.ElementAt(i)}: "); + } + depth -= codec.Metadata is not null ? spacing * 2 : spacing; + builder.AppendLine(Padding(in depth, "]")); + } + + depth -= spacing; + builder.AppendLine(Padding(in depth, "}")); + break; + case CompilableWrappingCodec compilable: + builder.AppendLine(Padding(in depth, $"<{codec.Id}> {codec} {{")); + depth += spacing; + builder.Append(FormatMetadata(codec.Metadata, ref depth, in spacing)); + builder.AppendLine(Padding(in depth, "child: {")); + depth += spacing; + Process(compilable.InnerCodec, ref builder, in spacing, ref depth); + depth -= spacing; + builder.AppendLine(Padding(in depth, "}")); + depth -= spacing; + builder.AppendLine(Padding(in depth, "}")); + break; + default: + var metadata = FormatMetadata(codec.Metadata, ref depth, in spacing); + + if(metadata is null) + builder.AppendLine(Padding(in depth, $"<{codec.Id}> {codec}")); + else + { + builder.AppendLine(Padding(in depth, $"<{codec.Id}> {codec} {{")); + depth += spacing; + builder.Append(metadata); + depth -= spacing; + } + break; + } + } + + private static StringBuilder? FormatMetadata(CodecMetadata? metadata, scoped ref int depth, scoped in int spacing) + { + if (metadata is null) + return null; + + var sb = new StringBuilder(); + + sb.AppendLine(Padding(in depth, "metadata: {")); + + depth += spacing; + + sb.Append(Padding(in depth, "schema_name: ")); + sb.Append(metadata.SchemaName ?? "NULL"); + sb.AppendLine(","); + + sb.Append(Padding(in depth, "is_schema_defined: ")); + sb.Append(metadata.IsSchemaDefined); + sb.AppendLine(","); + sb.Append(Padding(in depth, "ancestors: [")); + + if (metadata.Ancestors is null || metadata.Ancestors.Length == 0) + sb.AppendLine("],"); + else + { + depth += spacing; + sb.AppendLine(); + + for(var i = 0; i != metadata.Ancestors.Length; i++) + { + var ancestor = metadata.Ancestors[i]; + + var name = ancestor.Codec is not null + ? ancestor.Codec.GetType().Name + : ancestor.Descriptor.GetType().Name; + + sb.AppendLine(Padding(in depth, $"[{(ancestor.Codec?.Id ?? ancestor.Descriptor.Id)}]: {name}")); + } + + depth -= spacing; + sb.AppendLine(Padding(in depth, "],")); + } + + return sb; + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/CompilableCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/CompilableCodec.cs index 5bd05165..9be9ffbf 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/CompilableCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/CompilableCodec.cs @@ -1,3 +1,5 @@ +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; namespace EdgeDB.Binary.Codecs @@ -5,20 +7,28 @@ namespace EdgeDB.Binary.Codecs internal sealed class CompilableWrappingCodec : ICodec { + public Guid Id + => _id; + + public CodecMetadata? Metadata + => _metadata; + public ICodec InnerCodec { get; } private readonly Guid _id; private readonly Type _rootCodecType; + private readonly CodecMetadata? _metadata; - public CompilableWrappingCodec(Guid id, ICodec innerCodec, Type rootCodecType) + public CompilableWrappingCodec(in Guid id, ICodec innerCodec, Type rootCodecType, CodecMetadata? metadata = null) { _id = id; InnerCodec = innerCodec; _rootCodecType = rootCodecType; + _metadata = metadata; } // to avoid state changes to this compilable, pass in the inner codec post-walk. - public ICodec Compile(Type type, ICodec? innerCodec = null) + public ICodec Compile(IProtocolProvider provider, Type type, ICodec? innerCodec = null) { innerCodec ??= InnerCodec; @@ -26,13 +36,13 @@ public ICodec Compile(Type type, ICodec? innerCodec = null) var cacheKey = HashCode.Combine(type, genType, _id); - return CodecBuilder.CompiledCodecCache.GetOrAdd(cacheKey, (k) => + return CodecBuilder.GetProviderCache(provider).CompiledCodecCache.GetOrAdd(cacheKey, (k) => { - var codec = (ICodec)Activator.CreateInstance(genType, innerCodec)!; + var codec = (ICodec)Activator.CreateInstance(genType, _id, innerCodec, _metadata)!; if(codec is IComplexCodec complex) { - complex.BuildRuntimeCodecs(); + complex.BuildRuntimeCodecs(provider); } return codec; @@ -45,9 +55,7 @@ public Type GetInnerType() : InnerCodec.ConverterType; public override string ToString() - { - return $"[{_id}] CompilableWrappingCodec{{{_rootCodecType.Name}<{InnerCodec}>}}"; - } + => $"compilable({_rootCodecType.Name})"; Type ICodec.ConverterType => throw new NotSupportedException(); bool ICodec.CanConvert(Type t) => throw new NotSupportedException(); diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/CompoundCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/CompoundCodec.cs new file mode 100644 index 00000000..e7fcda75 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/CompoundCodec.cs @@ -0,0 +1,40 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using EdgeDB.Binary.Protocol.V2._0.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Codecs +{ + internal class CompoundCodec : BaseCodec + { + private readonly ICodec[] _codecs; + private readonly TypeOperation _operation; + + + public CompoundCodec(in Guid id, TypeOperation operation, ICodec[] codecs, CodecMetadata? metadata = null) + : base(in id, metadata) + { + _operation = operation; + _codecs = codecs; + } + + public override object? Deserialize(ref PacketReader reader, CodecContext context) + { + // TODO + + return null!; + } + + + public override void Serialize(ref PacketWriter writer, object? value, CodecContext context) + { + // TODO + } + + public override string ToString() + => "compound"; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/DecimalCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/DecimalCodec.cs index d19754fd..48f1cc4a 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/DecimalCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/DecimalCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using EdgeDB.Utils; using System.Globalization; @@ -9,6 +10,12 @@ internal sealed class DecimalCodec { public const int NBASE = 10000; + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000108"); + + public DecimalCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override decimal Deserialize(ref PacketReader reader, CodecContext context) { var numDigits = reader.ReadUInt16(); @@ -95,5 +102,8 @@ public override void Serialize(ref PacketWriter writer, decimal value, CodecCont foreach (var digit in digits) writer.Write(digit); } + + public override string ToString() + => "std::decimal"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/EnumerationCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/EnumerationCodec.cs new file mode 100644 index 00000000..cacea1b4 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/EnumerationCodec.cs @@ -0,0 +1,41 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Codecs +{ + internal sealed class EnumerationCodec : TextCodec + { + public readonly string[] Members; + + public EnumerationCodec(in Guid id, string[] members, CodecMetadata? metadata = null) + : base(in id, metadata) + { + Members = members; + } + + public override void Serialize(ref PacketWriter writer, string? value, CodecContext context) + { + if (!Members.Contains(value)) + throw new ArgumentException("Value is not a member of this enumeration"); + + base.Serialize(ref writer, value, context); + } + + public override string Deserialize(ref PacketReader reader, CodecContext context) + { + var value = base.Deserialize(ref reader, context); + + if (!Members.Contains(value)) + throw new ArgumentException("Value is not a member of this enumeration"); + + return value; + } + + public override string ToString() + => "std::enum"; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Float32Codec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Float32Codec.cs index 3c2a1773..689b22e4 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Float32Codec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Float32Codec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class Float32Codec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000106"); + + public Float32Codec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override float Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadSingle(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, float value, CodecContex { writer.Write(value); } + + public override string ToString() + => "std::float32"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Float64Codec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Float64Codec.cs index 57d25664..eda522ba 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Float64Codec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Float64Codec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class Float64Codec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000107"); + + public Float64Codec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override double Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadDouble(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, double value, CodecConte { writer.Write(value); } + + public override string ToString() + => "std::float64"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseArgumentCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseArgumentCodec.cs index 02593044..2fb53b7f 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseArgumentCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseArgumentCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,6 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal abstract class BaseArgumentCodec : BaseCodec, IArgumentCodec { + protected BaseArgumentCodec(in Guid id, CodecMetadata? metadata) + : base(in id, metadata) + { } + public abstract void SerializeArguments(ref PacketWriter writer, T? value, CodecContext context); void IArgumentCodec.SerializeArguments(ref PacketWriter writer, object? value, CodecContext context) diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseCodec.cs index 9000f488..0dfad647 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,16 @@ namespace EdgeDB.Binary.Codecs { internal abstract class BaseCodec : ICodec { + public virtual Guid Id { get; } public virtual Type ConverterType => typeof(T); + public virtual CodecMetadata? Metadata { get; } + + + protected BaseCodec(in Guid id, CodecMetadata? metadata) + { + Metadata = metadata; + Id = id; + } public abstract void Serialize(ref PacketWriter writer, T? value, CodecContext context); public abstract T? Deserialize(ref PacketReader reader, CodecContext context); diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexCodec.cs index af6f901b..146110e2 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexCodec.cs @@ -1,3 +1,5 @@ +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -70,11 +72,12 @@ public override bool CanConvert(Type t) private readonly Type _runtimeCodecType; - public BaseComplexCodec() - : this(typeof(RuntimeCodec<>)) + public BaseComplexCodec(in Guid id, CodecMetadata? metadata) + : this(in id, metadata, typeof(RuntimeCodec<>)) { } - public BaseComplexCodec(Type runtimeCodecType) + public BaseComplexCodec(in Guid id, CodecMetadata? metadata, Type runtimeCodecType) + : base(in id, metadata) { _runtimeCodecType = runtimeCodecType; Converters = new(); @@ -95,7 +98,7 @@ private void RegisterConverter(Converter converter) collection.Add(converter); } - public void BuildRuntimeCodecs() + public void BuildRuntimeCodecs(IProtocolProvider provider) { if (Converters is null) return; @@ -107,7 +110,7 @@ public void BuildRuntimeCodecs() { var codecType = _runtimeCodecType.MakeGenericType(typeof(T), converter.Key); - var codecs = converter.Value.Select(x => CodecBuilder.CompiledCodecCache.GetOrAdd( + var codecs = converter.Value.Select(x => CodecBuilder.GetProviderCache(provider).CompiledCodecCache.GetOrAdd( HashCode.Combine(codecType, x, this), t => (ICodec)Activator.CreateInstance(codecType, this, x)! )).ToList(); @@ -121,12 +124,12 @@ public void BuildRuntimeCodecs() } } - public virtual ICodec GetCodecFor(Type type) + public virtual ICodec GetCodecFor(IProtocolProvider provider, Type type) { if (type == typeof(T)) return this; - BuildRuntimeCodecs(); + BuildRuntimeCodecs(provider); ICodec? codec; @@ -158,6 +161,7 @@ private sealed class RuntimeCodec public RuntimeCodec( BaseComplexCodec codec, Converter converter) + : base(codec.Id, codec.Metadata) { _codec = codec; _converter = converter; diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexScalarCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexScalarCodec.cs index df8bf2a6..34166983 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexScalarCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseComplexScalarCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -10,11 +11,9 @@ namespace EdgeDB.Binary.Codecs internal abstract class BaseComplexScalarCodec : BaseComplexCodec, IScalarCodec { - public BaseComplexScalarCodec() - : base(typeof(RuntimeScalarCodec<>)) - { - - } + public BaseComplexScalarCodec(in Guid id, CodecMetadata? metadata) + : base(in id, metadata, typeof(RuntimeScalarCodec<>)) + { } private sealed class RuntimeScalarCodec : BaseScalarCodec, IRuntimeCodec @@ -24,6 +23,7 @@ private sealed class RuntimeScalarCodec public RuntimeScalarCodec( BaseComplexScalarCodec codec, Converter converter) + : base (codec.Id, codec.Metadata) { _codec = codec; _converter = converter; diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseScalarCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseScalarCodec.cs index 0f7d57cd..ffb2689e 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseScalarCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseScalarCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,5 +9,9 @@ namespace EdgeDB.Binary.Codecs { internal abstract class BaseScalarCodec : BaseCodec, IScalarCodec - { } + { + protected BaseScalarCodec(in Guid id, CodecMetadata? metadata) + : base (in id, metadata) + { } + } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseTemporalCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseTemporalCodec.cs index 44b55cb8..190385b2 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseTemporalCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/BaseTemporalCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -11,6 +12,10 @@ internal abstract class BaseTemporalCodec : BaseComplexScalarCodec, ITemporalCodec where T : unmanaged { + public BaseTemporalCodec(in Guid id, CodecMetadata? metadata) + : base(in id, metadata) + { } + Type ITemporalCodec.ModelType => typeof(T); IEnumerable ITemporalCodec.SystemTypes => Converters is null ? Array.Empty() : Converters.Keys; diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/ICodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/ICodec.cs index 4454594d..65b2b353 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/ICodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/ICodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -15,6 +16,10 @@ internal interface ICodec : ICodec internal interface ICodec { + Guid Id { get; } + + CodecMetadata? Metadata { get; } + bool CanConvert(Type t); Type ConverterType { get; } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/IComplexCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/IComplexCodec.cs index 61c1166b..cdba9a1b 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/IComplexCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Impl/IComplexCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; @@ -10,9 +11,9 @@ internal interface IComplexCodec : ICodec { IEnumerable RuntimeCodecs { get; } - void BuildRuntimeCodecs(); + void BuildRuntimeCodecs(IProtocolProvider provider); - ICodec GetCodecFor(Type type); + ICodec GetCodecFor(IProtocolProvider provider, Type type); } internal interface IRuntimeCodec : ICodec diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Integer16Codec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Integer16Codec.cs index a54a547f..652dbdee 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Integer16Codec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Integer16Codec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class Integer16Codec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000103"); + + public Integer16Codec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override short Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadInt16(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, short value, CodecContex { writer.Write(value); } + + public override string ToString() + => "std::int16"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Integer32Codec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Integer32Codec.cs index 0bdf17e0..f3aa42dd 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Integer32Codec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Integer32Codec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class Integer32Codec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000104"); + + public Integer32Codec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override int Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadInt32(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, int value, CodecContext { writer.Write(value); } + + public override string ToString() + => "std::int32"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Integer64Codec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Integer64Codec.cs index 585ed2f4..99d42b94 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Integer64Codec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Integer64Codec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class Integer64Codec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000105"); + + public Integer64Codec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override long Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadInt64(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, long value, CodecContext { writer.Write(value); } + + public override string ToString() + => "std::int64"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/JsonCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/JsonCodec.cs index 06d140da..14186b3d 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/JsonCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/JsonCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System.Text; namespace EdgeDB.Binary.Codecs @@ -5,6 +6,12 @@ namespace EdgeDB.Binary.Codecs internal sealed class JsonCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-00000000010F"); + + public JsonCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override DataTypes.Json Deserialize(ref PacketReader reader, CodecContext context) { // format (unused) @@ -22,5 +29,8 @@ public override void Serialize(ref PacketWriter writer, DataTypes.Json value, Co writer.Write((byte)0x01); writer.Write(jsonData); } + + public override string ToString() + => "std::json"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/MemoryCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/MemoryCodec.cs index f2d389b6..47baf747 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/MemoryCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/MemoryCodec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class MemoryCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000130"); + + public MemoryCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + public override DataTypes.Memory Deserialize(ref PacketReader reader, CodecContext context) { return new DataTypes.Memory(reader.ReadInt64()); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, DataTypes.Memory value, { writer.Write(value.TotalBytes); } + + public override string ToString() + => "cfg::memory"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/NullCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/NullCodec.cs index 6cd4f42f..da992d71 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/NullCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/NullCodec.cs @@ -1,10 +1,22 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class NullCodec : ICodec, IArgumentCodec, ICacheableCodec { + public Guid Id + => Guid.Empty; + + public CodecMetadata? Metadata + => null; + public Type ConverterType => typeof(object); + public NullCodec() { } + + public NullCodec(CodecMetadata? metadata = null) { } // used in generic codec construction + public bool CanConvert(Type t) { return true; @@ -18,5 +30,8 @@ public void Serialize(ref PacketWriter writer, object? value, CodecContext conte } public void SerializeArguments(ref PacketWriter writer, object? value, CodecContext context) { } + + public override string ToString() + => "null_codec"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/ObjectCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/ObjectCodec.cs index 9fee68dd..34c5b2b2 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/ObjectCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/ObjectCodec.cs @@ -1,4 +1,5 @@ using EdgeDB.Binary; +using EdgeDB.Binary.Protocol.Common.Descriptors; using EdgeDB.Utils.FSharp; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; @@ -27,7 +28,7 @@ public Type TargetType private readonly ObjectCodec _codec; public TypeInitializedObjectCodec(Type target, ObjectCodec codec) - : base(codec.InnerCodecs, codec.PropertyNames) + : base(codec.Id, codec.Properties, codec.Metadata) { if (!TypeBuilder.TryGetTypeDeserializerInfo(target, out _deserializer!)) throw new NoTypeConverterException($"Failed to find type deserializer for {target}"); @@ -43,10 +44,9 @@ public TypeInitializedObjectCodec(Type target, ObjectCodec codec) // This method ensures we're not copying the packet in memory again but the downside is // our 'reader' variable isn't kept up to data with the reader in the object enumerator. var enumerator = new ObjectEnumerator( - ref reader.Data, + in reader.Data, reader.Position, - PropertyNames, - InnerCodecs, + Properties, context ); @@ -66,44 +66,40 @@ public TypeInitializedObjectCodec(Type target, ObjectCodec codec) } } + internal struct ObjectProperty + { + public readonly Cardinality Cardinality; + public readonly string Name; + public ICodec Codec; + + public ObjectProperty(Cardinality cardinality, ref ICodec codec, string name) + { + Cardinality = cardinality; + Codec = codec; + Name = name; + } + } + internal class ObjectCodec : BaseArgumentCodec, IMultiWrappingCodec, ICacheableCodec { - public ICodec[] InnerCodecs; - public readonly string[] PropertyNames; + public ICodec[] InnerCodecs + => _codecs; - private ConcurrentDictionary? _typeCodecs; + public readonly ObjectProperty[] Properties; - internal ObjectCodec(ObjectShapeDescriptor descriptor, List codecs) - { - InnerCodecs = new ICodec[descriptor.Shapes.Length]; - PropertyNames = new string[descriptor.Shapes.Length]; + private ConcurrentDictionary? _typeCodecs; - for(int i = 0; i != descriptor.Shapes.Length; i++) - { - var shape = descriptor.Shapes[i]; - InnerCodecs[i] = codecs[shape.TypePos]; - PropertyNames[i] = shape.Name; - } - } + private ICodec[] _codecs; - internal ObjectCodec(NamedTupleTypeDescriptor descriptor, List codecs) + internal ObjectCodec(in Guid id, ObjectProperty[] properties, CodecMetadata? metadata = null) + : base(in id, metadata) { - InnerCodecs = new ICodec[descriptor.Elements.Length]; - PropertyNames = new string[descriptor.Elements.Length]; - - for (int i = 0; i != descriptor.Elements.Length; i++) - { - var shape = descriptor.Elements[i]; - InnerCodecs[i] = codecs[shape.TypePos]; - PropertyNames[i] = shape.Name; - } - } + Properties = properties; - internal ObjectCodec(ICodec[] innerCodecs, string[] propertyNames) - { - InnerCodecs = innerCodecs; - PropertyNames = propertyNames; + _codecs = new ICodec[properties.Length]; + for (var i = 0; i != properties.Length; i++) + _codecs[i] = properties[i].Codec; } public TypeInitializedObjectCodec GetOrCreateTypeCodec(Type type) @@ -116,10 +112,9 @@ public TypeInitializedObjectCodec GetOrCreateTypeCodec(Type type) // This method ensures we're not copying the packet in memory again but the downside is // our 'reader' variable isn't kept up to data with the reader in the object enumerator. var enumerator = new ObjectEnumerator( - ref reader.Data, + in reader.Data, reader.Position, - PropertyNames, - InnerCodecs, + Properties, context ); @@ -146,7 +141,7 @@ public override void Serialize(ref PacketWriter writer, object? value, CodecCont object?[]? values = null; if (value is IDictionary dict) - values = PropertyNames.Select(x => dict[x]).ToArray(); + values = Properties.Select(x => dict[x.Name]).ToArray(); else if (value is object?[] arr) value = arr; @@ -185,7 +180,7 @@ public override void Serialize(ref PacketWriter writer, object? value, CodecCont } else { - var innerCodec = InnerCodecs[i]; + var innerCodec = Properties[i].Codec; // special case for enums if (element.GetType().IsEnum && innerCodec is TextCodec) @@ -203,16 +198,19 @@ public override void Serialize(ref PacketWriter writer, object? value, CodecCont } public override string ToString() - { - return $"ObjectCodec<{string.Join(", ", InnerCodecs.Zip(PropertyNames).Select(x => $"[{x.Second}: {x.First}]"))}>"; - } + => "object"; ICodec[] IMultiWrappingCodec.InnerCodecs { get => InnerCodecs; set { - InnerCodecs = value; + if (value.Length != Properties.Length) + throw new InvalidOperationException("Array length mismatch"); + + _codecs = value; + for (var i = 0; i != _codecs.Length; i++) + Properties[i].Codec = _codecs[i]; } } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/RangeCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/RangeCodec.cs index 7043a902..629cb303 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/RangeCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/RangeCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using EdgeDB.DataTypes; using System; using System.Collections.Generic; @@ -15,7 +16,9 @@ internal sealed class RangeCodec where T : struct { public ICodec _innerCodec; - public RangeCodec(ICodec innerCodec) + + public RangeCodec(in Guid id, ICodec innerCodec, CodecMetadata? metadata = null) + : base(in id, metadata) { _innerCodec = innerCodec; @@ -112,9 +115,7 @@ public override void Serialize(ref PacketWriter writer, Range value, CodecCon } public override string ToString() - { - return $"RangeCodec<{_innerCodec}>"; - } + => "range"; ICodec IWrappingCodec.InnerCodec { diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/SetCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/SetCodec.cs index f7d8050c..a30dbb1c 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/SetCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/SetCodec.cs @@ -1,3 +1,5 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class SetCodec @@ -6,7 +8,8 @@ internal sealed class SetCodec internal ICodec InnerCodec; private readonly bool _isSetOfArray; - public SetCodec(ICodec innerCodec) + public SetCodec(in Guid id, ICodec innerCodec, CodecMetadata? metadata = null) + : base(in id, metadata) { InnerCodec = innerCodec; var codecType = innerCodec.GetType(); @@ -110,9 +113,7 @@ public override void Serialize(ref PacketWriter writer, IEnumerable? value, } public override string ToString() - { - return $"SetCodec<{InnerCodec}>"; - } + => "set"; ICodec IWrappingCodec.InnerCodec { diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/SparceObjectCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/SparceObjectCodec.cs index 9ad1369b..5d31a6ac 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/SparceObjectCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/SparceObjectCodec.cs @@ -1,4 +1,5 @@ using EdgeDB.Binary; +using EdgeDB.Binary.Protocol.Common.Descriptors; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -15,17 +16,11 @@ internal sealed class SparceObjectCodec public ICodec[] InnerCodecs; public readonly string[] FieldNames; - internal SparceObjectCodec(InputShapeDescriptor descriptor, List codecs) + public SparceObjectCodec(in Guid id, ICodec[] innerCodecs, string[] fieldNames, CodecMetadata? metadata = null) + : base(in id, metadata) { - InnerCodecs = new ICodec[descriptor.Shapes.Length]; - FieldNames = new string[descriptor.Shapes.Length]; - - for (int i = 0; i != descriptor.Shapes.Length; i++) - { - var shape = descriptor.Shapes[i]; - InnerCodecs[i] = codecs[shape.TypePos]; - FieldNames[i] = shape.Name; - } + FieldNames = fieldNames; + InnerCodecs = innerCodecs; } public override object? Deserialize(ref PacketReader reader, CodecContext context) @@ -57,7 +52,7 @@ internal SparceObjectCodec(InputShapeDescriptor descriptor, List codecs) object? value; - value = InnerCodecs[i].Deserialize(context, innerData); + value = InnerCodecs[i].Deserialize(context, in innerData); dataDictionary.Add(elementName, value); } @@ -111,9 +106,7 @@ public override void Serialize(ref PacketWriter writer, object? value, CodecCont } public override string ToString() - { - return $"SparseObjectCodec<{string.Join(", ", InnerCodecs.Zip(FieldNames).Select(x => $"[{x.Second}: {x.First}]"))}>"; - } + => "sparce_object"; ICodec[] IMultiWrappingCodec.InnerCodecs { diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateDurationCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateDurationCodec.cs index ae05be9f..2460271f 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateDurationCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateDurationCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class DateDurationCodec : BaseTemporalCodec { - public DateDurationCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000112"); + + public DateDurationCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(From, To); } @@ -34,5 +38,8 @@ private DataTypes.DateDuration From(ref TimeSpan value) private TimeSpan To(ref DataTypes.DateDuration value) => value.TimeSpan; + + public override string ToString() + => "cal::date_duration"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateTimeCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateTimeCodec.cs index 0e947ea9..9cf7307e 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateTimeCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DateTimeCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class DateTimeCodec : BaseTemporalCodec { - public DateTimeCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-00000000010A"); + + public DateTimeCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(FromDT, ToDT); AddConverter(FromDTO, ToDTO); @@ -37,5 +41,8 @@ private DataTypes.DateTime FromDTO(ref DateTimeOffset value) private DateTimeOffset ToDTO(ref DataTypes.DateTime value) => value.DateTimeOffset; + + public override string ToString() + => "std::datetime"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DurationCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DurationCodec.cs index 2c5204d3..c6917925 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DurationCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/DurationCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class DurationCodec : BaseTemporalCodec { - public DurationCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-00000000010E"); + + public DurationCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(From, To); } @@ -35,5 +39,8 @@ private DataTypes.Duration From(ref TimeSpan value) private TimeSpan To(ref DataTypes.Duration value) => value.TimeSpan; + + public override string ToString() + => "std::duration"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateCodec.cs index 49102dde..0fb630f0 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class LocalDateCodec : BaseTemporalCodec { - public LocalDateCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-00000000010C"); + + public LocalDateCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(From, To); } @@ -30,5 +34,8 @@ private DataTypes.LocalDate From(ref DateOnly value) private DateOnly To(ref DataTypes.LocalDate value) => value.DateOnly; + + public override string ToString() + => "cal::local_date"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateTimeCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateTimeCodec.cs index db4a4e87..e232baa4 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateTimeCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalDateTimeCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class LocalDateTimeCodec : BaseTemporalCodec { - public LocalDateTimeCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-00000000010B"); + + public LocalDateTimeCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(FromDT, ToDT); AddConverter(FromDTO, ToDTO); @@ -37,5 +41,8 @@ private DataTypes.LocalDateTime FromDTO(ref DateTimeOffset value) private DateTimeOffset ToDTO(ref DataTypes.LocalDateTime value) => value.DateTimeOffset; + + public override string ToString() + => "cal::local_datetime"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalTimeCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalTimeCodec.cs index a8dd3bee..733e7b5d 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalTimeCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/LocalTimeCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class LocalTimeCodec : BaseTemporalCodec { - public LocalTimeCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-00000000010D"); + + public LocalTimeCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(FromTS, ToTS); AddConverter(FromTO, ToTO); @@ -37,5 +41,8 @@ private DataTypes.LocalTime FromTO(ref TimeOnly value) private TimeOnly ToTO(ref DataTypes.LocalTime value) => value.TimeOnly; + + public override string ToString() + => "cal::local_time"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/RelativeDurationCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/RelativeDurationCodec.cs index 0363cf07..caeba0e1 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/RelativeDurationCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Temporal/RelativeDurationCodec.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,10 @@ namespace EdgeDB.Binary.Codecs { internal sealed class RelativeDurationCodec : BaseTemporalCodec { - public RelativeDurationCodec() + public new static Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000111"); + + public RelativeDurationCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) { AddConverter(From, To); } @@ -34,5 +38,8 @@ private DataTypes.RelativeDuration From(ref TimeSpan value) private TimeSpan To(ref DataTypes.RelativeDuration value) => value.TimeSpan; + + public override string ToString() + => "cal::relative_duration"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/TextCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/TextCodec.cs index f7d2c589..ba72a3ea 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/TextCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/TextCodec.cs @@ -1,11 +1,22 @@ using EdgeDB.Binary; +using EdgeDB.Binary.Protocol.Common.Descriptors; using System.Text; namespace EdgeDB.Binary.Codecs { - internal sealed class TextCodec + internal class TextCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000101"); + + public TextCodec(CodecMetadata? metadata = null) + : base(in Id, metadata) + { } + + protected TextCodec(in Guid id, CodecMetadata? metadata = null) + : base(in id, metadata) + { } + public override string Deserialize(ref PacketReader reader, CodecContext context) { return reader.ConsumeString(); @@ -16,5 +27,8 @@ public override void Serialize(ref PacketWriter writer, string? value, CodecCont if (value is not null) writer.Write(Encoding.UTF8.GetBytes(value)); } + + public override string ToString() + => "std::str"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/TupleCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/TupleCodec.cs index c685077e..6993786c 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/TupleCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/TupleCodec.cs @@ -1,4 +1,5 @@ using EdgeDB.Binary; +using EdgeDB.Binary.Protocol.Common.Descriptors; using EdgeDB.DataTypes; using System.Reflection.Emit; using System.Runtime.CompilerServices; @@ -9,8 +10,9 @@ internal sealed class TupleCodec : BaseComplexCodec, IMultiWrappingCodec, ICacheableCodec { internal ICodec[] InnerCodecs; - - public TupleCodec(ICodec[] innerCodecs) + + public TupleCodec(in Guid id, ICodec[] innerCodecs, CodecMetadata? metadata = null) + : base(in id, metadata) { InnerCodecs = innerCodecs; @@ -89,11 +91,6 @@ public override void Serialize(ref PacketWriter writer, TransientTuple value, Co } } - public override string ToString() - { - return $"TupleCodec<{string.Join(", ", this.InnerCodecs.Select(x => x.ToString()))}>"; - } - public Type CreateValueTupleType() { var typeArr = InnerCodecs.Select(x => x.ConverterType).ToArray(); @@ -129,5 +126,8 @@ ICodec[] IMultiWrappingCodec.InnerCodecs InnerCodecs = value; } } + + public override string ToString() + => "tuple"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/UUIDCodec.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/UUIDCodec.cs index af105ecc..5b9e2f64 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/UUIDCodec.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/UUIDCodec.cs @@ -1,8 +1,16 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; + namespace EdgeDB.Binary.Codecs { internal sealed class UUIDCodec : BaseScalarCodec { + public new static readonly Guid Id = Guid.Parse("00000000-0000-0000-0000-000000000100"); + + public UUIDCodec(CodecMetadata? metadata = null) + : base (in Id, metadata) + { } + public override Guid Deserialize(ref PacketReader reader, CodecContext context) { return reader.ReadGuid(); @@ -12,5 +20,8 @@ public override void Serialize(ref PacketWriter writer, Guid value, CodecContext { writer.Write(value); } + + public override string ToString() + => "std::uuid"; } } diff --git a/src/EdgeDB.Net.Driver/Binary/Codecs/Visitors/TypeVisitor.cs b/src/EdgeDB.Net.Driver/Binary/Codecs/Visitors/TypeVisitor.cs index 28b6054e..216aa9df 100644 --- a/src/EdgeDB.Net.Driver/Binary/Codecs/Visitors/TypeVisitor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Codecs/Visitors/TypeVisitor.cs @@ -76,7 +76,7 @@ protected override void VisitCodec(ref ICodec codec) for (int i = 0; i != obj.InnerCodecs.Length; i++) { ref var innerCodec = ref obj.InnerCodecs[i]; - var name = obj.PropertyNames[i]; + var name = obj.Properties[i].Name; // use the defined type, if not found, use the codecs type // if the inner is compilable, use its inner type and set the real @@ -126,7 +126,7 @@ protected override void VisitCodec(ref ICodec codec) tuple.InnerCodecs[i] = innerCodec; } - codec = tuple.GetCodecFor(GetContextualTypeForComplexCodec(tuple)); + codec = tuple.GetCodecFor(_client.ProtocolProvider, GetContextualTypeForComplexCodec(tuple)); _logger.CodecVisitorComplexCodecFlattened(Depth, tuple, codec, Context.Type); } break; @@ -143,7 +143,7 @@ protected override void VisitCodec(ref ICodec codec) { VisitCodec(ref tmp); - codec = compilable.Compile(Context.Type, tmp); + codec = compilable.Compile(_client.ProtocolProvider, Context.Type, tmp); _logger.CodecVisitorCompiledCodec(Depth, compilable, codec, Context.Type); } @@ -156,7 +156,7 @@ protected override void VisitCodec(ref ICodec codec) break; case IComplexCodec complex: { - codec = complex.GetCodecFor(GetContextualTypeForComplexCodec(complex)); + codec = complex.GetCodecFor(_client.ProtocolProvider, GetContextualTypeForComplexCodec(complex)); _logger.CodecVisitorComplexCodecFlattened(Depth, complex, codec, Context.Type); } break; @@ -169,7 +169,7 @@ protected override void VisitCodec(ref ICodec codec) // ask the broker of the runtime codec for // the correct one. - codec = runtime.Broker.GetCodecFor(GetContextualTypeForComplexCodec(runtime.Broker)); + codec = runtime.Broker.GetCodecFor(_client.ProtocolProvider, GetContextualTypeForComplexCodec(runtime.Broker)); _logger.CodecVisitorRuntimeCodecBroker(Depth, runtime, runtime.Broker, codec, Context.Type); } diff --git a/src/EdgeDB.Net.Driver/Binary/Common/KeyValue.cs b/src/EdgeDB.Net.Driver/Binary/Common/KeyValue.cs index e0732148..5bb2c853 100644 --- a/src/EdgeDB.Net.Driver/Binary/Common/KeyValue.cs +++ b/src/EdgeDB.Net.Driver/Binary/Common/KeyValue.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/EdgeDB.Net.Driver/Binary/Common/MessageSeverity.cs b/src/EdgeDB.Net.Driver/Binary/Common/MessageSeverity.cs index b2c5425b..56e484fa 100644 --- a/src/EdgeDB.Net.Driver/Binary/Common/MessageSeverity.cs +++ b/src/EdgeDB.Net.Driver/Binary/Common/MessageSeverity.cs @@ -7,7 +7,7 @@ namespace EdgeDB { /// - /// Represents the log message severity within a + /// Represents the log message severity /// internal enum MessageSeverity : byte { diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/ITypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Descriptors/ITypeDescriptor.cs deleted file mode 100644 index 2f3460ac..00000000 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/ITypeDescriptor.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.Binary -{ - internal interface ITypeDescriptor - { - Guid Id { get; } - - public static ITypeDescriptor GetDescriptor(ref PacketReader reader) - { - var type = (DescriptorType)reader.ReadByte(); - var id = reader.ReadGuid(); - - ITypeDescriptor? descriptor = type switch - { - DescriptorType.ArrayTypeDescriptor => new ArrayTypeDescriptor(id, ref reader), - DescriptorType.BaseScalarTypeDescriptor => new BaseScalarTypeDescriptor(id), - DescriptorType.EnumerationTypeDescriptor => new EnumerationTypeDescriptor(id, ref reader), - DescriptorType.NamedTupleDescriptor => new NamedTupleTypeDescriptor(id, ref reader), - DescriptorType.ObjectShapeDescriptor => new ObjectShapeDescriptor(id, ref reader), - DescriptorType.ScalarTypeDescriptor => new ScalarTypeDescriptor(id, ref reader), - DescriptorType.ScalarTypeNameAnnotation => new ScalarTypeNameAnnotation(id, ref reader), - DescriptorType.SetDescriptor => new SetTypeDescriptor(id, ref reader), - DescriptorType.TupleTypeDescriptor => new TupleTypeDescriptor(id, ref reader), - DescriptorType.InputShapeDescriptor => new InputShapeDescriptor(id, ref reader), - DescriptorType.RangeTypeDescriptor => new RangeTypeDescriptor(id, ref reader), - _ => null - }; - - if(descriptor is null) - { - var rawType = (byte)type; - - if (rawType >= 0x80 && rawType <= 0xfe) - { - descriptor = new TypeAnnotationDescriptor(type, id, reader); - } - else - throw new InvalidDataException($"No descriptor found for type {type}"); - } - - return descriptor; - } - } -} diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/InputShapeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Descriptors/InputShapeDescriptor.cs deleted file mode 100644 index 4d951c7d..00000000 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/InputShapeDescriptor.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.Binary -{ - internal readonly struct InputShapeDescriptor : ITypeDescriptor - { - public readonly Guid Id; - - public readonly ShapeElement[] Shapes; - - public InputShapeDescriptor(Guid id, ref PacketReader reader) - { - Id = id; - - var elementCount = reader.ReadUInt16(); - - ShapeElement[] shapes = new ShapeElement[elementCount]; - for (int i = 0; i != elementCount; i++) - { - var flags = (ShapeElementFlags)reader.ReadUInt32(); - var cardinality = (Cardinality)reader.ReadByte(); - var name = reader.ReadString(); - var typePos = reader.ReadUInt16(); - - shapes[i] = new ShapeElement - { - Flags = flags, - Cardinality = cardinality, - Name = name, - TypePos = typePos - }; - } - - Shapes = shapes; - } - - Guid ITypeDescriptor.Id => Id; - } -} diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/ObjectShapeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Descriptors/ObjectShapeDescriptor.cs deleted file mode 100644 index f963a2f6..00000000 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/ObjectShapeDescriptor.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.Binary -{ - internal readonly struct ObjectShapeDescriptor : ITypeDescriptor - { - public readonly Guid Id; - - public readonly ShapeElement[] Shapes; - - public ObjectShapeDescriptor(Guid id, ref PacketReader reader) - { - Id = id; - - var elementCount = reader.ReadUInt16(); - - ShapeElement[] shapes = new ShapeElement[elementCount]; - for (int i = 0; i != elementCount; i++) - { - var flags = (ShapeElementFlags)reader.ReadUInt32(); - var cardinality = (Cardinality)reader.ReadByte(); - var name = reader.ReadString(); - var typePos = reader.ReadUInt16(); - - shapes[i] = new ShapeElement - { - Flags = flags, - Cardinality = cardinality, - Name = name, - TypePos = typePos - }; - } - - Shapes = shapes; - } - - Guid ITypeDescriptor.Id => Id; - } - - internal readonly struct ShapeElement - { - public readonly ShapeElementFlags Flags { get; init; } - public readonly Cardinality Cardinality { get; init; } - public readonly string Name { get; init; } - public readonly ushort TypePos { get; init; } - } - - internal enum ShapeElementFlags : uint - { - Implicit = 1 << 0, - LinkProperty = 1 << 1, - Link = 1 << 2 - } -} diff --git a/src/EdgeDB.Net.Driver/Binary/Dumps/DumpReader.cs b/src/EdgeDB.Net.Driver/Binary/Dumps/DumpReader.cs deleted file mode 100644 index 81794511..00000000 --- a/src/EdgeDB.Net.Driver/Binary/Dumps/DumpReader.cs +++ /dev/null @@ -1,104 +0,0 @@ -using EdgeDB.Binary; -using EdgeDB.Binary.Packets; -using EdgeDB.Utils; -using System.Security.Cryptography; - -namespace EdgeDB.Dumps -{ - internal sealed class DumpReader - { - public static (Restore Restore, IEnumerable Blocks) ReadDatabaseDump(Stream stream) - { - if (!stream.CanRead) - throw new ArgumentException($"Cannot read from {nameof(stream)}"); - - // read file format - var formatLength = DumpWriter.FileFormatLength; - var format = new byte[formatLength]; - ThrowIfEndOfStream(stream.Read(format) == formatLength); - - if (!format.SequenceEqual(DumpWriter.FileFormat)) - throw new FormatException("Format of stream does not match the edgedb dump format"); - - Restore? restore = null; - List blocks = new(); - - Span buff; - using(var ms = new MemoryStream()) - { - stream.CopyTo(ms); - - if (ms.TryGetBuffer(out var arr)) - buff = arr; - else - throw new EdgeDBException("Failed to get buffer from the stream"); - } - - var reader = new PacketReader(ref buff); - - var version = reader.ReadInt64(); - - if (version > DumpWriter.DumpVersion) - throw new ArgumentException($"Unsupported dump version {version}"); - - var header = ReadPacket(ref reader); - - if (header is not DumpHeader dumpHeader) - throw new FormatException($"Expected dump header but got {header?.Type}"); - - restore = new Restore() - { - HeaderData = dumpHeader.Raw - }; - - while (stream.Position < stream.Length) - { - var packet = ReadPacket(ref reader); - - if (packet is not DumpBlock dumpBlock) - throw new FormatException($"Expected dump block but got {header?.Type}"); - - blocks.Add(new RestoreBlock - { - BlockData = dumpBlock.Raw - }); - } - - return (restore!, blocks); - } - - private static IReceiveable ReadPacket(ref PacketReader reader) - { - var type = reader.ReadChar(); - - // read hash - reader.ReadBytes(20, out var hash); - - var length = (int)reader.ReadUInt32(); - - reader.ReadBytes(length, out var packetData); - - // check hash - using (var alg = SHA1.Create()) - { - if (!alg.ComputeHash(packetData.ToArray()).SequenceEqual(hash.ToArray())) - throw new ArgumentException("Hash did not match"); - } - - var innerReader = new PacketReader(ref packetData); - - return type switch - { - 'H' => new DumpHeader(ref innerReader, length), - 'D' => new DumpBlock(ref innerReader, length), - _ => throw new ArgumentException($"Unknown packet format {type}"), - }; - } - - private static void ThrowIfEndOfStream(bool readSuccess) - { - if (!readSuccess) - throw new EndOfStreamException(); - } - } -} diff --git a/src/EdgeDB.Net.Driver/Binary/Dumps/DumpWriter.cs b/src/EdgeDB.Net.Driver/Binary/Dumps/DumpWriter.cs deleted file mode 100644 index 973edc90..00000000 --- a/src/EdgeDB.Net.Driver/Binary/Dumps/DumpWriter.cs +++ /dev/null @@ -1,73 +0,0 @@ -using EdgeDB.Binary; -using EdgeDB.Binary.Packets; - -namespace EdgeDB.Dumps -{ - internal ref struct DumpWriter - { - public const long DumpVersion = 1; - public const int FileFormatLength = 17; - - public static readonly byte[] FileFormat = new byte[] - { - 0xff, 0xd8, 0x00,0x00, 0xd8, - - // file format "EDGEDB" - 69, 68, 71, 69, 68, 66, - - 0x00, - - // file format "DUMP - 68, 85, 77, 80, - - 0x00, - }; - - private static byte[] Version - => BitConverter.GetBytes((long)1).Reverse().ToArray(); - - private readonly PacketWriter _writer; - public DumpWriter() - { - _writer = new PacketWriter(); - - _writer.Write(FileFormat); - _writer.Write(Version); - } - - public void WriteDumpHeader(DumpHeader header) - { - _writer.Write('H'); - _writer.Write(header.Hash.ToArray()); - _writer.Write(header.Length); - _writer.Write(header.Raw); - } - - public void WriteDumpBlocks(List blocks) - { - for(int i = 0; i != blocks.Count; i++) - { - var block = blocks[i]; - - _writer.Write('D'); - _writer.Write(block.HashBuffer); - _writer.Write(block.Length); - _writer.Write(block.Raw); - } - } - - public ReadOnlyMemory Collect() - { - var bytes = _writer.GetBytes(); - - // copy the buffer - Memory result = new byte[bytes.Length]; - - bytes.CopyTo(result); - - _writer.Dispose(); - - return result; - } - } -} diff --git a/src/EdgeDB.Net.Driver/Binary/Duplexers/HttpDuplexer.cs b/src/EdgeDB.Net.Driver/Binary/Duplexers/HttpDuplexer.cs index 8abd9bcd..bd89afb9 100644 --- a/src/EdgeDB.Net.Driver/Binary/Duplexers/HttpDuplexer.cs +++ b/src/EdgeDB.Net.Driver/Binary/Duplexers/HttpDuplexer.cs @@ -1,3 +1,5 @@ +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.Common; using EdgeDB.Utils; using System; using System.Buffers; @@ -5,13 +7,14 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace EdgeDB.Binary.Duplexers { internal sealed class HttpDuplexer : IBinaryDuplexer { - public const string HTTP_BINARY_CONTENT_TYPE = "application/x.edgedb.v_1_0.binary"; + private static readonly Regex _contentTypeRegex = new(@"application\/x\.edgedb\.v_(\d+)_(\d+)\.binary"); public bool IsConnected => _client.IsConnected; @@ -19,6 +22,12 @@ public bool IsConnected public CancellationToken DisconnectToken => default; + public IProtocolProvider ProtocolProvider + => _client.ProtocolProvider; + + public string ContentTypeHeader + => $"application/x.edgedb.v_{ProtocolProvider.Version.Major}_{ProtocolProvider.Version.Minor}.binary"; + private readonly EdgeDBHttpClient _client; private readonly Queue _packetQueue; private readonly SemaphoreSlim _sendSemaphore; @@ -89,6 +98,22 @@ public async IAsyncEnumerable DuplexAsync([EnumeratorCancellation] } } + private Task SendInternalAsync(CancellationToken token = default, params Sendable[] packets) + { + var data = BinaryUtils.BuildPackets(packets); + + var message = new HttpRequestMessage(HttpMethod.Post, _client.Connection.GetExecUri()) + { + Content = new ReadOnlyMemoryContent(data) + }; + + message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _client.AuthorizationToken); + message.Content.Headers.ContentType = new(ContentTypeHeader); + message.Headers.TryAddWithoutValidation("X-EdgeDB-User", _client.Connection.Username); + + return _client.HttpClient.SendAsync(message, token); + } + public async ValueTask SendAsync(CancellationToken token = default, params Sendable[] packets) { if (!IsConnected) @@ -98,18 +123,7 @@ public async ValueTask SendAsync(CancellationToken token = default, params Senda try { - var data = BinaryUtils.BuildPackets(packets); - - var message = new HttpRequestMessage(HttpMethod.Post, _client.Connection.GetExecUri()) - { - Content = new ReadOnlyMemoryContent(data) - }; - - message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _client.AuthorizationToken); - message.Content.Headers.ContentType = new(HTTP_BINARY_CONTENT_TYPE); - message.Headers.TryAddWithoutValidation("X-EdgeDB-User", _client.Connection.Username); - - var result = await _client.HttpClient.SendAsync(message, token).ConfigureAwait(false); + var result = await SendInternalAsync(token, packets).ConfigureAwait(false); // only perform second iteration if debug log enabled. if (_client.Logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) @@ -120,10 +134,40 @@ public async ValueTask SendAsync(CancellationToken token = default, params Senda } } + var attempts = 0; + + process_result: + result.EnsureSuccessStatusCode(); - if (result.Content.Headers.ContentType is null || result.Content.Headers.ContentType.MediaType != HTTP_BINARY_CONTENT_TYPE) - throw new EdgeDBException($"HTTP response content type is not {HTTP_BINARY_CONTENT_TYPE}"); + if (result.Content.Headers.ContentType?.MediaType is null) + throw new EdgeDBException($"HTTP response content type is not present"); + + if (result.Content.Headers.ContentType.MediaType != ContentTypeHeader) + { + var match = _contentTypeRegex.Match(result.Content.Headers.ContentType.MediaType); + + if(match.Success) + { + var major = ushort.Parse(match.Groups[1].Value); + var minor = ushort.Parse(match.Groups[2].Value); + + if (!_client.TryNegotiateProtocol(in major, in minor)) + throw new EdgeDBException($"The protocol requirements of the server cannot be met, theirs: {major}.{minor}, ours: {ProtocolProvider.Version}"); + + // resend with new provider + attempts++; + + if (attempts > _client.ClientConfig.MaxConnectionRetries) + throw new EdgeDBException($"Failed to negotiate with server after {attempts} attempts"); + + result = await SendInternalAsync(token, packets); + goto process_result; + } + else + throw new EdgeDBException($"HTTP response content type is unknown/unsupported: {result.Content.Headers.ContentType.MediaType}"); + } + var stream = await result.Content.ReadAsStreamAsync().ConfigureAwait(false); var length = result.Content.Headers.ContentLength; @@ -149,11 +193,11 @@ private async Task ReadPacketsAsync(Stream stream, long length, CancellationToke { totalRead += await stream.ReadAsync(headerBuffer, token).ConfigureAwait(false); - StreamDuplexer.PacketHeader header; + PacketHeader header; unsafe { - header = *(StreamDuplexer.PacketHeader*)pin.Pointer; + header = *(PacketHeader*)pin.Pointer; } header.CorrectLength(); @@ -163,7 +207,17 @@ private async Task ReadPacketsAsync(Stream stream, long length, CancellationToke totalRead += await stream.ReadAsync(buffer, token).ConfigureAwait(false); - var packet = PacketSerializer.DeserializePacket(header.Type, ref buffer, header.Length, _client); + var packetFactory = ProtocolProvider.GetPacketFactory(header.Type); + + if (packetFactory is null) + { + // unknow/unsupported packet + _client.Logger.UnknownPacket($"{header.Type}{{0x{(byte)header.Type}}}:{header.Length}"); + + throw new UnexpectedMessageException($"Unknown/unsupported message type {header.Type}"); + } + + var packet = PacketSerializer.DeserializePacket(in packetFactory, in buffer); if (packet is null) throw new EdgeDBException($"Failed to deserialize packet type {header.Type}"); diff --git a/src/EdgeDB.Net.Driver/Binary/Duplexers/IBinaryDuplexer.cs b/src/EdgeDB.Net.Driver/Binary/Duplexers/IBinaryDuplexer.cs index 023a71cc..2d2030c8 100644 --- a/src/EdgeDB.Net.Driver/Binary/Duplexers/IBinaryDuplexer.cs +++ b/src/EdgeDB.Net.Driver/Binary/Duplexers/IBinaryDuplexer.cs @@ -1,4 +1,4 @@ -using EdgeDB.Binary.Packets; +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +9,7 @@ namespace EdgeDB.Binary { internal interface IBinaryDuplexer : IDisposable { + IProtocolProvider ProtocolProvider { get; } bool IsConnected { get; } CancellationToken DisconnectToken { get; } @@ -32,7 +33,7 @@ ValueTask SendAsync(Sendable packet, CancellationToken token = default) async Task DuplexAndSyncSingleAsync(Sendable packet, CancellationToken token = default) { - await SendAsync(token, packet, new Sync()).ConfigureAwait(false); + await SendAsync(token, packet, ProtocolProvider.Sync()).ConfigureAwait(false); return await ReadNextAsync(token).ConfigureAwait(false); } @@ -40,7 +41,7 @@ IAsyncEnumerable DuplexAsync(Sendable packet, CancellationToken to => DuplexAsync(token, packet); IAsyncEnumerable DuplexAndSyncAsync(Sendable packet, CancellationToken token = default) - => DuplexAsync(token, packet, new Sync()); + => DuplexAsync(token, packet, ProtocolProvider.Sync()); } internal readonly struct DuplexResult diff --git a/src/EdgeDB.Net.Driver/Binary/Duplexers/StreamDuplexer.cs b/src/EdgeDB.Net.Driver/Binary/Duplexers/StreamDuplexer.cs index e4446a1c..1fdb48c8 100644 --- a/src/EdgeDB.Net.Driver/Binary/Duplexers/StreamDuplexer.cs +++ b/src/EdgeDB.Net.Driver/Binary/Duplexers/StreamDuplexer.cs @@ -1,5 +1,5 @@ -using EdgeDB.Binary; -using EdgeDB.Binary.Packets; +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.Common; using EdgeDB.Utils; using System.Buffers; using System.Buffers.Binary; @@ -29,6 +29,9 @@ public event Func OnDisconnected public CancellationToken DisconnectToken => _disconnectTokenSource.Token; + public IProtocolProvider ProtocolProvider + => _client.ProtocolProvider; + private readonly EdgeDBBinaryClient _client; private readonly AsyncEvent> _onDisconnected = new(); private readonly AsyncEvent> _onMessage = new(); @@ -45,23 +48,6 @@ public CancellationToken DisconnectToken private Stream? _stream; private CancellationTokenSource _disconnectTokenSource; - [StructLayout(LayoutKind.Explicit, Pack = 0, Size = 5)] - internal struct PacketHeader - { - [FieldOffset(0)] - public readonly ServerMessageType Type; - - [FieldOffset(1)] - public int Length; - - public void CorrectLength() - { - BinaryUtils.CorrectEndianness(ref Length); - // remove the length of "Length" from the length of the packet - Length -= 4; - } - } - public unsafe StreamDuplexer(EdgeDBBinaryClient client) { _client = client; @@ -90,7 +76,7 @@ public async ValueTask DisconnectAsync(CancellationToken token = default) try { if (IsConnected) - await SendAsync(token, packets: new Terminate()).ConfigureAwait(false); + await SendAsync(token, packets: _client.ProtocolProvider.Terminate()).ConfigureAwait(false); } catch (EdgeDBException) { } // assume its because the connection is closed. @@ -135,10 +121,21 @@ public async ValueTask DisconnectAsync(CancellationToken token = default) _client.Logger.MessageReceived(_client.ClientId, header.Type, buffer.Length); - var packet = PacketSerializer.DeserializePacket(header.Type, ref buffer, header.Length, _client); + var packetFactory = _client.ProtocolProvider.GetPacketFactory(header.Type); + + if(packetFactory is null) + { + // unknow/unsupported packet + _client.Logger.UnknownPacket($"{header.Type}{{0x{(byte)header.Type}}}:{header.Length}"); + + await DisconnectInternalAsync(); + return null; + } + + var packet = PacketSerializer.DeserializePacket(in packetFactory, in buffer); // check for idle timeout - if(packet is ErrorResponse err && err.ErrorCode == ServerErrorCodes.IdleSessionTimeoutError) + if(packet is IProtocolError err && err.ErrorCode == ServerErrorCodes.IdleSessionTimeoutError) { // all connection state needs to be reset for the client here. _client.Logger.IdleDisconnect(); diff --git a/src/EdgeDB.Net.Driver/Binary/PacketReader.cs b/src/EdgeDB.Net.Driver/Binary/PacketReader.cs index b4e243c0..7953cd65 100644 --- a/src/EdgeDB.Net.Driver/Binary/PacketReader.cs +++ b/src/EdgeDB.Net.Driver/Binary/PacketReader.cs @@ -1,4 +1,3 @@ -using EdgeDB.Binary.Packets; using EdgeDB.Utils; using System.Buffers.Binary; using System.Numerics; @@ -32,20 +31,20 @@ internal int Limit } } - internal Span Data; + internal ReadOnlySpan Data; internal int Position; private int _limit; - public PacketReader(Span bytes, int position = 0) + public PacketReader(ReadOnlySpan bytes, int position = 0) { Data = bytes; Position = position; _limit = Data.Length; } - public PacketReader(ref Span bytes, int position = 0) + public PacketReader(scoped in ReadOnlySpan bytes, int position = 0) { Data = bytes; Position = position; @@ -59,18 +58,22 @@ private void VerifyInLimits(int sz) } #region Unmanaged basic reads & endianness correction - private T UnsafeReadAs() + private ref T UnsafeReadAs() where T : unmanaged { VerifyInLimits(sizeof(T)); - var ret = Unsafe.Read(Unsafe.AsPointer(ref Data[Position])); + ref var ret = ref Unsafe.As(ref Unsafe.AsRef(in Data[Position])); BinaryUtils.CorrectEndianness(ref ret); Position += sizeof(T); - return ret; + return ref ret; } - + + public ref T ReadStruct() + where T : unmanaged + => ref UnsafeReadAs(); + public bool ReadBoolean() => ReadByte() > 0; @@ -206,16 +209,11 @@ public byte[] ReadByteArray() return buffer.ToArray(); } - public void ReadBytes(int length, out Span buff) + public void ReadBytes(int length, out ReadOnlySpan buff) { VerifyInLimits(length); buff = Data[Position..(Position + length)]; Position += length; } - - public void Dispose() - { - Data.Clear(); - } } } diff --git a/src/EdgeDB.Net.Driver/Binary/PacketSerializer.cs b/src/EdgeDB.Net.Driver/Binary/PacketSerializer.cs index f7180aeb..4a465db6 100644 --- a/src/EdgeDB.Net.Driver/Binary/PacketSerializer.cs +++ b/src/EdgeDB.Net.Driver/Binary/PacketSerializer.cs @@ -1,8 +1,8 @@ -using EdgeDB.Binary.Packets; using EdgeDB.Binary.Codecs; using System.Collections.Concurrent; using System.Numerics; using System.Reflection; +using EdgeDB.Binary.Protocol; namespace EdgeDB.Binary { @@ -22,62 +22,10 @@ internal sealed class PacketSerializer return val.Key; } - public static IReceiveable? DeserializePacket(ServerMessageType type, ref Memory buffer, int length, EdgeDBBinaryClient client) + public static IReceiveable DeserializePacket(in PacketReadFactory factory, in Memory buffer) { var reader = new PacketReader(buffer.Span); - - try - { - switch (type) - { - case ServerMessageType.Authentication: - return new AuthenticationStatus(ref reader); - case ServerMessageType.CommandComplete: - return new CommandComplete(ref reader); - case ServerMessageType.CommandDataDescription: - return new CommandDataDescription(ref reader); - case ServerMessageType.Data: - return new Data(ref reader); - case ServerMessageType.DumpBlock: - return new DumpBlock(ref reader, in length); - case ServerMessageType.DumpHeader: - return new DumpHeader(ref reader, in length); - case ServerMessageType.ErrorResponse: - return new ErrorResponse(ref reader); - case ServerMessageType.LogMessage: - return new LogMessage(ref reader); - case ServerMessageType.ParameterStatus: - return new ParameterStatus(ref reader); - case ServerMessageType.ReadyForCommand: - return new ReadyForCommand(ref reader); - case ServerMessageType.RestoreReady: - return new RestoreReady(ref reader); - case ServerMessageType.ServerHandshake: - return new ServerHandshake(ref reader); - case ServerMessageType.ServerKeyData: - return new ServerKeyData(ref reader); - case ServerMessageType.StateDataDescription: - return new StateDataDescription(ref reader); - default: - // skip the packet length - reader.Skip(length); - - client.Logger.UnknownPacket(type.ToString("X")); - return null; - } - } - finally - { - // ensure that we read the entire packet - if (!reader.Empty) - { - // log a warning - client.Logger.DidntReadTillEnd(type, length); - } - - reader.Dispose(); - } - + return factory(ref reader, buffer.Length); } private static readonly Dictionary _scalarTypeMap = new() diff --git a/src/EdgeDB.Net.Driver/Binary/PacketWriter.cs b/src/EdgeDB.Net.Driver/Binary/PacketWriter.cs index 9c808d36..652411cd 100644 --- a/src/EdgeDB.Net.Driver/Binary/PacketWriter.cs +++ b/src/EdgeDB.Net.Driver/Binary/PacketWriter.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -15,10 +16,10 @@ namespace EdgeDB.Binary { internal unsafe ref struct PacketWriter { - public long Index + public readonly long Index => _trackedPointer - _basePointer; - public bool CanWrite + public readonly bool CanWrite => _canWrite; internal int Size; @@ -76,6 +77,10 @@ public ReadOnlyMemory GetBytes() Free(); return buffer; } + + public ReadOnlyMemory GetRetainedBytes() + => _rawBuffer[..(_isDynamic ? (int)Index : Size)]; + #endregion private void ThrowIfCantWrite() @@ -272,6 +277,12 @@ public void Write(string value) } } + public void Reset() + { + _rawBuffer.Span.Clear(); + _trackedPointer = _basePointer; + } + /// /// Unpins the raw memory, allowing the buffer to /// be managed by the GC. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerMessageType.cs b/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerMessageType.cs deleted file mode 100644 index 09cb5a39..00000000 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerMessageType.cs +++ /dev/null @@ -1,80 +0,0 @@ - -namespace EdgeDB.Binary -{ - /// - /// Represents all supported message types sent by the server. - /// - internal enum ServerMessageType : sbyte - { - /// - /// A message. - /// - RestoreReady = 0x2b, - - /// - /// A message. - /// - DumpBlock = 0x3d, - - /// - /// A message. - /// - DumpHeader = 0x40, - - /// - /// A message. - /// - CommandComplete = 0x43, - - /// - /// A message. - /// - Data = 0x44, - - /// - /// A message. - /// - ErrorResponse = 0x45, - - /// - /// A message. - /// - ServerKeyData = 0x4b, - - /// - /// A message. - /// - LogMessage = 0x4c, - - /// - /// A message. - /// - Authentication = 0x52, - - /// - /// A message. - /// - CommandDataDescription = 0x54, - - /// - /// A message. - /// - ParameterStatus = 0x53, - - /// - /// A message. - /// - ReadyForCommand = 0x5a, - - /// - /// A message. - /// - StateDataDescription = 0x73, - - /// - /// A message. - /// - ServerHandshake = 0x76, - - } -} diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/ClientMessageTypes.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ClientMessageTypes.cs similarity index 100% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/ClientMessageTypes.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/ClientMessageTypes.cs diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/CodecMetadata.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/CodecMetadata.cs new file mode 100644 index 00000000..91265dce --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/CodecMetadata.cs @@ -0,0 +1,36 @@ +using EdgeDB.Binary.Codecs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.Common.Descriptors +{ + internal sealed class CodecMetadata + { + public string? SchemaName { get; } + + public bool IsSchemaDefined { get; } + + public CodecAncestor[] Ancestors { get; } + + public CodecMetadata(string? schemaName, bool isSchemaDefined, CodecAncestor[]? ancestors = null) + { + SchemaName = schemaName; + IsSchemaDefined = isSchemaDefined; + Ancestors = ancestors ?? Array.Empty(); + } + } + internal struct CodecAncestor + { + public ICodec? Codec; + public ITypeDescriptor Descriptor; + + public CodecAncestor(ref ICodec? codec, ref ITypeDescriptor descriptor) + { + Codec = codec; + Descriptor = descriptor; + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/ShapeElementFlags.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/ShapeElementFlags.cs new file mode 100644 index 00000000..0b174af1 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/ShapeElementFlags.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.Common.Descriptors +{ + internal enum ShapeElementFlags : uint + { + Implicit = 1 << 0, + LinkProperty = 1 << 1, + Link = 1 << 2 + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/TupleElement.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/TupleElement.cs new file mode 100644 index 00000000..16ef704f --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/TupleElement.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.Common.Descriptors +{ + internal readonly struct TupleElement + { + public readonly string Name; + + public readonly short TypePos; + + public TupleElement(scoped ref PacketReader reader) + { + Name = reader.ReadString(); + TypePos = reader.ReadInt16(); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/TypeOperation.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/TypeOperation.cs new file mode 100644 index 00000000..2c63e323 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/Descriptors/TypeOperation.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.Common.Descriptors +{ + internal enum TypeOperation : byte + { + // Foo | Bar + Union = 1, + + // Foo & Bar + Intersection = 2 + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/Common/IProtocolError.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/IProtocolError.cs new file mode 100644 index 00000000..ab2b0420 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/IProtocolError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.Common +{ + internal interface IProtocolError + { + ErrorSeverity Severity { get; } + ServerErrorCodes ErrorCode { get; } + string Message { get; } + + bool TryGetAttribute(in ushort code, out KeyValue kv); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/Common/PacketHeader.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/PacketHeader.cs new file mode 100644 index 00000000..3b45884a --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Common/PacketHeader.cs @@ -0,0 +1,28 @@ +using EdgeDB.Utils; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.Common +{ + [StructLayout(LayoutKind.Explicit, Pack = 0, Size = 5)] + internal struct PacketHeader + { + [FieldOffset(0)] + public readonly ServerMessageType Type; + + [FieldOffset(1)] + public int Length; + + public void CorrectLength() + { + BinaryUtils.CorrectEndianness(ref Length); + // remove the length of "Length" from the length of the packet + Length -= 4; + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/IDumpRestoreProvider.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/IDumpRestoreProvider.cs new file mode 100644 index 00000000..fbfa332f --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/IDumpRestoreProvider.cs @@ -0,0 +1,56 @@ +using EdgeDB.Binary.Protocol.DumpRestore.V1._0; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.DumpRestore +{ + internal interface IDumpRestoreProvider + { + private static IDumpRestoreProvider? _defaultProvider; + private static readonly ConcurrentDictionary _providers = new(); + public static IDumpRestoreProvider GetDefaultProvider() + => _defaultProvider ??= Providers[ProtocolVersion.DumpRestoreDefaultVersion].Factory(); + + public static IDumpRestoreProvider GetProvider(EdgeDBBinaryClient client) + => _providers.GetOrAdd(client.Connection, _ => GetDefaultProvider()); + + public static void UpdateProviderFor(EdgeDBBinaryClient client, IDumpRestoreProvider provider, IDumpRestoreProvider old) + => _providers.TryUpdate(client.Connection, provider, old); + + public static readonly Dictionary Factory)> Providers = new() + { + { (1, 0), (typeof(V1DumpRestoreProvider), () => new V1DumpRestoreProvider()) } + }; + + public static IDumpRestoreProvider GetProvider(EdgeDBBinaryClient client, ProtocolVersion? requestedVersion) + { + if (requestedVersion is null) + return GetProvider(client); + + var dumprestoreProvider = GetProvider(client); + + if (dumprestoreProvider.DumpRestoreVersion != requestedVersion) + { + if (!Providers.TryGetValue(requestedVersion, out var provider)) + throw new ArgumentException($"Unsupported dump/restore version {requestedVersion}"); + + var newProvider = provider.Factory(); + + UpdateProviderFor(client, newProvider, dumprestoreProvider); + + dumprestoreProvider = newProvider; + } + + return dumprestoreProvider; + } + + ProtocolVersion DumpRestoreVersion { get; } + + Task RestoreDatabaseAsync(EdgeDBBinaryClient client, Stream stream, CancellationToken token); + Task DumpDatabaseAsync(EdgeDBBinaryClient client, Stream stream, CancellationToken token = default); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/DumpSectionHeader.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/DumpSectionHeader.cs new file mode 100644 index 00000000..2b569663 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/DumpSectionHeader.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.DumpRestore.V1._0 +{ + // 0x00 -> 0x02 Type + // 0x02 -> 0x16 Hash + // 0x16 -> 0x20 Length + [StructLayout(LayoutKind.Explicit, Size = 26)] + internal struct DumpSectionHeader + { + [FieldOffset(0)] + public char Type; + + [FieldOffset(22)] + public int Length; + + public readonly unsafe ReadOnlySpan Hash + => new((byte*)Unsafe.AsPointer(ref Unsafe.AsRef(in this)) + 1, 20); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/DumpState.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/DumpState.cs new file mode 100644 index 00000000..ee21310d --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/DumpState.cs @@ -0,0 +1,39 @@ +using EdgeDB.Binary.Protocol.V1._0.Packets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.DumpRestore.V1._0 +{ + internal sealed class DumpState + { + private readonly Stream _stream; + + public DumpState(Stream stream) + { + _stream = stream; + } + + public ValueTask WriteHeaderAsync(in DumpHeader header) + { + var writer = new PacketWriter(); + writer.Write('H'); + writer.Write(header.Hash.ToArray()); + writer.Write(header.Length); + writer.Write(header.Raw); + return _stream.WriteAsync(writer.GetBytes()); + } + + public ValueTask WriteBlockAsync(in DumpBlock block) + { + var writer = new PacketWriter(); + writer.Write('D'); + writer.Write(block.HashBuffer); + writer.Write(block.Length); + writer.Write(block.Raw); + return _stream.WriteAsync(writer.GetBytes()); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/V1DumpRestoreProvider.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/V1DumpRestoreProvider.cs new file mode 100644 index 00000000..f7f7005d --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/DumpRestore/V1.0/V1DumpRestoreProvider.cs @@ -0,0 +1,211 @@ +using EdgeDB.Binary.Protocol.V1._0.Packets; +using EdgeDB.Utils; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics.Arm; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.DumpRestore.V1._0 +{ + internal class V1DumpRestoreProvider : IDumpRestoreProvider + { + public const int DUMP_FILE_FORMAT_LENGTH = 17; + + public static readonly byte[] DUMP_FILE_FORMAT_BLOB = new byte[] + { + 0xff, 0xd8, 0x00,0x00, 0xd8, + + // file format "EDGEDB" + 69, 68, 71, 69, 68, 66, + + 0x00, + + // file format "DUMP + 68, 85, 77, 80, + + 0x00, + }; + + public ProtocolVersion DumpRestoreVersion + => _version; + + private readonly ProtocolVersion _version = (1, 0); + + public async Task DumpDatabaseAsync(EdgeDBBinaryClient client, Stream stream, CancellationToken token = default(CancellationToken)) + { + using var cmdLock = await client.AquireCommandLockAsync(token).ConfigureAwait(false); + + try + { + var state = new DumpState(stream); + + await foreach (var result in client.Duplexer.DuplexAndSyncAsync(new Dump(), token)) + { + switch (result.Packet) + { + case ReadyForCommand: + result.Finish(); + break; + case DumpHeader dumpHeader: + await state.WriteHeaderAsync(in dumpHeader); + break; + case DumpBlock block: + await state.WriteBlockAsync(in block); + break; + case ErrorResponse error: + { + throw new EdgeDBErrorException(error); + } + } + } + } + catch (Exception x) when (x is OperationCanceledException or TaskCanceledException) + { + throw new TimeoutException("Database dump timed out", x); + } + } + + public async Task RestoreDatabaseAsync(EdgeDBBinaryClient client, Stream stream, CancellationToken token) + { + using var cmdLock = await client.AquireCommandLockAsync(token).ConfigureAwait(false); + + var count = await client + .QueryRequiredSingleAsync(""" + select count( + schema::Module + filter not .builtin and not .name = "default" + ) + count( + schema::Object + filter .name like "default::%" + ) + """, token: token).ConfigureAwait(false); + + if (count > 0) + throw new InvalidOperationException("Cannot restore: Database isn't empty"); + + var packets = ReadDatabaseDump(stream); + + await foreach (var result in client.Duplexer.DuplexAsync(packets.Restore, token)) + { + switch (result.Packet) + { + case ErrorResponse err: + throw new EdgeDBErrorException(err); + case RestoreReady: + result.Finish(); + break; + default: + throw new UnexpectedMessageException(ServerMessageType.RestoreReady, result.Packet.Type); + } + } + + foreach (var block in packets.Blocks) + { + await client.Duplexer.SendAsync(block, token).ConfigureAwait(false); + } + + var restoreResult = await client.Duplexer.DuplexSingleAsync(new RestoreEOF(), token).ConfigureAwait(false); + + return restoreResult is null + ? throw new UnexpectedDisconnectException() + : restoreResult is ErrorResponse error + ? throw new EdgeDBErrorException(error) + : restoreResult is not CommandComplete complete + ? throw new UnexpectedMessageException(ServerMessageType.CommandComplete, restoreResult.Type) + : complete.Status; + } + + public unsafe (Restore Restore, IEnumerable Blocks) ReadDatabaseDump(Stream stream) + { + if (!stream.CanRead) + throw new ArgumentException($"Cannot read from {nameof(stream)}"); + + // read file format + Span formatBuffer = stackalloc byte[DUMP_FILE_FORMAT_LENGTH]; + + ThrowIfEndOfStream(stream.Read(formatBuffer) == DUMP_FILE_FORMAT_LENGTH); + + if (!formatBuffer.SequenceEqual(DUMP_FILE_FORMAT_BLOB)) + throw new FormatException("Format of stream does not match the edgedb dump format"); + + Restore? restore = null; + List blocks = new(); + + long version = 0; + + BinaryUtils.ReadPrimitive(stream, ref version); + + + if (version > _version.Major) + throw new ArgumentException($"Unsupported dump version {version}"); + + var header = ReadPacket(stream); + + if (header is not DumpHeader dumpHeader) + throw new FormatException($"Expected dump header but got {header?.Type}"); + + restore = new Restore() + { + HeaderData = dumpHeader.Raw + }; + + while (stream.Position < stream.Length) + { + var packet = ReadPacket(stream); + + if (packet is not DumpBlock dumpBlock) + throw new FormatException($"Expected dump block but got {header?.Type}"); + + blocks.Add(new RestoreBlock + { + BlockData = dumpBlock.Raw + }); + } + + return (restore!, blocks); + } + + private unsafe static IReceiveable ReadPacket(Stream stream) + { + scoped Span headerBuffer = stackalloc byte[sizeof(DumpSectionHeader)]; + + stream.Read(headerBuffer); + + ref var header = ref Unsafe.As(ref headerBuffer[0]); + + BinaryUtils.CorrectEndianness(ref header.Length); + + // read packet data + var data = new byte[header.Length]; + + stream.Read(data); + + // check hash + using(var alg = SHA1.Create()) + { + if(!alg.ComputeHash(data).SequenceEqual(header.Hash.ToArray())) + throw new ArgumentException("Hash did not match"); + } + + var reader = new PacketReader(data); + + return header.Type switch + { + 'H' => new DumpHeader(ref reader, in header.Length), + 'D' => new DumpBlock(ref reader, in header.Length), + _ => throw new ArgumentException($"Unknown packet format {header.Type}") + }; + } + + private static void ThrowIfEndOfStream(bool readSuccess) + { + if (!readSuccess) + throw new EndOfStreamException(); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/ExecuteResult.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ExecuteResult.cs new file mode 100644 index 00000000..40df8eec --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/ExecuteResult.cs @@ -0,0 +1,21 @@ +using EdgeDB.Binary.Codecs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol +{ + internal class ExecuteResult + { + public CodecInfo OutCodecInfo { get; } + public ReadOnlyMemory[] Data { get; } + + public ExecuteResult(ReadOnlyMemory[] data, CodecInfo outCodecInfo) + { + Data = data; + OutCodecInfo = outCodecInfo; + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/IProtocolProvider.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/IProtocolProvider.cs new file mode 100644 index 00000000..5752f812 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/IProtocolProvider.cs @@ -0,0 +1,62 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.V1._0; +using EdgeDB.Binary.Protocol.V2._0; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol +{ + internal delegate IReceiveable PacketReadFactory(ref PacketReader reader, in int length); + internal delegate IProtocolProvider ProtocolProviderFactory(EdgeDBBinaryClient client); + internal delegate ref ICodec? RelativeCodecDelegate(in int position); + internal delegate ref ITypeDescriptor RelativeDescriptorDelegate(in int position); + + internal interface IProtocolProvider + { + private static ProtocolProviderFactory? _defaultProvider; + private static readonly ConcurrentDictionary _providers = new(); + + public static IProtocolProvider GetDefaultProvider(EdgeDBBinaryClient client) + => (_defaultProvider ??= Providers[ProtocolVersion.EdgeDBBinaryDefaultVersion].Factory)(client); + + public static IProtocolProvider GetProvider(EdgeDBBinaryClient client) + => _providers.GetOrAdd(client.Connection, _ => Providers[ProtocolVersion.EdgeDBBinaryDefaultVersion].Factory)(client); + + public static void UpdateProviderFor(EdgeDBBinaryClient client, IProtocolProvider provider) + => _providers.AddOrUpdate(client.Connection, Providers[provider.Version].Factory, (_, __) => Providers[provider.Version].Factory); + + public static readonly Dictionary Providers = new() + { + { (1, 0), (typeof(V1ProtocolProvider), c => new V1ProtocolProvider(c)) }, + { (2, 0), (typeof(V2ProtocolProvider), c => new V2ProtocolProvider(c)) } + }; + + ProtocolPhase Phase { get; } + + ProtocolVersion Version { get; } + + IReadOnlyDictionary ServerConfig { get; } + + PacketReadFactory? GetPacketFactory(ServerMessageType type); + + Task ParseQueryAsync(QueryParameters query, CancellationToken token); + + Task ExecuteQueryAsync(QueryParameters query, ParseResult result, CancellationToken token); + + ITypeDescriptor GetDescriptor(ref PacketReader reader); + + ICodec? BuildCodec(in T descriptor, RelativeCodecDelegate getRelativeCodec, RelativeDescriptorDelegate getRelativeDescriptor) where T : ITypeDescriptor; + + Task SendSyncMessageAsync(CancellationToken token); + + ValueTask ProcessAsync(scoped in T message) where T : IReceiveable; + + Sendable Handshake(); + Sendable Terminate(); + Sendable Sync(); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/IReceiveable.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/IReceiveable.cs similarity index 92% rename from src/EdgeDB.Net.Driver/Binary/Packets/IReceiveable.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/IReceiveable.cs index 7a4ae771..cc26f918 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/IReceiveable.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/IReceiveable.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol { /// /// Represents a generic packet received from the server. diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/ITypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ITypeDescriptor.cs new file mode 100644 index 00000000..2d297b56 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/ITypeDescriptor.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary +{ + internal interface ITypeDescriptor + { + Guid Id { get; } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/ParseResult.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ParseResult.cs new file mode 100644 index 00000000..a65bd57d --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/ParseResult.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol +{ + internal sealed class ParseResult + { + public CodecInfo InCodecInfo { get; } + public CodecInfo OutCodecInfo { get; } + public ReadOnlyMemory? StateData { get; } + public Capabilities Capabilities { get; } + public Cardinality Cardinality { get; } + + public ParseResult( + CodecInfo inCodecInfo, + CodecInfo outCodecInfo, + scoped in ReadOnlyMemory? stateData, + Cardinality cardinality, + Capabilities capabilities) + { + InCodecInfo = inCodecInfo; + OutCodecInfo = outCodecInfo; + StateData = stateData; + Capabilities = capabilities; + Cardinality = cardinality; + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/ProtocolPhase.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ProtocolPhase.cs new file mode 100644 index 00000000..49fda6e6 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/ProtocolPhase.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol +{ + internal enum ProtocolPhase + { + Connection, + Auth, + Command, + Dump, + Termination, + Errored + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/ProtocolVersion.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ProtocolVersion.cs new file mode 100644 index 00000000..f5f5aee4 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/ProtocolVersion.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB +{ + /// + /// Represents a protocol version used within EdgeDB. + /// + public sealed class ProtocolVersion : IComparable + { + /// + /// The default protocol version used for dump/restore operations. + /// + public static readonly ProtocolVersion DumpRestoreDefaultVersion = (1, 0); + + /// + /// The default protocol version used for the edgedb binary protocol. + /// + public static readonly ProtocolVersion EdgeDBBinaryDefaultVersion = (2, 0); + + /// + /// Gets the major component of the protocol. + /// + public ushort Major { get; init; } + + /// + /// Gets the minor version of the protocol. + /// + public ushort Minor { get; init; } + + private int IntValue + => Minor + (Major << 16); + + /// + /// Constructs a new . + /// + /// The major component of the protocol. + /// The minor component of the protocol. + public ProtocolVersion(ushort major, ushort minor) + { + Major = major; + Minor = minor; + } + + /// + public override string ToString() + { + return $"{Major}.{Minor}"; + } + + /// + public bool Equals(in ushort major, in ushort minor) + { + return Major == major && Minor == minor; + } + + public override bool Equals(object? obj) + { + if (obj is not ProtocolVersion version) + return base.Equals(obj); + + return version.Major == Major && version.Minor == Minor; + } + + public int CompareTo(ProtocolVersion? other) + { + if(other is null) + { + return int.MinValue; + } + + return IntValue - other.IntValue; + } + + public static implicit operator ProtocolVersion((int major, int minor) v) => new((ushort)v.major, (ushort)v.minor); + + public override int GetHashCode() + => HashCode.Combine(Major, Minor); + + internal void Deconstruct(out int major, out int minor) + { + major = Major; + minor = Minor; + } + + public static bool operator ==(ProtocolVersion left, ProtocolVersion right) + { + if (left is null) + return right is null; + return left.Equals(right); + } + + public static bool operator !=(ProtocolVersion left, ProtocolVersion right) + { + return !(left == right); + } + + public static bool operator <(ProtocolVersion left, ProtocolVersion right) + { + return left is null ? right is not null : left.CompareTo(right) < 0; + } + + public static bool operator <=(ProtocolVersion left, ProtocolVersion right) + { + return left is null || left.CompareTo(right) <= 0; + } + + public static bool operator >(ProtocolVersion left, ProtocolVersion right) + { + return left is not null && left.CompareTo(right) > 0; + } + + public static bool operator >=(ProtocolVersion left, ProtocolVersion right) + { + return left is null ? right is null : left.CompareTo(right) >= 0; + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/QueryParameters.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/QueryParameters.cs new file mode 100644 index 00000000..13530743 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/QueryParameters.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol +{ + internal sealed class QueryParameters + { + public string Query { get; } + public IDictionary? Arguments { get; } + public Capabilities Capabilities { get; } + public Cardinality Cardinality { get; } + public IOFormat Format { get; } + + public bool ImplicitTypeNames { get; } + + public QueryParameters( + string query, IDictionary? args, + Capabilities capabilities, Cardinality cardinality, + IOFormat format, bool implicitTypeNames) + { + Query = query; + Arguments = args; + Capabilities = capabilities; + Cardinality = cardinality; + Format = format; + ImplicitTypeNames = implicitTypeNames; + } + + public ulong GetCacheKey() + { + return CodecBuilder.GetCacheHashKey(Query, Cardinality, Format); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendable.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/Sendable.cs similarity index 91% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendable.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/Sendable.cs index 104098e9..a3b715e9 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendable.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/Sendable.cs @@ -4,13 +4,13 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol { internal abstract class Sendable { public abstract int Size { get; } - - public abstract ClientMessageTypes Type { get;} + + public abstract ClientMessageTypes Type { get; } protected abstract void BuildPacket(ref PacketWriter writer); diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/ServerMessageType.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/ServerMessageType.cs new file mode 100644 index 00000000..0c99bd8c --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/ServerMessageType.cs @@ -0,0 +1,25 @@ + +namespace EdgeDB.Binary +{ + /// + /// Represents all supported message types sent by the server. + /// + internal enum ServerMessageType : sbyte + { + RestoreReady = 0x2b, + DumpBlock = 0x3d, + DumpHeader = 0x40, + CommandComplete = 0x43, + Data = 0x44, + ErrorResponse = 0x45, + ServerKeyData = 0x4b, + LogMessage = 0x4c, + Authentication = 0x52, + CommandDataDescription = 0x54, + ParameterStatus = 0x53, + ReadyForCommand = 0x5a, + StateDataDescription = 0x73, + ServerHandshake = 0x76, + + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/ArrayTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ArrayTypeDescriptor.cs similarity index 78% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/ArrayTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ArrayTypeDescriptor.cs index 249c1a70..c7807c74 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/ArrayTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ArrayTypeDescriptor.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct ArrayTypeDescriptor : ITypeDescriptor { @@ -14,9 +15,10 @@ namespace EdgeDB.Binary public readonly uint[] Dimensions; - public ArrayTypeDescriptor(Guid id, ref PacketReader reader) + public ArrayTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; + TypePos = reader.ReadUInt16(); var count = reader.ReadUInt16(); diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/BaseScalarTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/BaseScalarTypeDescriptor.cs similarity index 71% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/BaseScalarTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/BaseScalarTypeDescriptor.cs index 4dcafd90..a209823e 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/BaseScalarTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/BaseScalarTypeDescriptor.cs @@ -1,16 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct BaseScalarTypeDescriptor : ITypeDescriptor { public readonly Guid Id; - public BaseScalarTypeDescriptor(Guid id) + public BaseScalarTypeDescriptor(scoped in Guid id) { Id = id; } diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/DescriptorType.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/DescriptorType.cs similarity index 89% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/DescriptorType.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/DescriptorType.cs index 2bd620d0..5cd61b69 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/DescriptorType.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/DescriptorType.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal enum DescriptorType : byte { diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/EnumerationTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/EnumerationTypeDescriptor.cs similarity index 79% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/EnumerationTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/EnumerationTypeDescriptor.cs index beb9c4f7..391d7ad7 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/EnumerationTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/EnumerationTypeDescriptor.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct EnumerationTypeDescriptor : ITypeDescriptor { @@ -12,7 +12,7 @@ namespace EdgeDB.Binary public readonly string[] Members; - public EnumerationTypeDescriptor(Guid id, ref PacketReader reader) + public EnumerationTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/InputShapeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/InputShapeDescriptor.cs new file mode 100644 index 00000000..c60f853f --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/InputShapeDescriptor.cs @@ -0,0 +1,33 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors +{ + internal readonly struct InputShapeDescriptor : ITypeDescriptor + { + public readonly Guid Id; + + public readonly ShapeElement[] Shapes; + + public InputShapeDescriptor(scoped in Guid id, scoped ref PacketReader reader) + { + Id = id; + + var elementCount = reader.ReadUInt16(); + + var shapes = new ShapeElement[elementCount]; + for (int i = 0; i != elementCount; i++) + { + shapes[i] = new ShapeElement(ref reader); + } + + Shapes = shapes; + } + + Guid ITypeDescriptor.Id => Id; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/NamedTupleTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/NamedTupleTypeDescriptor.cs similarity index 51% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/NamedTupleTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/NamedTupleTypeDescriptor.cs index b8baddb0..e61c8b6a 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/NamedTupleTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/NamedTupleTypeDescriptor.cs @@ -1,10 +1,11 @@ -using System; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct NamedTupleTypeDescriptor : ITypeDescriptor { @@ -12,7 +13,7 @@ namespace EdgeDB.Binary public readonly TupleElement[] Elements; - public NamedTupleTypeDescriptor(Guid id, ref PacketReader reader) + public NamedTupleTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; @@ -22,14 +23,7 @@ public NamedTupleTypeDescriptor(Guid id, ref PacketReader reader) for (int i = 0; i != count; i++) { - var name = reader.ReadString(); - var typePos = reader.ReadInt16(); - - elements[i] = new TupleElement - { - Name = name, - TypePos = typePos - }; + elements[i] = new TupleElement(ref reader); } Elements = elements; @@ -37,11 +31,4 @@ public NamedTupleTypeDescriptor(Guid id, ref PacketReader reader) Guid ITypeDescriptor.Id => Id; } - - internal readonly struct TupleElement - { - public readonly string Name { get; init; } - - public readonly short TypePos { get; init; } - } } diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ObjectShapeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ObjectShapeDescriptor.cs new file mode 100644 index 00000000..44e77060 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ObjectShapeDescriptor.cs @@ -0,0 +1,35 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors +{ + internal readonly struct ObjectShapeDescriptor : ITypeDescriptor + { + public readonly Guid Id; + + public readonly ShapeElement[] Shapes; + + public ObjectShapeDescriptor(scoped in Guid id, scoped ref PacketReader reader) + { + Id = id; + + var elementCount = reader.ReadUInt16(); + + ShapeElement[] shapes = new ShapeElement[elementCount]; + for (int i = 0; i != elementCount; i++) + { + shapes[i] = new ShapeElement(ref reader); + } + + Shapes = shapes; + } + + Guid ITypeDescriptor.Id => Id; + } + + +} diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/RangeTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/RangeTypeDescriptor.cs similarity index 71% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/RangeTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/RangeTypeDescriptor.cs index 3786c80c..d1b42ba5 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/RangeTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/RangeTypeDescriptor.cs @@ -1,17 +1,17 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct RangeTypeDescriptor : ITypeDescriptor { public readonly Guid Id; public readonly ushort TypePos; - public RangeTypeDescriptor(Guid id, ref PacketReader reader) + public RangeTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; TypePos = reader.ReadUInt16(); diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/ScalarTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ScalarTypeDescriptor.cs similarity index 72% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/ScalarTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ScalarTypeDescriptor.cs index 946db329..e7b8b3dd 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/ScalarTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ScalarTypeDescriptor.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct ScalarTypeDescriptor : ITypeDescriptor { @@ -12,7 +12,7 @@ namespace EdgeDB.Binary public readonly ushort BaseTypePos; - public ScalarTypeDescriptor(Guid id, ref PacketReader reader) + public ScalarTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; BaseTypePos = reader.ReadUInt16(); diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/ScalarTypeNameAnnotation.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ScalarTypeNameAnnotation.cs similarity index 71% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/ScalarTypeNameAnnotation.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ScalarTypeNameAnnotation.cs index 63c8bbd6..c5189042 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/ScalarTypeNameAnnotation.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ScalarTypeNameAnnotation.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct ScalarTypeNameAnnotation : ITypeDescriptor { @@ -12,7 +12,7 @@ namespace EdgeDB.Binary public readonly string Name; - public ScalarTypeNameAnnotation(Guid id, ref PacketReader reader) + public ScalarTypeNameAnnotation(scoped in Guid id, scoped ref PacketReader reader) { Id = id; Name = reader.ReadString(); diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/SetDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/SetDescriptor.cs similarity index 74% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/SetDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/SetDescriptor.cs index 2a036ae7..0aaf03de 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/SetDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/SetDescriptor.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct SetTypeDescriptor : ITypeDescriptor { @@ -12,7 +12,7 @@ namespace EdgeDB.Binary public readonly ushort TypePos; - public SetTypeDescriptor(Guid id, ref PacketReader reader) + public SetTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; TypePos = reader.ReadUInt16(); diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ShapeElement.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ShapeElement.cs new file mode 100644 index 00000000..02121d1f --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/ShapeElement.cs @@ -0,0 +1,25 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors +{ + internal readonly struct ShapeElement + { + public readonly ShapeElementFlags Flags; + public readonly Cardinality Cardinality; + public readonly string Name; + public readonly ushort TypePos; + + public ShapeElement(ref PacketReader reader) + { + Flags = (ShapeElementFlags)reader.ReadUInt32(); + Cardinality = (Cardinality)reader.ReadByte(); + Name = reader.ReadString(); + TypePos = reader.ReadUInt16(); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/TupleTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/TupleTypeDescriptor.cs similarity index 83% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/TupleTypeDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/TupleTypeDescriptor.cs index 94683664..ff631ac7 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/TupleTypeDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/TupleTypeDescriptor.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct TupleTypeDescriptor : ITypeDescriptor { @@ -15,7 +15,7 @@ public bool IsEmpty public readonly ushort[] ElementTypeDescriptorsIndex; - public TupleTypeDescriptor(Guid id, ref PacketReader reader) + public TupleTypeDescriptor(scoped in Guid id, scoped ref PacketReader reader) { Id = id; var count = reader.ReadUInt16(); diff --git a/src/EdgeDB.Net.Driver/Binary/Descriptors/TypeAnnotationDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/TypeAnnotationDescriptor.cs similarity index 71% rename from src/EdgeDB.Net.Driver/Binary/Descriptors/TypeAnnotationDescriptor.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/TypeAnnotationDescriptor.cs index 778f0208..103cd606 100644 --- a/src/EdgeDB.Net.Driver/Binary/Descriptors/TypeAnnotationDescriptor.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Descriptors/TypeAnnotationDescriptor.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Descriptors { internal readonly struct TypeAnnotationDescriptor : ITypeDescriptor { @@ -12,7 +12,7 @@ namespace EdgeDB.Binary public readonly Guid Id; public readonly string Annotation; - public TypeAnnotationDescriptor(DescriptorType type, Guid id, PacketReader reader) + public TypeAnnotationDescriptor(scoped in DescriptorType type, scoped in Guid id, scoped ref PacketReader reader) { Type = type; Id = id; diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/AuthStatus.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/AuthStatus.cs similarity index 94% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/AuthStatus.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/AuthStatus.cs index 06140873..61b90cdc 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/AuthStatus.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/AuthStatus.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the authentication state. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/AuthenticationStatus.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/AuthenticationStatus.cs similarity index 97% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/AuthenticationStatus.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/AuthenticationStatus.cs index c57ff07f..ddf21718 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/AuthenticationStatus.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/AuthenticationStatus.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the AuthenticationOK, diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/CommandComplete.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/CommandComplete.cs similarity index 95% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/CommandComplete.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/CommandComplete.cs index 2f9cf368..72b5d17a 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/CommandComplete.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/CommandComplete.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Command Complete packet diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/CommandDataDescription.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/CommandDataDescription.cs similarity index 96% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/CommandDataDescription.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/CommandDataDescription.cs index c63ed0a4..2fba3361 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/CommandDataDescription.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/CommandDataDescription.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Command Data Description packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/Data.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/Data.cs similarity index 94% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/Data.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/Data.cs index 21f40b46..bfd64739 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/Data.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/Data.cs @@ -1,6 +1,7 @@ +using EdgeDB.Binary.Protocol; using System.Collections.Immutable; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Data packet diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/DumpBlock.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/DumpBlock.cs similarity index 92% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/DumpBlock.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/DumpBlock.cs index e706914e..8c5f4405 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/DumpBlock.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/DumpBlock.cs @@ -1,8 +1,9 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System.Collections.Immutable; using System.Security.Cryptography; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Dump Block packet. @@ -48,7 +49,7 @@ internal DumpBlock(ref PacketReader reader, in int length) HashBuffer = SHA1.Create().ComputeHash(Raw); - using var r = new PacketReader(rawBuff); + var r = new PacketReader(rawBuff); _attributes = r.ReadKeyValues(); } } diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/DumpHeader.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/DumpHeader.cs similarity index 98% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/DumpHeader.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/DumpHeader.cs index 489dc7dc..83db701f 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/DumpHeader.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/DumpHeader.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -7,7 +8,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Dump Header packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ErrorResponse.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ErrorResponse.cs similarity index 64% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ErrorResponse.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ErrorResponse.cs index 056cc504..59eeae65 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ErrorResponse.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ErrorResponse.cs @@ -1,16 +1,21 @@ +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.Common; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Error Response packet. /// - internal readonly struct ErrorResponse : IReceiveable, IExecuteError +#pragma warning disable CS0618 // Type or member is obsolete + internal readonly struct ErrorResponse : IReceiveable, IExecuteError, IProtocolError +#pragma warning restore CS0618 // Type or member is obsolete { /// public ServerMessageType Type @@ -31,12 +36,6 @@ public ServerMessageType Type /// public string Message { get; } - /// - /// Gets a collection of attributes sent with this error. - /// - public IReadOnlyCollection Attributes - => _attributes.ToImmutableArray(); - private readonly KeyValue[] _attributes; internal ErrorResponse(ref PacketReader reader) @@ -47,14 +46,21 @@ internal ErrorResponse(ref PacketReader reader) _attributes = reader.ReadKeyValues(); } - internal bool TryGetAttribute(ushort code, out KeyValue value) + public bool TryGetAttribute(in ushort code, out KeyValue kv) { - value = _attributes.FirstOrDefault(x => x.Code == code); - return _attributes.Any(x => x.Code == code); - } + for(int i = 0; i != _attributes.Length; i++) + { + ref var attr = ref _attributes[i]; - string? IExecuteError.Message => Message; + if (attr.Code == code) + { + kv = attr; + return true; + } + } - ServerErrorCodes IExecuteError.ErrorCode => ErrorCode; + kv = default; + return false; + } } } diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/LogMessage.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/LogMessage.cs similarity index 95% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/LogMessage.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/LogMessage.cs index bdf579bc..5d5f65a9 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/LogMessage.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/LogMessage.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Log Message packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ParameterStatus.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ParameterStatus.cs similarity index 93% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ParameterStatus.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ParameterStatus.cs index 230bb7fd..506ce8af 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ParameterStatus.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ParameterStatus.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Parameter Status packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ReadyForCommand.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ReadyForCommand.cs similarity index 93% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ReadyForCommand.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ReadyForCommand.cs index 121bc22f..554008a3 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ReadyForCommand.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ReadyForCommand.cs @@ -1,6 +1,7 @@ +using EdgeDB.Binary.Protocol; using System.Collections.Immutable; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Ready for Command packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/RestoreReady.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/RestoreReady.cs similarity index 93% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/RestoreReady.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/RestoreReady.cs index 9f8e3bcf..ba0037a6 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/RestoreReady.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/RestoreReady.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Restore Ready packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerHandshake.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ServerHandshake.cs similarity index 90% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerHandshake.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ServerHandshake.cs index df672f02..04de383a 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerHandshake.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ServerHandshake.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Server Handshake packet. @@ -19,12 +20,12 @@ public ServerMessageType Type /// /// Gets the major version of the server. /// - public ushort MajorVersion { get; } + public readonly ushort MajorVersion; /// /// Gets the minor version of the server. /// - public ushort MinorVersion { get; } + public readonly ushort MinorVersion; /// /// Gets a collection of s used by the server. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerKeyData.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ServerKeyData.cs similarity index 92% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerKeyData.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ServerKeyData.cs index d7dea277..96fe8bea 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/ServerKeyData.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/ServerKeyData.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the Server Key Data packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/StateDataDescription.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/StateDataDescription.cs similarity index 92% rename from src/EdgeDB.Net.Driver/Binary/Packets/Receivables/StateDataDescription.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/StateDataDescription.cs index c816bb3d..320845cb 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Receivables/StateDataDescription.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Receivables/StateDataDescription.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// Represents the State Data Description packet. diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/AuthenticationSASLInitialResponse.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/AuthenticationSASLInitialResponse.cs similarity index 93% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/AuthenticationSASLInitialResponse.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/AuthenticationSASLInitialResponse.cs index 32dde943..43415657 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/AuthenticationSASLInitialResponse.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/AuthenticationSASLInitialResponse.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class AuthenticationSASLInitialResponse : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/AuthenticationSASLResponse.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/AuthenticationSASLResponse.cs similarity index 91% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/AuthenticationSASLResponse.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/AuthenticationSASLResponse.cs index 9fca3ff3..e075d63d 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/AuthenticationSASLResponse.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/AuthenticationSASLResponse.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class AuthenticationSASLResponse : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/ClientHandshake.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/ClientHandshake.cs similarity index 86% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/ClientHandshake.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/ClientHandshake.cs index 2fd7efb9..88250d0a 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/ClientHandshake.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/ClientHandshake.cs @@ -1,4 +1,6 @@ -namespace EdgeDB.Binary.Packets +using EdgeDB.Binary.Protocol; + +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class ClientHandshake : Sendable { @@ -12,8 +14,8 @@ public override int Size } - public short MajorVersion { get; set; } - public short MinorVersion { get; set; } + public ushort MajorVersion { get; set; } + public ushort MinorVersion { get; set; } public ConnectionParam[] ConnectionParameters { get; set; } = Array.Empty(); public ProtocolExtension[] Extensions { get; set; } = Array.Empty(); diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Dump.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Dump.cs similarity index 87% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Dump.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Dump.cs index b6e9cd83..aa20661b 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Dump.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Dump.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class Dump : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Execute.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Execute.cs similarity index 97% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Execute.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Execute.cs index f322a972..661617db 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Execute.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Execute.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class Execute : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Parse.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Parse.cs similarity index 96% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Parse.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Parse.cs index 3baaa453..5fea2ca0 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Parse.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Parse.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -6,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { /// /// https://www.edgedb.com/docs/reference/protocol/messages#prepare diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Restore.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Restore.cs similarity index 91% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Restore.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Restore.cs index 865b504f..3b970ba4 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Restore.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Restore.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class Restore : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/RestoreBlock.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/RestoreBlock.cs similarity index 88% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/RestoreBlock.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/RestoreBlock.cs index 5d51509e..02bd5609 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/RestoreBlock.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/RestoreBlock.cs @@ -1,3 +1,4 @@ +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -5,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class RestoreBlock : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/RestoreEOF.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/RestoreEOF.cs similarity index 84% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/RestoreEOF.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/RestoreEOF.cs index 28a35b30..0c7f0ae3 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/RestoreEOF.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/RestoreEOF.cs @@ -1,10 +1,11 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class RestoreEOF : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Sync.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Sync.cs similarity index 83% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Sync.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Sync.cs index 7d255bb8..4298ebfc 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Sync.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Sync.cs @@ -1,10 +1,11 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class Sync : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Terminate.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Terminate.cs similarity index 83% rename from src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Terminate.cs rename to src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Terminate.cs index 31c5f712..abd7bd90 100644 --- a/src/EdgeDB.Net.Driver/Binary/Packets/Sendables/Terminate.cs +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/Sendables/Terminate.cs @@ -1,10 +1,11 @@ +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace EdgeDB.Binary.Packets +namespace EdgeDB.Binary.Protocol.V1._0.Packets { internal sealed class Terminate : Sendable { diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/V1ProtocolProvider.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/V1ProtocolProvider.cs new file mode 100644 index 00000000..f28ea146 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V1.0/V1ProtocolProvider.cs @@ -0,0 +1,627 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.V1._0.Descriptors; +using EdgeDB.Binary.Protocol.V1._0.Packets; +using EdgeDB.Utils; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Dynamic; +using System.Linq; +using System.Net; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V1._0 +{ + internal class V1ProtocolProvider : IProtocolProvider + { + public int SuggestedPoolConcurrency { get; private set; } + + public IReadOnlyDictionary ServerConfig + => _rawServerConfig.ToImmutableDictionary(); + + public ref ReadOnlyMemory ServerKey + => ref _serverKey; + + public ProtocolPhase Phase { get; private set; } + + public virtual ProtocolVersion Version { get; } = (1, 0); + + private IBinaryDuplexer Duplexer + => _client.Duplexer; + + private ILogger Logger + => _client.Logger; + + private readonly EdgeDBBinaryClient _client; + private ReadOnlyMemory _serverKey; + private Dictionary _rawServerConfig = new(); + + public V1ProtocolProvider(EdgeDBBinaryClient client) + { + _client = client; + } + + public virtual PacketReadFactory? GetPacketFactory(ServerMessageType type) + { + return type switch + { + ServerMessageType.Authentication => (ref PacketReader reader, in int length) => new AuthenticationStatus(ref reader), + ServerMessageType.CommandComplete => (ref PacketReader reader, in int length) => new CommandComplete(ref reader), + ServerMessageType.CommandDataDescription => (ref PacketReader reader, in int length) => new CommandDataDescription(ref reader), + ServerMessageType.Data => (ref PacketReader reader, in int length) => new Data(ref reader), + ServerMessageType.DumpBlock => (ref PacketReader reader, in int length) => new DumpBlock(ref reader, in length), + ServerMessageType.DumpHeader => (ref PacketReader reader, in int length) => new DumpHeader(ref reader, in length), + ServerMessageType.ErrorResponse => (ref PacketReader reader, in int length) => new ErrorResponse(ref reader), + ServerMessageType.LogMessage => (ref PacketReader reader, in int length) => new LogMessage(ref reader), + ServerMessageType.ParameterStatus => (ref PacketReader reader, in int length) => new ParameterStatus(ref reader), + ServerMessageType.ReadyForCommand => (ref PacketReader reader, in int length) => new ReadyForCommand(ref reader), + ServerMessageType.RestoreReady => (ref PacketReader reader, in int length) => new RestoreReady(ref reader), + ServerMessageType.ServerHandshake => (ref PacketReader reader, in int length) => new ServerHandshake(ref reader), + ServerMessageType.ServerKeyData => (ref PacketReader reader, in int length) => new ServerKeyData(ref reader), + ServerMessageType.StateDataDescription => (ref PacketReader reader, in int length) => new StateDataDescription(ref reader), + _ => null, + }; + } + + public virtual async Task ExecuteQueryAsync(QueryParameters queryParameters, ParseResult parseResult, CancellationToken token) + { + if (parseResult.InCodecInfo.Codec is not IArgumentCodec argumentCodec) + throw new MissingCodecException($"Cannot encode arguments, {parseResult.InCodecInfo.Codec} is not a registered argument codec"); + + ErrorResponse? error = null; + var executeSuccess = false; + var gotStateDescriptor = false; + var successfullyParsed = false; + var receivedData = new List>(); + + var stateBuf = parseResult.StateData; + + do + { + await foreach (var result in Duplexer.DuplexAndSyncAsync(new Execute() + { + Capabilities = queryParameters.Capabilities, + Query = queryParameters.Query, + Format = queryParameters.Format, + ExpectedCardinality = queryParameters.Cardinality, + ExplicitObjectIds = _client.ClientConfig.ExplicitObjectIds, + StateTypeDescriptorId = _client.StateDescriptorId, + StateData = stateBuf, + ImplicitTypeNames = queryParameters.ImplicitTypeNames, // used for type builder + ImplicitTypeIds = _client.ClientConfig.ImplicitTypeIds, + Arguments = argumentCodec.SerializeArguments(_client, queryParameters.Arguments), + ImplicitLimit = _client.ClientConfig.ImplicitLimit, + InputTypeDescriptorId = parseResult.InCodecInfo.Id, + OutputTypeDescriptorId = parseResult.OutCodecInfo.Id, + }, token)) + { + switch (result.Packet) + { + case Data data: + receivedData.Add(data.PayloadBuffer); + break; + case StateDataDescription stateDescriptor: + { + var stateCodec = CodecBuilder.BuildCodec(_client, stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); + var stateCodecId = stateDescriptor.TypeDescriptorId; + _client.UpdateStateCodec(stateCodec, stateCodecId); + gotStateDescriptor = true; + stateBuf = _client.SerializeState(); + } + break; + case ErrorResponse err when err.ErrorCode is ServerErrorCodes.StateMismatchError: + { + // we should have received a state descriptor at this point, + // if we have not, this is a issue with the client implementation + if (!gotStateDescriptor) + { + throw new EdgeDBException("Failed to properly encode state data, this is a bug."); + } + + // we can safely retry by finishing this duplex and starting a + // new one with the 'while' loop. + result.Finish(); + } + break; + case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.ParameterTypeMismatchError: + error = err; + break; + case ReadyForCommand ready: + _client.UpdateTransactionState(ready.TransactionState); + result.Finish(); + executeSuccess = true; + break; + } + } + } + while (!successfullyParsed && !executeSuccess); + + if (error.HasValue) + throw new EdgeDBErrorException(error.Value, queryParameters.Query); + + return new ExecuteResult(receivedData.ToArray(), parseResult.OutCodecInfo); + } + + public virtual async Task ParseQueryAsync(QueryParameters queryParameters, CancellationToken token) + { + ErrorResponse? error = null; + var parseAttempts = 0; + var successfullyParsed = false; + var gotStateDescriptor = false; + + var cacheKey = queryParameters.GetCacheKey(); + + var stateBuf = _client.SerializeState(); + + var parseCardinality = queryParameters.Cardinality; + var parseCapabilities = queryParameters.Capabilities; + + if (!CodecBuilder.TryGetCodecs(this, cacheKey, out var inCodecInfo, out var outCodecInfo)) + { + while (!successfullyParsed) + { + if (parseAttempts > 2) + { + throw error.HasValue + ? new EdgeDBException($"Failed to parse query after {parseAttempts} attempts", new EdgeDBErrorException(error.Value, queryParameters.Query)) + : new EdgeDBException($"Failed to parse query after {parseAttempts} attempts"); + } + + await foreach (var result in Duplexer.DuplexAndSyncAsync(new Parse + { + Capabilities = parseCapabilities, + Query = queryParameters.Query, + Format = queryParameters.Format, + ExpectedCardinality = queryParameters.Cardinality, + ExplicitObjectIds = _client.ClientConfig.ExplicitObjectIds, + StateTypeDescriptorId = _client.StateDescriptorId, + StateData = stateBuf, + ImplicitLimit = _client.ClientConfig.ImplicitLimit, + ImplicitTypeNames = queryParameters.ImplicitTypeNames, // used for type builder + ImplicitTypeIds = _client.ClientConfig.ImplicitTypeIds, + }, token)) + { + switch (result.Packet) + { + case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.StateMismatchError: + error = err; + break; + case ErrorResponse err when err.ErrorCode is ServerErrorCodes.StateMismatchError: + { + // we should have received a state descriptor at this point, + // if we have not, this is a issue with the client implementation + if (!gotStateDescriptor) + { + throw new EdgeDBException("Failed to properly encode state data, this is a bug."); + } + + // we can safely retry by finishing this duplex and starting a + // new one with the 'while' loop. + result.Finish(); + } + break; + case CommandDataDescription descriptor: + { + outCodecInfo = new(descriptor.OutputTypeDescriptorId, + CodecBuilder.BuildCodec(_client, descriptor.OutputTypeDescriptorId, descriptor.OutputTypeDescriptorBuffer)); + + inCodecInfo = new(descriptor.InputTypeDescriptorId, + CodecBuilder.BuildCodec(_client, descriptor.InputTypeDescriptorId, descriptor.InputTypeDescriptorBuffer)); + + CodecBuilder.UpdateKeyMap(cacheKey, descriptor.InputTypeDescriptorId, descriptor.OutputTypeDescriptorId); + + parseCardinality = descriptor.Cardinality; + parseCapabilities = descriptor.Capabilities; + } + break; + case StateDataDescription stateDescriptor: + { + var stateCodec = CodecBuilder.BuildCodec(_client, stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); + var stateCodecId = stateDescriptor.TypeDescriptorId; + _client.UpdateStateCodec(stateCodec, stateCodecId); + gotStateDescriptor = true; + stateBuf = _client.SerializeState(); + } + break; + case ReadyForCommand ready: + _client.UpdateTransactionState(ready.TransactionState); + result.Finish(); + successfullyParsed = true; + break; + default: + break; + } + } + + parseAttempts++; + } + } + + if (error.HasValue) + throw new EdgeDBErrorException(error.Value, queryParameters.Query); + + if (outCodecInfo is null) + throw new MissingCodecException("Couldn't find a valid output codec"); + + if (inCodecInfo is null) + throw new MissingCodecException("Couldn't find a valid input codec"); + + if (inCodecInfo.Codec is not IArgumentCodec) + throw new MissingCodecException($"Cannot encode arguments, {inCodecInfo.Codec} is not a registered argument codec"); + + return new ParseResult(inCodecInfo, outCodecInfo, in stateBuf, parseCardinality, parseCapabilities); + } + + public virtual ICodec? BuildCodec(in T descriptor, RelativeCodecDelegate getRelativeCodec, RelativeDescriptorDelegate getRelativeDescriptor) + where T : ITypeDescriptor + { + switch (descriptor) + { + case EnumerationTypeDescriptor: + return CodecBuilder.GetOrCreateCodec(this, _ => new TextCodec()); + case NamedTupleTypeDescriptor namedTuple: + { + var elements = new ObjectProperty[namedTuple.Elements.Length]; + + for (int i = 0; i != namedTuple.Elements.Length; i++) + { + ref var element = ref namedTuple.Elements[i]; + + elements[i] = new ObjectProperty(Cardinality.Many, ref getRelativeCodec(element.TypePos)!, element.Name); + } + + return new ObjectCodec(in namedTuple.Id, elements); + } + case ObjectShapeDescriptor objectShape: + { + var elements = new ObjectProperty[objectShape.Shapes.Length]; + + for (int i = 0; i != objectShape.Shapes.Length; i++) + { + ref var element = ref objectShape.Shapes[i]; + + elements[i] = new ObjectProperty( + element.Cardinality, + ref getRelativeCodec(element.TypePos)!, + element.Name + ); + } + + return new ObjectCodec(in objectShape.Id, elements); + } + case InputShapeDescriptor input: + { + var names = new string[input.Shapes.Length]; + var innerCodecs = new ICodec[input.Shapes.Length]; + + for (int i = 0; i != input.Shapes.Length; i++) + { + ref var element = ref input.Shapes[i]; + + names[i] = element.Name; + innerCodecs[i] = getRelativeCodec(element.TypePos)!; + } + + return new SparceObjectCodec(in input.Id, innerCodecs, names); + } + case TupleTypeDescriptor tuple: + { + var innerCodecs = new ICodec[tuple.ElementTypeDescriptorsIndex.Length]; + for (int i = 0; i != tuple.ElementTypeDescriptorsIndex.Length; i++) + innerCodecs[i] = getRelativeCodec(tuple.ElementTypeDescriptorsIndex[i])!; + + return new TupleCodec(in tuple.Id, innerCodecs); + } + case RangeTypeDescriptor range: + { + ref var innerCodec = ref getRelativeCodec(range.TypePos)!; + + return new CompilableWrappingCodec(in range.Id, innerCodec, typeof(RangeCodec<>)); + } + case ArrayTypeDescriptor array: + { + ref var innerCodec = ref getRelativeCodec(array.TypePos)!; + + return new CompilableWrappingCodec(in array.Id, innerCodec, typeof(ArrayCodec<>)); + } + case SetTypeDescriptor set: + { + ref var innerCodec = ref getRelativeCodec(set.TypePos)!; + + return new CompilableWrappingCodec(in set.Id, innerCodec, typeof(SetCodec<>)); + } + case BaseScalarTypeDescriptor scalar: + throw new MissingCodecException($"Could not find the scalar type {scalar.Id}. Please file a bug report with your query that caused this error."); + default: + throw new MissingCodecException($"Could not find a type descriptor with type {descriptor.Id}. Please file a bug report with your query that caused this error."); + } + } + + public virtual ITypeDescriptor GetDescriptor(ref PacketReader reader) + { + var type = (DescriptorType)reader.ReadByte(); + var id = reader.ReadGuid(); + + ITypeDescriptor? descriptor = type switch + { + DescriptorType.ArrayTypeDescriptor => new ArrayTypeDescriptor(in id, ref reader), + DescriptorType.BaseScalarTypeDescriptor => new BaseScalarTypeDescriptor(id), + DescriptorType.EnumerationTypeDescriptor => new EnumerationTypeDescriptor(id, ref reader), + DescriptorType.NamedTupleDescriptor => new NamedTupleTypeDescriptor(id, ref reader), + DescriptorType.ObjectShapeDescriptor => new ObjectShapeDescriptor(id, ref reader), + DescriptorType.ScalarTypeDescriptor => new ScalarTypeDescriptor(id, ref reader), + DescriptorType.ScalarTypeNameAnnotation => new ScalarTypeNameAnnotation(id, ref reader), + DescriptorType.SetDescriptor => new SetTypeDescriptor(id, ref reader), + DescriptorType.TupleTypeDescriptor => new TupleTypeDescriptor(id, ref reader), + DescriptorType.InputShapeDescriptor => new InputShapeDescriptor(id, ref reader), + DescriptorType.RangeTypeDescriptor => new RangeTypeDescriptor(id, ref reader), + _ => null + }; + + if (descriptor is null) + { + var rawType = (byte)type; + + if (rawType >= 0x80 && rawType <= 0xfe) + { + descriptor = new TypeAnnotationDescriptor(in type, in id, ref reader); + } + else + throw new InvalidDataException($"No descriptor found for type {type}"); + } + + return descriptor; + } + + public virtual Sendable Handshake() + { + return new ClientHandshake() + { + MajorVersion = Version.Major, + MinorVersion = Version.Minor, + ConnectionParameters = _client.Connection.SecretKey is not null + ? new ConnectionParam[] + { + new ConnectionParam + { + Name = "user", + Value = _client.Connection.Username! + }, + new ConnectionParam + { + Name = "database", + Value = _client.Connection.Database! + }, + new ConnectionParam + { + Name = "secret_key", + Value = _client.Connection.SecretKey + } + } + : new ConnectionParam[] + { + new ConnectionParam + { + Name = "user", + Value = _client.Connection.Username! + }, + new ConnectionParam + { + Name = "database", + Value = _client.Connection.Database! + }, + } + }; + } + + public virtual ValueTask ProcessAsync(in T message) where T : IReceiveable + { + switch (message) + { + case ReadyForCommand ready: + _client.UpdateTransactionState(ready.TransactionState); + Phase = ProtocolPhase.Command; + break; + case ServerHandshake handshake: + if (!Version.Equals(in handshake.MajorVersion, in handshake.MinorVersion)) + { + var negotiated = _client.TryNegotiateProtocol(in handshake.MajorVersion, in handshake.MinorVersion); + + if(!negotiated && Version.Major != handshake.MajorVersion) + { + // major difference results in a disconnect + Logger.ProtocolMajorMismatch($"{handshake.MajorVersion}.{handshake.MinorVersion}", Version.ToString()); + return _client.DisconnectAsync(); + } + else if (!negotiated) + { + // minor mismatch + Logger.ProtocolMinorMismatch($"{handshake.MajorVersion}.{handshake.MinorVersion}", Version.ToString()); + } + + if(negotiated) + { + // this provider is basically dead now, do nothing. + return ValueTask.CompletedTask; + } + } + break; + case ErrorResponse err: + Logger.ErrorResponseReceived(err.Severity, err.Message); + _client.CancelReadyState(); + Phase = ProtocolPhase.Errored; + break; + case AuthenticationStatus authStatus: + if (authStatus.AuthStatus == AuthStatus.AuthenticationRequiredSASLMessage) + { + if (authStatus.AuthenticationMethods is null || authStatus.AuthenticationMethods.Length == 0) + throw new EdgeDBException("Expected an authentication method for AuthenticationStatus message. but got null"); + + return new(StartSASLAuthenticationAsync(authStatus.AuthenticationMethods[0])); + } + else if (authStatus.AuthStatus != AuthStatus.AuthenticationOK) + throw new UnexpectedMessageException("Expected AuthenticationRequiredSASLMessage, got " + authStatus.AuthStatus); + break; + case ServerKeyData keyData: + _serverKey = keyData.KeyBuffer; + break; + case StateDataDescription stateDescriptor: + var stateCodec = CodecBuilder.BuildCodec(_client, stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); + var stateCodecId = stateDescriptor.TypeDescriptorId; + _client.UpdateStateCodec(stateCodec, stateCodecId); + break; + case ParameterStatus parameterStatus: + ParseServerSettings(parameterStatus); + break; + case LogMessage log: + return _client.OnLogAsync(log.Severity, log.Code, log.Content); + default: + break; + } + + return ValueTask.CompletedTask; + } + + protected virtual void ParseServerSettings(ParameterStatus status) + { + try + { + switch (status.Name) + { + case "suggested_pool_concurrency": + var str = Encoding.UTF8.GetString(status.ValueBuffer); + if (!int.TryParse(str, out var suggestedPoolConcurrency)) + { + throw new FormatException("suggested_pool_concurrency type didn't match the expected type of int"); + } + SuggestedPoolConcurrency = suggestedPoolConcurrency; + break; + + case "system_config": + var reader = new PacketReader(status.ValueBuffer); + var length = reader.ReadInt32() - 16; + var descriptorId = reader.ReadGuid(); + reader.ReadBytes(length, out var typeDesc); + + var codec = CodecBuilder.GetCodec(this, descriptorId); + + if (codec is null) + { + var innerReader = new PacketReader(in typeDesc); + codec = CodecBuilder.BuildCodec(_client, descriptorId, ref innerReader); + + if (codec is null) + throw new MissingCodecException("Failed to build codec for system_config"); + } + + // disard length + reader.Skip(4); + + var obj = codec.Deserialize(ref reader, _client.CodecContext)!; + + _rawServerConfig = ((ExpandoObject)obj).ToDictionary(x => x.Key, x => x.Value); + break; + + default: + break; + } + } + catch (Exception x) + { + Logger.ServerSettingsParseFailed(x); + } + } + + protected virtual async Task StartSASLAuthenticationAsync(string method) + { + Phase = ProtocolPhase.Auth; + + using var scram = new Scram(); + + if (method is not "SCRAM-SHA-256") + { + throw new ProtocolViolationException("The only supported method is SCRAM-SHA-256"); + } + + var initialMsg = scram.BuildInitialMessage(_client.Connection.Username!); + + var expectedSig = Array.Empty(); + + await foreach (var result in Duplexer.DuplexAsync( + new AuthenticationSASLInitialResponse( + Encoding.UTF8.GetBytes(initialMsg), method + ))) + { + switch (result.Packet) + { + case AuthenticationStatus authResult: + { + switch (authResult.AuthStatus) + { + case AuthStatus.AuthenticationSASLContinue: + { + var (final, sig) = scram.BuildFinalMessage(Encoding.UTF8.GetString(authResult.SASLDataBuffer), _client.Connection.Password!); + + expectedSig = sig; + + await Duplexer.SendAsync(packets: new AuthenticationSASLResponse( + Encoding.UTF8.GetBytes(final) + )).ConfigureAwait(false); + } + break; + case AuthStatus.AuthenticationSASLFinal: + { + var key = Scram.ParseServerSig( + Encoding.UTF8.GetString(authResult.SASLDataBuffer) + ); + + if (!key.SequenceEqual(expectedSig)) + { + throw new InvalidSignatureException(); + } + } + break; + case AuthStatus.AuthenticationOK: + { + result.Finish(); + } + break; + default: + throw new UnexpectedMessageException($"Expected coninue or final but got {authResult.AuthStatus}"); + } + } + break; + case ErrorResponse err: + throw new EdgeDBErrorException(err); + } + } + } + + public virtual async Task SendSyncMessageAsync(CancellationToken token) + { + // if the current client is not connected, reconnect it + if (!Duplexer.IsConnected) + await _client.ReconnectAsync(token); + + var result = await Duplexer.DuplexSingleAsync(new Sync(), token).ConfigureAwait(false); + var rfc = result?.ThrowIfErrorOrNot(); + + if (rfc.HasValue) + { + _client.UpdateTransactionState(rfc.Value.TransactionState); + } + } + + public virtual Sendable Terminate() + => new Terminate(); + public virtual Sendable Sync() + => new Sync(); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ArrayTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ArrayTypeDescriptor.cs new file mode 100644 index 00000000..b09f4096 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ArrayTypeDescriptor.cs @@ -0,0 +1,59 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct ArrayTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public readonly ushort[] Ancestors; + + public readonly ushort Type; + + public readonly int[] Dimensions; + + public ArrayTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + + var ancestorsCount = reader.ReadUInt16(); + var ancestors = new ushort[ancestorsCount]; + + for (var i = 0; i != ancestorsCount; i++) + { + ancestors[i] = reader.ReadUInt16(); + } + + Ancestors = ancestors; + + Type = reader.ReadUInt16(); + + var dimensionCount = reader.ReadUInt16(); + var dimensions = new int[dimensionCount]; + + for(var i = 0; i != dimensionCount; i++) + { + dimensions[i] = reader.ReadInt32(); + } + + Dimensions = dimensions; + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined, IMetadataDescriptor.ConstructAncestors(Ancestors, relativeCodec, relativeDescriptor)); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/CompoundTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/CompoundTypeDescriptor.cs new file mode 100644 index 00000000..1bffbda6 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/CompoundTypeDescriptor.cs @@ -0,0 +1,45 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct CompoundTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchmeaDefined; + + public readonly TypeOperation Operation; + + public readonly ushort[] Components; + + public CompoundTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Name = reader.ReadString(); + IsSchmeaDefined = reader.ReadBoolean(); + Operation = (TypeOperation)reader.ReadByte(); + + var componentCount = reader.ReadUInt16(); + var components = new ushort[componentCount]; + + for(var i = 0; i != componentCount; i++) + { + components[i] = reader.ReadUInt16(); + } + + Components = components; + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchmeaDefined); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/DescriptorType.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/DescriptorType.cs new file mode 100644 index 00000000..f7400e08 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/DescriptorType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal enum DescriptorType : byte + { + Set = 0, + ObjectOutput = 1, + Scalar = 3, + Tuple = 4, + NamedTuple = 5, + Array = 6, + Enumeration = 7, + Input = 8, + Range = 9, + Object = 10, + Compound = 11, + TypeAnnotationText = 127 + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/EnumerationTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/EnumerationTypeDescriptor.cs new file mode 100644 index 00000000..e611d789 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/EnumerationTypeDescriptor.cs @@ -0,0 +1,55 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct EnumerationTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public readonly ushort[] Ancestors; + + public readonly string[] Members; + + public EnumerationTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + + var ancestorsCount = reader.ReadUInt16(); + var ancestors = new ushort[ancestorsCount]; + + for (var i = 0; i != ancestorsCount; i++) + { + ancestors[i] = reader.ReadUInt16(); + } + + Ancestors = ancestors; + + var memberCount = reader.ReadInt16(); + var members = new string[memberCount]; + + for(var i = 0; i != memberCount; i++) + { + members[i] = reader.ReadString(); + } + + Members = members; + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined, IMetadataDescriptor.ConstructAncestors(Ancestors, relativeCodec, relativeDescriptor)); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/IMetadataDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/IMetadataDescriptor.cs new file mode 100644 index 00000000..553dcadb --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/IMetadataDescriptor.cs @@ -0,0 +1,30 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal interface IMetadataDescriptor + { + static CodecAncestor[] ConstructAncestors(ushort[] ancestors, RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + { + var codecAncestors = new CodecAncestor[ancestors.Length]; + + for (var i = 0; i != ancestors.Length; i++) + { + codecAncestors[i] = new CodecAncestor( + ref relativeCodec(ancestors[i]), + ref relativeDescriptor(ancestors[i]) + ); + } + + return codecAncestors; + } + + CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/InputShapeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/InputShapeDescriptor.cs new file mode 100644 index 00000000..18f03182 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/InputShapeDescriptor.cs @@ -0,0 +1,34 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using InputShapeElement = EdgeDB.Binary.Protocol.V1._0.Descriptors.ShapeElement; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct InputShapeDescriptor : ITypeDescriptor + { + public readonly Guid Id; + + public readonly InputShapeElement[] Elements; + + public InputShapeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + + var elementCount = reader.ReadUInt16(); + var elements = new InputShapeElement[elementCount]; + + for (var i = 0; i != elementCount; i++) + { + elements[i] = new InputShapeElement(ref reader); + } + + Elements = elements; + } + + Guid ITypeDescriptor.Id => Id; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/NamedTupleTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/NamedTupleTypeDescriptor.cs new file mode 100644 index 00000000..a9b3ed7b --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/NamedTupleTypeDescriptor.cs @@ -0,0 +1,55 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct NamedTupleTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public readonly ushort[] Ancestors; + + public readonly TupleElement[] Elements; + + public NamedTupleTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + + var ancestorsCount = reader.ReadUInt16(); + var ancestors = new ushort[ancestorsCount]; + + for (var i = 0; i != ancestorsCount; i++) + { + ancestors[i] = reader.ReadUInt16(); + } + + Ancestors = ancestors; + + var elementCount = reader.ReadUInt16(); + var elements = new TupleElement[elementCount]; + + for (int i = 0; i != elementCount; i++) + { + elements[i] = new TupleElement(ref reader); + } + + Elements = elements; + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined, IMetadataDescriptor.ConstructAncestors(Ancestors, relativeCodec, relativeDescriptor)); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ObjectOutputShapeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ObjectOutputShapeDescriptor.cs new file mode 100644 index 00000000..2e96946b --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ObjectOutputShapeDescriptor.cs @@ -0,0 +1,39 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct ObjectOutputShapeDescriptor : ITypeDescriptor + { + public readonly Guid Id; + + public readonly bool IsEphemeralFreeShape; + + public readonly ushort Type; + + public readonly ShapeElement[] Elements; + + public ObjectOutputShapeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + IsEphemeralFreeShape = reader.ReadBoolean(); + Type = reader.ReadUInt16(); + + var elementCount = reader.ReadUInt16(); + var elements = new ShapeElement[elementCount]; + + for(var i = 0; i != elementCount; i++) + { + elements[i] = new ShapeElement(ref reader); + } + + Elements = elements; + } + + Guid ITypeDescriptor.Id => Id; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ObjectTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ObjectTypeDescriptor.cs new file mode 100644 index 00000000..1535c702 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ObjectTypeDescriptor.cs @@ -0,0 +1,31 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct ObjectTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public ObjectTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/RangeTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/RangeTypeDescriptor.cs new file mode 100644 index 00000000..a30eb817 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/RangeTypeDescriptor.cs @@ -0,0 +1,48 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct RangeTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public readonly ushort[] Ancestors; + + public readonly ushort Type; + + public RangeTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + + var ancestorsCount = reader.ReadUInt16(); + var ancestors = new ushort[ancestorsCount]; + + for (var i = 0; i != ancestorsCount; i++) + { + ancestors[i] = reader.ReadUInt16(); + } + + Ancestors = ancestors; + + Type = reader.ReadUInt16(); + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined, IMetadataDescriptor.ConstructAncestors(Ancestors, relativeCodec, relativeDescriptor)); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ScalarTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ScalarTypeDescriptor.cs new file mode 100644 index 00000000..7d27733f --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ScalarTypeDescriptor.cs @@ -0,0 +1,43 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct ScalarTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public readonly ushort[] Ancestors; + + public ScalarTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + + var ancestorsCount = reader.ReadUInt16(); + var ancestors = new ushort[ancestorsCount]; + + for(var i = 0; i != ancestorsCount; i++) + { + ancestors[i] = reader.ReadUInt16(); + } + + Ancestors = ancestors; + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined, IMetadataDescriptor.ConstructAncestors(Ancestors, relativeCodec, relativeDescriptor)); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/SetDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/SetDescriptor.cs new file mode 100644 index 00000000..14c6e025 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/SetDescriptor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct SetDescriptor : ITypeDescriptor + { + public readonly Guid Id; + + public readonly ushort Type; + + public SetDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Type = reader.ReadUInt16(); + } + + Guid ITypeDescriptor.Id => Id; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ShapeElement.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ShapeElement.cs new file mode 100644 index 00000000..bb145a0e --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/ShapeElement.cs @@ -0,0 +1,27 @@ +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct ShapeElement + { + public readonly ShapeElementFlags Flags; + public readonly Cardinality Cardinality; + public readonly string Name; + public readonly ushort TypePos; + public readonly ushort SourceType; + + public ShapeElement(ref PacketReader reader) + { + Flags = (ShapeElementFlags)reader.ReadUInt32(); + Cardinality = (Cardinality)reader.ReadByte(); + Name = reader.ReadString(); + TypePos = reader.ReadUInt16(); + SourceType = reader.ReadUInt16(); + } + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/TupleTypeDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/TupleTypeDescriptor.cs new file mode 100644 index 00000000..7cbbd87f --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/TupleTypeDescriptor.cs @@ -0,0 +1,55 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.Common.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct TupleTypeDescriptor : ITypeDescriptor, IMetadataDescriptor + { + public readonly Guid Id; + + public readonly string Name; + + public readonly bool IsSchemaDefined; + + public readonly ushort[] Ancestors; + + public readonly ushort[] Elements; + + public TupleTypeDescriptor(ref PacketReader reader, in Guid id) + { + Id = id; + Name = reader.ReadString(); + IsSchemaDefined = reader.ReadBoolean(); + + var ancestorsCount = reader.ReadUInt16(); + var ancestors = new ushort[ancestorsCount]; + + for(var i = 0; i != ancestorsCount; i++) + { + ancestors[i] = reader.ReadUInt16(); + } + + Ancestors = ancestors; + + var elementCount = reader.ReadUInt16(); + var elements = new ushort[elementCount]; + + for (var i = 0; i != elementCount; i++) + { + elements[i] = reader.ReadUInt16(); + } + + Elements = elements; + } + + Guid ITypeDescriptor.Id => Id; + + public CodecMetadata? GetMetadata(RelativeCodecDelegate relativeCodec, RelativeDescriptorDelegate relativeDescriptor) + => new(Name, IsSchemaDefined, IMetadataDescriptor.ConstructAncestors(Ancestors, relativeCodec, relativeDescriptor)); + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/TypeAnnotationTextDescriptor.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/TypeAnnotationTextDescriptor.cs new file mode 100644 index 00000000..d1463cf4 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/Descriptors/TypeAnnotationTextDescriptor.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0.Descriptors +{ + internal readonly struct TypeAnnotationTextDescriptor : ITypeDescriptor + { + public readonly Guid Id; + + public readonly ushort Descriptor; + + public readonly string Key; + + public readonly string Value; + + public TypeAnnotationTextDescriptor(ref PacketReader reader) + { + Id = Guid.Empty; + + Descriptor = reader.ReadUInt16(); + + Key = reader.ReadString(); + Value = reader.ReadString(); + } + + Guid ITypeDescriptor.Id => Id; + } +} diff --git a/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/V2ProtocolProvider.cs b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/V2ProtocolProvider.cs new file mode 100644 index 00000000..b0889d59 --- /dev/null +++ b/src/EdgeDB.Net.Driver/Binary/Protocol/V2.0/V2ProtocolProvider.cs @@ -0,0 +1,178 @@ +using EdgeDB.Binary.Codecs; +using EdgeDB.Binary.Protocol.V1._0; +using EdgeDB.Binary.Protocol.V2._0.Descriptors; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.Binary.Protocol.V2._0 +{ + internal class V2ProtocolProvider : V1ProtocolProvider + { + public override ProtocolVersion Version { get; } = (2, 0); + + public V2ProtocolProvider(EdgeDBBinaryClient client) + : base(client) + { + } + + public override ITypeDescriptor GetDescriptor(ref PacketReader reader) + { + var length = reader.ReadUInt32(); + + reader.Limit = (int)length; + + var type = (DescriptorType)reader.ReadByte(); + + try + { + if (type is DescriptorType.TypeAnnotationText) + { + return new TypeAnnotationTextDescriptor(ref reader); + } + + var id = reader.ReadGuid(); + + return type switch + { + DescriptorType.Array => new ArrayTypeDescriptor(ref reader, in id), + DescriptorType.Compound => new CompoundTypeDescriptor(ref reader, in id), + DescriptorType.Enumeration => new EnumerationTypeDescriptor(ref reader, in id), + DescriptorType.Input => new InputShapeDescriptor(ref reader, in id), + DescriptorType.NamedTuple => new NamedTupleTypeDescriptor(ref reader, in id), + DescriptorType.Object => new ObjectTypeDescriptor(ref reader, in id), + DescriptorType.ObjectOutput => new ObjectOutputShapeDescriptor(ref reader, in id), + DescriptorType.Range => new RangeTypeDescriptor(ref reader, in id), + DescriptorType.Scalar => new ScalarTypeDescriptor(ref reader, in id), + DescriptorType.Set => new SetDescriptor(ref reader, in id), + DescriptorType.Tuple => new TupleTypeDescriptor(ref reader, in id), + _ => throw new InvalidDataException($"No descriptor found for type {type}") + }; + } + finally + { + reader.Limit = -1; + } + } + + public override ICodec? BuildCodec( + in T descriptor, + RelativeCodecDelegate getRelativeCodec, + RelativeDescriptorDelegate getRelativeDescriptor) + { + var metadata = descriptor is IMetadataDescriptor metadataDescriptor + ? metadataDescriptor.GetMetadata(getRelativeCodec, getRelativeDescriptor) + : null; + + switch (descriptor) + { + case ArrayTypeDescriptor array: + { + ref var innerCodec = ref getRelativeCodec(array.Type)!; + + return new CompilableWrappingCodec(descriptor.Id, innerCodec, typeof(ArrayCodec<>), metadata); + } + case CompoundTypeDescriptor compound: + { + var codecs = new ICodec[compound.Components.Length]; + + for(var i = 0; i != compound.Components.Length; i++) + { + codecs[i] = getRelativeCodec(compound.Components[i])!; + } + + return new CompoundCodec(in compound.Id, compound.Operation, codecs, metadata); + } + case EnumerationTypeDescriptor enumeration: + { + return new EnumerationCodec(in enumeration.Id, enumeration.Members, metadata); + } + case InputShapeDescriptor input: + { + var names = new string[input.Elements.Length]; + var codecs = new ICodec[input.Elements.Length]; + + for(var i = 0; i != input.Elements.Length; i++) + { + names[i] = input.Elements[i].Name; + codecs[i] = getRelativeCodec(input.Elements[i].TypePos)!; + } + + return new SparceObjectCodec(in input.Id, codecs, names, metadata); + } + case NamedTupleTypeDescriptor namedTuple: + { + var elements = new ObjectProperty[namedTuple.Elements.Length]; + + for (var i = 0; i != namedTuple.Elements.Length; i++) + { + ref var element = ref namedTuple.Elements[i]; + + elements[i] = new ObjectProperty( + Cardinality.Many, + ref getRelativeCodec(element.TypePos)!, + element.Name + ); + } + + return new ObjectCodec(in namedTuple.Id, elements, metadata); + } + case ObjectTypeDescriptor: + return null; + case ObjectOutputShapeDescriptor objectOutput: + { + if(!objectOutput.IsEphemeralFreeShape) + { + ref var objectDescriptor = ref getRelativeDescriptor(objectOutput.Type); + + if (objectDescriptor is not ObjectTypeDescriptor objectTypeDescriptor) + throw new InvalidOperationException($"Expected ObjectTypeDescriptor but got {objectDescriptor}"); + + metadata = objectTypeDescriptor.GetMetadata(getRelativeCodec, getRelativeDescriptor); + } + + var elements = new ObjectProperty[objectOutput.Elements.Length]; + + for (var i = 0; i != objectOutput.Elements.Length; i++) + { + ref var element = ref objectOutput.Elements[i]; + + elements[i] = new ObjectProperty( + element.Cardinality, + ref getRelativeCodec(element.TypePos)!, + element.Name + ); + } + + return new ObjectCodec(in objectOutput.Id, elements, metadata); + } + case RangeTypeDescriptor range: + { + ref var innerCodec = ref getRelativeCodec(range.Type)!; + + return new CompilableWrappingCodec(in range.Id, innerCodec, typeof(RangeCodec<>), metadata); + } + case ScalarTypeDescriptor scalar: + throw new MissingCodecException($"Could not find the scalar type {scalar.Id}. Please file a bug report with your query that caused this error."); + case SetDescriptor set: + { + ref var innerCodec = ref getRelativeCodec(set.Type)!; + + return new CompilableWrappingCodec(in set.Id, innerCodec, typeof(SetCodec<>), metadata); + } + case TupleTypeDescriptor tuple: + { + var innerCodecs = new ICodec[tuple.Elements.Length]; + for (var i = 0; i != tuple.Elements.Length; i++) + innerCodecs[i] = getRelativeCodec(tuple.Elements[i])!; + + return new TupleCodec(in tuple.Id, innerCodecs, metadata); + } + default: + throw new MissingCodecException($"Could not find a type descriptor with type {descriptor.Id}. Please file a bug report with your query that caused this error."); + } + } + } +} diff --git a/src/EdgeDB.Net.Driver/ClientPoolHolder.cs b/src/EdgeDB.Net.Driver/ClientPoolHolder.cs index c118d016..0c914bcc 100644 --- a/src/EdgeDB.Net.Driver/ClientPoolHolder.cs +++ b/src/EdgeDB.Net.Driver/ClientPoolHolder.cs @@ -66,6 +66,8 @@ public async Task GetPoolHandleAsync(CancellationToken token) } } + public static IDisposable Empty => new PoolHandle(); + private class PoolHandle : IDisposable { public bool HasReleased { get; private set; } diff --git a/src/EdgeDB.Net.Driver/Clients/BaseEdgeDBClient.cs b/src/EdgeDB.Net.Driver/Clients/BaseEdgeDBClient.cs index c9d710fb..6d133d81 100644 --- a/src/EdgeDB.Net.Driver/Clients/BaseEdgeDBClient.cs +++ b/src/EdgeDB.Net.Driver/Clients/BaseEdgeDBClient.cs @@ -94,7 +94,7 @@ public BaseEdgeDBClient WithGlobals(IDictionary globals) return this; } - protected virtual void UpdateTransactionState(TransactionState state) { } + internal virtual void UpdateTransactionState(TransactionState state) { } #endregion #region Connect/Disconnect diff --git a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs index b932a367..0ee864de 100644 --- a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs +++ b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs @@ -1,5 +1,4 @@ using EdgeDB.Binary; -using EdgeDB.Binary.Packets; using EdgeDB.Binary.Codecs; using EdgeDB.Utils; using Microsoft.Extensions.Logging; @@ -12,6 +11,9 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using EdgeDB.Binary.Protocol; +using ProtocolExecuteResult = EdgeDB.Binary.Protocol.ExecuteResult; +using EdgeDB.Binary.Protocol.Common; namespace EdgeDB { @@ -20,35 +22,7 @@ namespace EdgeDB /// internal abstract class EdgeDBBinaryClient : BaseEdgeDBClient { - /// - /// The major version of the protocol that this client supports. - /// - public const int PROTOCOL_MAJOR_VERSION = 1; - - /// - /// The minor version of the protocol that this client supports. - /// - public const int PROTOCOL_MINOR_VERSION = 0; - #region Events - /// - /// Fired when the client receives a . - /// - public event Func OnServerLog - { - add => _onServerLog.Add(value); - remove => _onServerLog.Remove(value); - } - - /// - /// Fired when a query is executed. - /// - public event Func QueryExecuted - { - add => _queryExecuted.Add(value); - remove => _queryExecuted.Remove(value); - } - /// /// Fired when the client disconnects. /// @@ -64,23 +38,22 @@ public event Func QueryExecuted /// public virtual bool IsIdle { get; private set; } = true; - /// - /// Gets the raw server config. - /// - /// - /// This dictionary can be empty if the client hasn't connected to the database. - /// public IReadOnlyDictionary ServerConfig - => RawServerConfig.ToImmutableDictionary(); - + => _protocolProvider.ServerConfig; + internal abstract IBinaryDuplexer Duplexer { get; } + internal virtual IProtocolProvider ProtocolProvider + => _protocolProvider; + internal CodecContext CodecContext => _codecContext; + internal ref Guid StateDescriptorId + => ref _stateDescriptorId; + internal byte[] ServerKey; internal int? SuggestedPoolConcurrency; - internal Dictionary RawServerConfig = new(); internal readonly ILogger Logger; internal readonly TimeSpan MessageTimeout; @@ -93,9 +66,6 @@ internal EdgeDBConfig ClientConfig protected CancellationToken DisconnectCancelToken => Duplexer.DisconnectToken; - private readonly AsyncEvent> _queryExecuted = new(); - private readonly AsyncEvent> _onServerLog = new(); - private ICodec? _stateCodec; private Guid _stateDescriptorId; @@ -107,6 +77,7 @@ protected CancellationToken DisconnectCancelToken private readonly EdgeDBConfig _config; private readonly CodecContext _codecContext; private uint _currentRetries; + private IProtocolProvider _protocolProvider; /// /// Creates a new binary client with the provided conection and config. @@ -135,6 +106,10 @@ public EdgeDBBinaryClient( _readySource = new(); _readyCancelTokenSource = new(); _codecContext = new(this); + + _protocolProvider = IProtocolProvider.GetProvider(this); + + Logger.ClientProtocolInit(_protocolProvider.Version, string.Join(", ", IProtocolProvider.Providers.Keys)); } #region Commands/queries @@ -145,19 +120,13 @@ public async Task SyncAsync(CancellationToken token = default) if (!Duplexer.IsConnected) await ReconnectAsync(token); - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, DisconnectCancelToken); + using var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token, DisconnectCancelToken); - await _semaphore.WaitAsync(linkedTokenSource.Token).ConfigureAwait(false); + await _semaphore.WaitAsync(linkedToken.Token).ConfigureAwait(false); try { - var result = await Duplexer.DuplexSingleAsync(new Sync(), linkedTokenSource.Token).ConfigureAwait(false); - var rfc = result?.ThrowIfErrorOrNot(); - - if(rfc.HasValue) - { - UpdateTransactionState(rfc.Value.TransactionState); - } + await _protocolProvider.SendSyncMessageAsync(linkedToken.Token).ConfigureAwait(false); } finally { @@ -165,23 +134,11 @@ public async Task SyncAsync(CancellationToken token = default) } } - internal readonly struct RawExecuteResult - { - public readonly ICodec Deserializer; - public readonly Data[] Data; - - public RawExecuteResult(ICodec codec, List data) - { - Data = data.ToArray(); - Deserializer = codec; - } - } - /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. - internal virtual async Task ExecuteInternalAsync(string query, IDictionary? args = null, Cardinality? cardinality = null, + internal virtual async Task ExecuteInternalAsync(string query, IDictionary? args = null, Cardinality? cardinality = null, Capabilities? capabilities = Capabilities.Modifications, IOFormat format = IOFormat.Binary, bool isRetry = false, bool implicitTypeName = false, CancellationToken token = default) { @@ -198,177 +155,14 @@ internal virtual async Task ExecuteInternalAsync(string query, IsIdle = false; bool released = false; - ExecuteResult? execResult = null; - ErrorResponse? error = null; - bool gotStateDescriptor = false; - bool successfullyParsed = false; - int parseAttempts = 0; + var arguments = new QueryParameters(query, args, capabilities ?? Capabilities.Modifications, cardinality ?? Cardinality.Many, format, implicitTypeName); try { - var cacheKey = CodecBuilder.GetCacheHashKey(query, cardinality ?? Cardinality.Many, format); - - var stateBuf = SerializeState(); - - if (!CodecBuilder.TryGetCodecs(cacheKey, out var inCodecInfo, out var outCodecInfo)) - { - while (!successfullyParsed) - { - if(parseAttempts > 2) - { - throw error.HasValue - ? new EdgeDBException($"Failed to parse query after {parseAttempts} attempts", new EdgeDBErrorException(error.Value, query)) - : new EdgeDBException($"Failed to parse query after {parseAttempts} attempts"); - } - - await foreach (var result in Duplexer.DuplexAndSyncAsync(new Parse - { - Capabilities = capabilities, - Query = query, - Format = format, - ExpectedCardinality = cardinality ?? Cardinality.Many, - ExplicitObjectIds = _config.ExplicitObjectIds, - StateTypeDescriptorId = _stateDescriptorId, - StateData = stateBuf, - ImplicitLimit = _config.ImplicitLimit, - ImplicitTypeNames = implicitTypeName, // used for type builder - ImplicitTypeIds = _config.ImplicitTypeIds, - }, linkedTokenSource.Token)) - { - switch (result.Packet) - { - case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.StateMismatchError: - error = err; - break; - case ErrorResponse err when err.ErrorCode is ServerErrorCodes.StateMismatchError: - { - // we should have received a state descriptor at this point, - // if we have not, this is a issue with the client implementation - if (!gotStateDescriptor) - { - throw new EdgeDBException("Failed to properly encode state data, this is a bug."); - } - - // we can safely retry by finishing this duplex and starting a - // new one with the 'while' loop. - result.Finish(); - } - break; - case CommandDataDescription descriptor: - { - outCodecInfo = new(descriptor.OutputTypeDescriptorId, - CodecBuilder.BuildCodec(this, descriptor.OutputTypeDescriptorId, descriptor.OutputTypeDescriptorBuffer)); - - inCodecInfo = new(descriptor.InputTypeDescriptorId, - CodecBuilder.BuildCodec(this, descriptor.InputTypeDescriptorId, descriptor.InputTypeDescriptorBuffer)); - - CodecBuilder.UpdateKeyMap(cacheKey, descriptor.InputTypeDescriptorId, descriptor.OutputTypeDescriptorId); - } - break; - case StateDataDescription stateDescriptor: - { - _stateCodec = CodecBuilder.BuildCodec(this, stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); - _stateDescriptorId = stateDescriptor.TypeDescriptorId; - gotStateDescriptor = true; - stateBuf = SerializeState(); - } - break; - case ReadyForCommand ready: - UpdateTransactionState(ready.TransactionState); - result.Finish(); - successfullyParsed = true; - break; - default: - break; - } - } - - parseAttempts++; - } - - - if (error.HasValue) - throw new EdgeDBErrorException(error.Value, query); - - if (outCodecInfo is null) - throw new MissingCodecException("Couldn't find a valid output codec"); - - if (inCodecInfo is null) - throw new MissingCodecException("Couldn't find a valid input codec"); - } - - if (inCodecInfo.Codec is not IArgumentCodec argumentCodec) - throw new MissingCodecException($"Cannot encode arguments, {inCodecInfo.Codec} is not a registered argument codec"); - - List receivedData = new(); - bool executeSuccess = false; - - do - { - await foreach (var result in Duplexer.DuplexAndSyncAsync(new Execute() - { - Capabilities = capabilities, - Query = query, - Format = format, - ExpectedCardinality = cardinality ?? Cardinality.Many, - ExplicitObjectIds = _config.ExplicitObjectIds, - StateTypeDescriptorId = _stateDescriptorId, - StateData = stateBuf, - ImplicitTypeNames = implicitTypeName, // used for type builder - ImplicitTypeIds = _config.ImplicitTypeIds, - Arguments = argumentCodec.SerializeArguments(this, args), - ImplicitLimit = _config.ImplicitLimit, - InputTypeDescriptorId = inCodecInfo.Id, - OutputTypeDescriptorId = outCodecInfo.Id, - }, linkedTokenSource.Token)) - { - switch (result.Packet) - { - case Data data: - receivedData.Add(data); - break; - case StateDataDescription stateDescriptor: - { - _stateCodec = CodecBuilder.BuildCodec(this, stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); - _stateDescriptorId = stateDescriptor.TypeDescriptorId; - gotStateDescriptor = true; - stateBuf = SerializeState(); - } - break; - case ErrorResponse err when err.ErrorCode is ServerErrorCodes.StateMismatchError: - { - // we should have received a state descriptor at this point, - // if we have not, this is a issue with the client implementation - if (!gotStateDescriptor) - { - throw new EdgeDBException("Failed to properly encode state data, this is a bug."); - } - - // we can safely retry by finishing this duplex and starting a - // new one with the 'while' loop. - result.Finish(); - } - break; - case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.ParameterTypeMismatchError: - error = err; - break; - case ReadyForCommand ready: - UpdateTransactionState(ready.TransactionState); - result.Finish(); - executeSuccess = true; - break; - } - } - } - while (!successfullyParsed && !executeSuccess); + var parseResult = await _protocolProvider.ParseQueryAsync(arguments, linkedTokenSource.Token); - if (error.HasValue) - throw new EdgeDBErrorException(error.Value, query); - - execResult = new ExecuteResult(true, null, query); - - return new RawExecuteResult(outCodecInfo.Codec!, receivedData); + return await _protocolProvider.ExecuteQueryAsync(arguments, parseResult, linkedTokenSource.Token); } catch (OperationCanceledException ce) { @@ -399,10 +193,6 @@ internal virtual async Task ExecuteInternalAsync(string query, } catch (Exception x) { - execResult = x is EdgeDBErrorException err - ? new ExecuteResult(false, err, query) - : new ExecuteResult(false, x, query); - Logger.InternalExecuteFailed(x); if (x is EdgeDBErrorException) @@ -412,8 +202,6 @@ internal virtual async Task ExecuteInternalAsync(string query, } finally { - if(execResult.HasValue) - _ = Task.Run(async () => await _queryExecuted.InvokeAsync(execResult!.Value).ConfigureAwait(false), token); IsIdle = true; if(!released) _semaphore.Release(); } @@ -421,7 +209,7 @@ internal virtual async Task ExecuteInternalAsync(string query, /// /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. public override async Task ExecuteAsync(string query, IDictionary? args = null, @@ -430,7 +218,7 @@ public override async Task ExecuteAsync(string query, IDictionary /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. /// Target type doesn't match received type. @@ -451,9 +239,11 @@ public override async Task ExecuteAsync(string query, IDictionary(this, result.Deserializer, ref result.Data[i]); + var obj = ObjectBuilder.BuildResult(this, codec, in result.Data[i]); array[i] = obj; } @@ -462,7 +252,7 @@ public override async Task ExecuteAsync(string query, IDictionary /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. /// The results cardinality was not what the query expected. @@ -485,16 +275,14 @@ public override async Task ExecuteAsync(string query, IDictionary 1) throw new ResultCardinalityMismatchException(Cardinality.AtMostOne, Cardinality.Many); - var queryResult = result.Data.FirstOrDefault(); - - return queryResult.PayloadBuffer is null + return result.Data.Length == 0 ? default - : ObjectBuilder.BuildResult(this, result.Deserializer, ref result.Data[0]); + : ObjectBuilder.BuildResult(this, result.OutCodecInfo.Codec, in result.Data[0]); } /// /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. /// The results cardinality was not what the query expected. @@ -517,17 +305,16 @@ public override async Task QueryRequiredSingleAsync(string que if (result.Data.Length is > 1 or 0) throw new ResultCardinalityMismatchException(Cardinality.One, result.Data.Length > 1 ? Cardinality.Many : Cardinality.AtMostOne); - var queryResult = result.Data.FirstOrDefault(); - return queryResult.PayloadBuffer is null + return result.Data.Length != 1 ? throw new MissingRequiredException() - : ObjectBuilder.BuildResult(this, result.Deserializer, ref result.Data[0])!; + : ObjectBuilder.BuildResult(this, result.OutCodecInfo.Codec, in result.Data[0])!; } /// /// The results cardinality was not what the query expected. /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. public override async Task QueryJsonAsync(string query, IDictionary? args = null, Capabilities? capabilities = Capabilities.Modifications, CancellationToken token = default) @@ -535,13 +322,13 @@ public override async Task QueryRequiredSingleAsync(string que var result = await ExecuteInternalAsync(query, args, Cardinality.Many, capabilities, IOFormat.Json, token: token); return result.Data.Length == 1 - ? (string)result.Deserializer.Deserialize(this, result.Data[0].PayloadBuffer)! + ? (string)result.OutCodecInfo.Codec.Deserialize(this, in result.Data[0])! : "[]"; } /// /// A general error occored. - /// The client received an . + /// The client received an . /// The client received an unexpected message. /// A codec could not be found for the given input arguments or the result. public override async Task> QueryJsonElementsAsync(string query, IDictionary? args = null, Capabilities? capabilities = Capabilities.Modifications, CancellationToken token = default) @@ -549,187 +336,59 @@ public override async Task QueryRequiredSingleAsync(string que var result = await ExecuteInternalAsync(query, args, Cardinality.Many, capabilities, IOFormat.JsonElements, token: token); return result.Data.Any() - ? result.Data.Select(x => new DataTypes.Json((string?)result.Deserializer.Deserialize(this, x.PayloadBuffer))).ToImmutableArray() + ? result.Data.Select(x => new DataTypes.Json((string?)result.OutCodecInfo.Codec.Deserialize(this, in x))).ToImmutableArray() : ImmutableArray.Empty; } #endregion - #region Packet handling - private async ValueTask HandlePacketAsync(IReceiveable payload) + #region Helper functions + internal ValueTask OnLogAsync(MessageSeverity severity, ServerErrorCodes code, string message) { - switch (payload) + switch (severity) { - case ServerHandshake handshake: - if (handshake.MajorVersion != PROTOCOL_MAJOR_VERSION || handshake.MinorVersion < PROTOCOL_MINOR_VERSION) - { - Logger.ProtocolMajorMismatch($"{handshake.MajorVersion}.{handshake.MinorVersion}", $"{PROTOCOL_MAJOR_VERSION}.{PROTOCOL_MINOR_VERSION}"); - await DisconnectAsync().ConfigureAwait(false); - } - else if (handshake.MajorVersion == PROTOCOL_MAJOR_VERSION && handshake.MinorVersion > PROTOCOL_MINOR_VERSION) - Logger.ProtocolMinorMismatch($"{handshake.MajorVersion}.{handshake.MinorVersion}", $"{PROTOCOL_MAJOR_VERSION}.{PROTOCOL_MINOR_VERSION}"); - break; - case ErrorResponse err: - Logger.ErrorResponseReceived(err.Severity, err.Message); - if (!_readyCancelTokenSource.IsCancellationRequested) - _readyCancelTokenSource.Cancel(); - break; - case AuthenticationStatus authStatus: - if (authStatus.AuthStatus == AuthStatus.AuthenticationRequiredSASLMessage) - await StartSASLAuthenticationAsync(authStatus).ConfigureAwait(false); - else if(authStatus.AuthStatus != AuthStatus.AuthenticationOK) - throw new UnexpectedMessageException("Expected AuthenticationRequiredSASLMessage, got " + authStatus.AuthStatus); + case MessageSeverity.Warning: + Logger.LogWarning("[SERVER: {@Code}]: {@Message}", code, message); break; - case ServerKeyData keyData: - ServerKey = keyData.KeyBuffer; + case MessageSeverity.Debug: + Logger.LogDebug("[SERVER: {@Code}]: {@Message}", code, message); break; - case StateDataDescription stateDescriptor: - _stateCodec = CodecBuilder.BuildCodec(this, stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); - _stateDescriptorId = stateDescriptor.TypeDescriptorId; + case MessageSeverity.Info: + Logger.LogInformation("[SERVER: {@Code}]: {@Message}", code, message); break; - case ParameterStatus parameterStatus: - ParseServerSettings(parameterStatus); - break; - case LogMessage log: - try - { - await _onServerLog.InvokeAsync(log).ConfigureAwait(false); - } - catch(Exception x) - { - Logger.EventHandlerError(x); - } - break; - default: + case MessageSeverity.Notice: + Logger.LogInformation("[SERVER NOTICE: {@Code}]: {@Message}", code, message); break; } + + return ValueTask.CompletedTask; } - #endregion - #region SASL - private async Task StartSASLAuthenticationAsync(AuthenticationStatus authStatus) + internal bool TryNegotiateProtocol(in ushort major, in ushort minor) { - IsIdle = false; + Logger.BeginProtocolNegotiation(_protocolProvider.Version, (major, minor)); - try + if(IProtocolProvider.Providers.TryGetValue((major, minor), out var provider)) { - using var scram = new Scram(); - - var method = authStatus.AuthenticationMethods![0]; - - if (method is not "SCRAM-SHA-256") - { - throw new ProtocolViolationException("The only supported method is SCRAM-SHA-256"); - } - - var initialMsg = scram.BuildInitialMessagePacket(this, Connection.Username!, method); - byte[] expectedSig = Array.Empty(); - - await foreach(var result in Duplexer.DuplexAsync(initialMsg)) - { - switch (result.Packet) - { - case AuthenticationStatus authResult: - { - switch (authResult.AuthStatus) - { - case AuthStatus.AuthenticationSASLContinue: - { - var (msg, sig) = scram.BuildFinalMessagePacket(this, in authResult, Connection.Password!); - expectedSig = sig; - - await Duplexer.SendAsync(packets: msg).ConfigureAwait(false); - } - break; - case AuthStatus.AuthenticationSASLFinal: - { - var key = Scram.ParseServerFinalMessage(this, in authResult); - - if (!key.SequenceEqual(expectedSig)) - { - throw new InvalidSignatureException(); - } - } - break; - case AuthStatus.AuthenticationOK: - { - _currentRetries = 0; - result.Finish(); - } - break; - default: - throw new UnexpectedMessageException($"Expected coninue or final but got {authResult.AuthStatus}"); - } - } - break; - case ErrorResponse err: - throw new EdgeDBErrorException(err); - } - } - } - finally - { - IsIdle = true; + _protocolProvider = provider.Factory(this); + IProtocolProvider.UpdateProviderFor(this, _protocolProvider); + return true; } + + return false; } - #endregion - #region Helper functions - protected void TriggerReady() + internal void CancelReadyState() { - _readySource.TrySetResult(); + if (!_readyCancelTokenSource.IsCancellationRequested) + _readyCancelTokenSource.Cancel(); } - private void ParseServerSettings(ParameterStatus status) + protected void TriggerReady() { - try - { - switch (status.Name) - { - case "suggested_pool_concurrency": - var str = Encoding.UTF8.GetString(status.ValueBuffer); - if (!int.TryParse(str, out var suggestedPoolConcurrency)) - { - throw new FormatException("suggested_pool_concurrency type didn't match the expected type of int"); - } - SuggestedPoolConcurrency = suggestedPoolConcurrency; - break; - - case "system_config": - var reader = new PacketReader(status.ValueBuffer); - var length = reader.ReadInt32() - 16; - var descriptorId = reader.ReadGuid(); - reader.ReadBytes(length, out var typeDesc); - - var codec = CodecBuilder.GetCodec(descriptorId); - - if (codec is null) - { - var innerReader = new PacketReader(ref typeDesc); - codec = CodecBuilder.BuildCodec(this, descriptorId, ref innerReader); - - if (codec is null) - throw new MissingCodecException("Failed to build codec for system_config"); - } - - // disard length - reader.Skip(4); - - var obj = codec.Deserialize(ref reader, _codecContext)!; - - RawServerConfig = ((ExpandoObject)obj).ToDictionary(x => x.Key, x => x.Value); - break; - - default: - break; - } - } - catch (Exception x) - { - Logger.ServerSettingsParseFailed(x); - } + _readySource.TrySetResult(); } - private ReadOnlyMemory? SerializeState() + internal ReadOnlyMemory? SerializeState() { // TODO: version check this, prevent the state codec // from being walked again if the state data hasn't @@ -742,6 +401,13 @@ private void ParseServerSettings(ParameterStatus status) return _stateCodec.Serialize(this, data); } + + internal void UpdateStateCodec(ICodec codec, Guid stateCodecId) + { + _stateCodec = codec; + _stateDescriptorId = stateCodecId; + } + #endregion #region Connect/disconnect @@ -750,13 +416,16 @@ private void ParseServerSettings(ParameterStatus status) /// /// /// This task waits for the underlying connection to receive a - /// message indicating the client + /// ready message indicating the client /// can start to preform queries. /// /// public override async ValueTask ConnectAsync(CancellationToken token = default) { - await _connectSemaphone.WaitAsync(token).ConfigureAwait(false); + if (IsConnected) + return; + + await _connectSemaphone.WaitAsync(token); if (IsConnected) return; @@ -765,6 +434,9 @@ public override async ValueTask ConnectAsync(CancellationToken token = default) try { + if (IsConnected) + return; + await ConnectInternalAsync(token: token); using var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token, _readyCancelTokenSource.Token); @@ -774,23 +446,13 @@ public override async ValueTask ConnectAsync(CancellationToken token = default) // run a message loop until the client is ready for commands while (!linkedToken.IsCancellationRequested) { - var message = await Duplexer.ReadNextAsync(linkedToken.Token).ConfigureAwait(false); - - if (message is null) - throw new UnexpectedDisconnectException(); - - if (message is ReadyForCommand) - { - // reset connection attempts - _currentRetries = 0; - break; - } + var message = await Duplexer.ReadNextAsync(linkedToken.Token).ConfigureAwait(false) ?? throw new UnexpectedDisconnectException(); try { - await HandlePacketAsync(message).ConfigureAwait(false); + await _protocolProvider.ProcessAsync(in message); } - catch(EdgeDBErrorException x) when (x.ShouldReconnect) + catch (EdgeDBErrorException x) when (x.ShouldReconnect) { if (_config.RetryMode is ConnectionRetryMode.AlwaysRetry) { @@ -820,6 +482,13 @@ public override async ValueTask ConnectAsync(CancellationToken token = default) throw; } + + if (_protocolProvider.Phase is ProtocolPhase.Command) + { + // reset connection attempts + _currentRetries = 0; + break; + } } _readySource.SetResult(); @@ -868,46 +537,9 @@ private async Task ConnectInternalAsync(int attempts = 0, CancellationToken toke if(Duplexer is StreamDuplexer streamDuplexer) streamDuplexer.Init(stream); - var connParams = Connection.SecretKey is not null - ? new ConnectionParam[] - { - new ConnectionParam - { - Name = "user", - Value = Connection.Username! - }, - new ConnectionParam - { - Name = "database", - Value = Connection.Database! - }, - new ConnectionParam - { - Name = "secret_key", - Value = Connection.SecretKey - } - } - : new ConnectionParam[] - { - new ConnectionParam - { - Name = "user", - Value = Connection.Username! - }, - new ConnectionParam - { - Name = "database", - Value = Connection.Database! - }, - }; - + // send handshake - await Duplexer.SendAsync(token, new ClientHandshake - { - MajorVersion = PROTOCOL_MAJOR_VERSION, - MinorVersion = PROTOCOL_MINOR_VERSION, - ConnectionParameters = connParams - }).ConfigureAwait(false); + await Duplexer.SendAsync(token, _protocolProvider.Handshake()).ConfigureAwait(false); } catch (EdgeDBException x) when (x.ShouldReconnect) { diff --git a/src/EdgeDB.Net.Driver/Clients/EdgeDBHttpClient.cs b/src/EdgeDB.Net.Driver/Clients/EdgeDBHttpClient.cs index 4b777f20..596d852e 100644 --- a/src/EdgeDB.Net.Driver/Clients/EdgeDBHttpClient.cs +++ b/src/EdgeDB.Net.Driver/Clients/EdgeDBHttpClient.cs @@ -51,6 +51,9 @@ internal EdgeDBHttpClient(EdgeDBConnection connection, EdgeDBConfig config, IDis private async Task AuthenticateAsync() { + if (Connection.Password is null) + throw new ConfigurationException("A password is required for HTTP authentication"); + using var scram = new Scram(); var first = scram.BuildInitialMessage(Connection.Username); @@ -72,7 +75,7 @@ private async Task AuthenticateAsync() var keys = ParseKeys(authenticate); - var (final, sig) = scram.BuildFinalMessage(Encoding.UTF8.GetString(Convert.FromBase64String(keys["data"])), Connection.Password!); + var (final, sig) = scram.BuildFinalMessage(Encoding.UTF8.GetString(Convert.FromBase64String(keys["data"])), Connection.Password); var finalMessage = new HttpRequestMessage(HttpMethod.Get, Connection.GetAuthUri()); diff --git a/src/EdgeDB.Net.Driver/Clients/EdgeDBTcpClient.cs b/src/EdgeDB.Net.Driver/Clients/EdgeDBTcpClient.cs index eea5083c..d7d082a0 100644 --- a/src/EdgeDB.Net.Driver/Clients/EdgeDBTcpClient.cs +++ b/src/EdgeDB.Net.Driver/Clients/EdgeDBTcpClient.cs @@ -121,7 +121,7 @@ protected override ValueTask CloseStreamAsync(CancellationToken token = default) return ValueTask.CompletedTask; } - protected override void UpdateTransactionState(TransactionState state) + internal override void UpdateTransactionState(TransactionState state) { TransactionState = state; } diff --git a/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj b/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj index b70445d6..2bc7939d 100644 --- a/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj +++ b/src/EdgeDB.Net.Driver/EdgeDB.Net.Driver.csproj @@ -14,6 +14,11 @@ true True + + + + + diff --git a/src/EdgeDB.Net.Driver/EdgeDBClient.cs b/src/EdgeDB.Net.Driver/EdgeDBClient.cs index 6ddc4ccc..d1116a44 100644 --- a/src/EdgeDB.Net.Driver/EdgeDBClient.cs +++ b/src/EdgeDB.Net.Driver/EdgeDBClient.cs @@ -13,10 +13,11 @@ public sealed class EdgeDBClient : IEdgeDBQueryable, IAsyncDisposable /// /// Fired when a client in the client pool executes a query. /// + [Obsolete("This event will no longer be triggered by the binding, and will be removed in a later version.")] public event Func QueryExecuted { - add => _queryExecuted.Add(value); - remove => _queryExecuted.Remove(value); + add { } + remove { } } /// @@ -41,8 +42,8 @@ public int ConnectedClients public int AvailableClients => _availableClients.Count(x => { - if (x is EdgeDBBinaryClient binaryCliet) - return binaryCliet.IsIdle; + if (x is EdgeDBBinaryClient binaryClient) + return binaryClient.IsIdle; return x.IsConnected; }); @@ -78,12 +79,11 @@ public IReadOnlyDictionary Aliases /// or the clients don't support getting a server config. /// public IReadOnlyDictionary ServerConfig - => _edgedbConfig.ToImmutableDictionary(); + => _edgedbConfig; internal EdgeDBClientType ClientType => _poolConfig.ClientType; - private readonly AsyncEvent> _queryExecuted = new(); private readonly EdgeDBConnection _connection; private readonly EdgeDBClientPoolConfig _poolConfig; private readonly ConcurrentDictionary _clients; @@ -93,7 +93,7 @@ internal EdgeDBClientType ClientType private readonly Session _session; private readonly Func>? _clientFactory; - private Dictionary _edgedbConfig; + private IReadOnlyDictionary _edgedbConfig; private ConcurrentStack _availableClients; private int _poolSize; private ulong _clientIndex; @@ -369,9 +369,9 @@ async ValueTask OnConnect(BaseEdgeDBClient _) if ( _edgedbConfig is null || - (_edgedbConfig.Count != client.RawServerConfig.Count || _edgedbConfig.Except(client.RawServerConfig).Any())) + (_edgedbConfig.Count != client.ServerConfig.Count || _edgedbConfig.Except(client.ServerConfig).Any())) { - _edgedbConfig = client.RawServerConfig; + _edgedbConfig = client.ServerConfig; } client.OnConnect -= OnConnect; @@ -379,8 +379,6 @@ _edgedbConfig is null || client.OnConnect += OnConnect; - client.QueryExecuted += (i) => _queryExecuted.InvokeAsync(i); - client.OnDisposed += (c) => { // reset state @@ -402,9 +400,9 @@ _edgedbConfig is null || if ( _edgedbConfig is null || - (_edgedbConfig.Count != client.RawServerConfig.Count || _edgedbConfig.Except(client.RawServerConfig).Any())) + (_edgedbConfig.Count != client.ServerConfig.Count || _edgedbConfig.Except(client.ServerConfig).Any())) { - _edgedbConfig = client.RawServerConfig; + _edgedbConfig = client.ServerConfig; } client.OnDisposed += (c) => diff --git a/src/EdgeDB.Net.Driver/Extensions/ClientPoolExtensions.cs b/src/EdgeDB.Net.Driver/Extensions/ClientPoolExtensions.cs index 3cffa14a..d6fb59f2 100644 --- a/src/EdgeDB.Net.Driver/Extensions/ClientPoolExtensions.cs +++ b/src/EdgeDB.Net.Driver/Extensions/ClientPoolExtensions.cs @@ -1,5 +1,3 @@ -using EdgeDB.Binary.Packets; - namespace EdgeDB { public static class ClientPoolExtensions diff --git a/src/EdgeDB.Net.Driver/Extensions/CodecExtensions.cs b/src/EdgeDB.Net.Driver/Extensions/CodecExtensions.cs index 33269403..c1f5d648 100644 --- a/src/EdgeDB.Net.Driver/Extensions/CodecExtensions.cs +++ b/src/EdgeDB.Net.Driver/Extensions/CodecExtensions.cs @@ -6,13 +6,13 @@ namespace EdgeDB internal static class CodecExtensions { #region ICodec - public static object? Deserialize(this ICodec codec, EdgeDBBinaryClient client, Span buffer) + public static object? Deserialize(this ICodec codec, EdgeDBBinaryClient client, in ReadOnlySpan buffer) { var reader = new PacketReader(buffer); return codec.Deserialize(ref reader, client.CodecContext); } - public static object? Deserialize(this ICodec codec, CodecContext context, Span buffer) + public static object? Deserialize(this ICodec codec, CodecContext context, in ReadOnlySpan buffer) { var reader = new PacketReader(buffer); return codec.Deserialize(ref reader, context); @@ -24,6 +24,13 @@ internal static class CodecExtensions return codec.Deserialize(ref reader, client.CodecContext); } + public static object? Deserialize(this ICodec codec, EdgeDBBinaryClient client, in ReadOnlyMemory buffer) + { + var reader = new PacketReader(buffer.Span); + return codec.Deserialize(ref reader, client.CodecContext); + } + + public static ReadOnlyMemory Serialize(this ICodec codec, EdgeDBBinaryClient client, object? value) { @@ -40,13 +47,19 @@ public static ReadOnlyMemory Serialize(this ICodec codec, EdgeDBBinaryClie return codec.Deserialize(ref reader, client.CodecContext); } - public static T? Deserialize(this ICodec codec, EdgeDBBinaryClient client, Span buffer) + public static T? Deserialize(this ICodec codec, EdgeDBBinaryClient client, in ReadOnlySpan buffer) { var reader = new PacketReader(buffer); return codec.Deserialize(ref reader, client.CodecContext); } - public static T? Deserialize(this ICodec codec, CodecContext context, Span buffer) + public static T? Deserialize(this ICodec codec, EdgeDBBinaryClient client, in ReadOnlyMemory buffer) + { + var reader = new PacketReader(buffer.Span); + return codec.Deserialize(ref reader, client.CodecContext); + } + + public static T? Deserialize(this ICodec codec, CodecContext context, in ReadOnlySpan buffer) { var reader = new PacketReader(buffer); return codec.Deserialize(ref reader, context); diff --git a/src/EdgeDB.Net.Driver/Extensions/EdgeDBClientExtensions.cs b/src/EdgeDB.Net.Driver/Extensions/EdgeDBClientExtensions.cs index ab6aca3a..4a57f6fd 100644 --- a/src/EdgeDB.Net.Driver/Extensions/EdgeDBClientExtensions.cs +++ b/src/EdgeDB.Net.Driver/Extensions/EdgeDBClientExtensions.cs @@ -1,6 +1,7 @@ using EdgeDB.Binary; -using EdgeDB.Binary.Packets; -using EdgeDB.Dumps; +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.DumpRestore; +using Newtonsoft.Json.Linq; using System.Runtime.InteropServices; namespace EdgeDB @@ -249,69 +250,56 @@ internal static async Task TransactionInternalAsync(ITransactibleClient client, /// Dumps the current database to a stream. /// /// The client to preform the dump with. + /// /// A token to cancel the operation with. - /// A stream containing the entire dumped database. + /// A memory stream containing the entire dumped database. /// The server sent an error message during the dumping process. /// The server sent a mismatched packet. - public static async Task DumpDatabaseAsync(this EdgeDBClient pool, CancellationToken token = default) + public static async Task DumpDatabaseAsync( + this EdgeDBClient pool, + ProtocolVersion? dumprestoreVersion = null, + CancellationToken token = default) { - await using var client = await pool.GetOrCreateClientAsync(token).ConfigureAwait(false); - using var cmdLock = await client.AquireCommandLockAsync(token).ConfigureAwait(false); - - try - { - var stream = new MemoryStream(); - - DumpHeader header = default; - List blocks = new(); - - await foreach(var result in client.Duplexer.DuplexAndSyncAsync(new Dump(), token)) - { - switch (result.Packet) - { - case ReadyForCommand: - result.Finish(); - break; - case DumpHeader dumpHeader: - header = dumpHeader; - break; - case DumpBlock block: - { - blocks.Add(block); - } - break; - case ErrorResponse error: - { - throw new EdgeDBErrorException(error); - } - } - } - - WriteDumpDataToStream(stream, ref header, blocks); + var ms = new MemoryStream(); + await DumpDatabaseAsync(pool, ms, dumprestoreVersion, token); + return ms; + } - stream.Position = 0; - return stream; - } - catch (Exception x) when (x is OperationCanceledException or TaskCanceledException) + /// + /// Dumps the database to a stream. + /// + /// The client to preform the dump with. + /// The stream to write the dump to. + /// The version of the dump format to use. + /// A token to cancel the operation with. + /// A memory stream containing the entire dumped database. + /// The server sent an error message during the dumping process. + /// The server sent a mismatched packet. + /// The provided stream cannot be written to. + public static async Task DumpDatabaseAsync( + this EdgeDBClient pool, + Stream stream, + ProtocolVersion? dumprestoreVersion = null, + CancellationToken token = default) + { + if(!stream.CanWrite) { - throw new TimeoutException("Database dump timed out", x); + throw new ArgumentException("Cannot write to stream"); } - } - private static void WriteDumpDataToStream(Stream stream, ref DumpHeader header, List blocks) - { - var writer = new DumpWriter(); - writer.WriteDumpHeader(header); - writer.WriteDumpBlocks(blocks); + await using var client = await pool.GetOrCreateClientAsync(token).ConfigureAwait(false); - stream.Write(writer.Collect().Span); + var dumprestoreProvider = IDumpRestoreProvider.GetProvider(client, dumprestoreVersion); + + await dumprestoreProvider.DumpDatabaseAsync(client, stream, token); } /// /// Restores the database based on a database dump stream. /// - /// The TCP client to preform the restore with. + /// The client to preform the restore with. /// The stream containing the database dump. + /// The version of the dump format to use. /// A token to cancel the operation with. /// The status result of the restore. /// @@ -319,49 +307,22 @@ private static void WriteDumpDataToStream(Stream stream, ref DumpHeader header, /// due to the database not being empty. /// /// The server sent an error during the restore operation. - public static async Task RestoreDatabaseAsync(this EdgeDBClient pool, Stream stream, CancellationToken token = default) + public static async Task RestoreDatabaseAsync( + this EdgeDBClient pool, + Stream stream, + ProtocolVersion? dumprestoreVersion = null, + CancellationToken token = default) { await using var client = await pool.GetOrCreateClientAsync(token).ConfigureAwait(false); - using var cmdLock = await client.AquireCommandLockAsync(token).ConfigureAwait(false); - - var reader = new DumpReader(); - var count = await client.QueryRequiredSingleAsync("select count(schema::Module filter not .builtin and not .name = \"default\") + count(schema::Object filter .name like \"default::%\")", token: token).ConfigureAwait(false); - - if (count > 0) - throw new InvalidOperationException("Cannot restore: Database isn't empty"); - - var packets = DumpReader.ReadDatabaseDump(stream); - - await foreach(var result in client.Duplexer.DuplexAsync(packets.Restore, token)) + if(!stream.CanRead) { - switch (result.Packet) - { - case ErrorResponse err: - throw new EdgeDBErrorException(err); - case RestoreReady: - result.Finish(); - break; - default: - throw new UnexpectedMessageException(ServerMessageType.RestoreReady, result.Packet.Type); - } + throw new ArgumentException("Cannot read from the provided stream"); } - - foreach (var block in packets.Blocks) - { - await client.Duplexer.SendAsync(block, token).ConfigureAwait(false); - } - - var restoreResult = await client.Duplexer.DuplexSingleAsync(new RestoreEOF(), token).ConfigureAwait(false); - if (restoreResult is null) - throw new UnexpectedDisconnectException(); + var dumprestoreProvider = IDumpRestoreProvider.GetProvider(client, dumprestoreVersion); - return restoreResult is ErrorResponse error - ? throw new EdgeDBErrorException(error) - : restoreResult is not CommandComplete complete - ? throw new UnexpectedMessageException(ServerMessageType.CommandComplete, restoreResult.Type) - : complete.Status; + return await dumprestoreProvider.RestoreDatabaseAsync(client, stream, token); } #endregion } diff --git a/src/EdgeDB.Net.Driver/Extensions/ReceivableExtensions.cs b/src/EdgeDB.Net.Driver/Extensions/ReceivableExtensions.cs index 67f578b1..a9dee9cc 100644 --- a/src/EdgeDB.Net.Driver/Extensions/ReceivableExtensions.cs +++ b/src/EdgeDB.Net.Driver/Extensions/ReceivableExtensions.cs @@ -1,5 +1,5 @@ -using EdgeDB.Binary; -using EdgeDB.Binary.Packets; +using EdgeDB.Binary.Protocol; +using EdgeDB.Binary.Protocol.Common; using System; using System.Collections.Generic; using System.Linq; @@ -12,14 +12,14 @@ internal static class ReceivableExtensions { public static void ThrowIfErrrorResponse(this IReceiveable packet, string? query = null) { - if (packet is ErrorResponse err) + if (packet is IProtocolError err) throw new EdgeDBErrorException(err, query); } public static TPacket ThrowIfErrorOrNot(this IReceiveable packet) where TPacket : IReceiveable, new() { - if (packet is ErrorResponse err) + if (packet is IProtocolError err) throw new EdgeDBErrorException(err); if (packet is not TPacket p) diff --git a/src/EdgeDB.Net.Driver/Log.cs b/src/EdgeDB.Net.Driver/Log.cs index 027cfdaa..96272b30 100644 --- a/src/EdgeDB.Net.Driver/Log.cs +++ b/src/EdgeDB.Net.Driver/Log.cs @@ -112,14 +112,14 @@ internal static partial class Log [LoggerMessage( 16, LogLevel.Trace, - "{pos}: Read type descriptor {Descriptor} with UUID {ID}. Size {Size}")] - public static partial void TraceTypeDescriptor(this ILogger logger, ITypeDescriptor descriptor, Guid id, int size, string pos); + "{pos}: {ID} | {Size}: {Descriptor}")] + public static partial void TraceTypeDescriptor(this ILogger logger, string descriptor, Guid id, string size, string pos); [LoggerMessage( 17, LogLevel.Trace, - "Codec built: {Final} with tree size of {TreeSize}. Final cache size: {CacheSize}")] - public static partial void TraceCodecBuilderResult(this ILogger logger, ICodec final, int treeSize, int cacheSize); + "Codec built with tree size of {TreeSize}. Final cache size: {CacheSize}:\n{Final}")] + public static partial void TraceCodecBuilderResult(this ILogger logger, string final, int treeSize, int cacheSize); [LoggerMessage( 18, @@ -194,5 +194,23 @@ internal static partial class Log LogLevel.Trace, "Codec {Id} added to cache with description {Codec}")] public static partial void CodecAddedToCache(this ILogger logger, Guid id, ICodec codec); + + [LoggerMessage( + 30, + LogLevel.Debug, + "Server asked for negotiation, ours: {Current} - theirs: {Server}")] + public static partial void BeginProtocolNegotiation(this ILogger logget, ProtocolVersion current, ProtocolVersion server); + + [LoggerMessage( + 31, + LogLevel.Debug, + "Binary protocol {Protocol} used, avaliable: [{Supported}]")] + public static partial void ClientProtocolInit(this ILogger logger, ProtocolVersion protocol, string supported); + + [LoggerMessage( + 32, + LogLevel.Debug, + "Codec visited in preperation for deserialization: \n{Codec}")] + public static partial void ObjectDeserializationPrep(this ILogger logger, string codec); } } diff --git a/src/EdgeDB.Net.Driver/Models/DataTypes/TransientTuple.cs b/src/EdgeDB.Net.Driver/Models/DataTypes/TransientTuple.cs index 4830f417..a94c430e 100644 --- a/src/EdgeDB.Net.Driver/Models/DataTypes/TransientTuple.cs +++ b/src/EdgeDB.Net.Driver/Models/DataTypes/TransientTuple.cs @@ -175,7 +175,7 @@ public static TransientTuple FromObjectEnumerator(ref ObjectEnumerator enumerato for(int i = 0; enumerator.Next(out _, out var value); i++) { - types[i] = value?.GetType() ?? enumerator.Codecs[i].ConverterType; + types[i] = value?.GetType() ?? enumerator.Properties[i].Codec.ConverterType; values[i] = value; } diff --git a/src/EdgeDB.Net.Driver/Models/ErrorSeverity.cs b/src/EdgeDB.Net.Driver/Models/ErrorSeverity.cs index 57d17c41..23f93b93 100644 --- a/src/EdgeDB.Net.Driver/Models/ErrorSeverity.cs +++ b/src/EdgeDB.Net.Driver/Models/ErrorSeverity.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -7,7 +7,7 @@ namespace EdgeDB { /// - /// An enum representing the error severity of a . + /// An enum representing the error severity of a . /// public enum ErrorSeverity : byte { diff --git a/src/EdgeDB.Net.Driver/Models/Exceptions/EdgeDBErrorException.cs b/src/EdgeDB.Net.Driver/Models/Exceptions/EdgeDBErrorException.cs index 975ca14b..53c6ec0d 100644 --- a/src/EdgeDB.Net.Driver/Models/Exceptions/EdgeDBErrorException.cs +++ b/src/EdgeDB.Net.Driver/Models/Exceptions/EdgeDBErrorException.cs @@ -1,4 +1,4 @@ -using EdgeDB.Binary.Packets; +using EdgeDB.Binary.Protocol.Common; using System; using System.Collections.Generic; using System.Diagnostics; @@ -17,6 +17,9 @@ public sealed class EdgeDBErrorException : EdgeDBException private const ushort ERROR_LINE_END = 0xFFF6; private const ushort ERROR_UTF16COLUMN_START = 0xFFF5; private const ushort ERROR_UTF16COLUMN_END = 0xFFF8; + private const ushort ERROR_DETAILS = 0x0002; + private const ushort ERROR_TRACEBACK = 0x0101; + private const ushort ERROR_HINT = 0x0001; /// /// Gets the details related to the error. @@ -44,43 +47,43 @@ public sealed class EdgeDBErrorException : EdgeDBException public ServerErrorCodes ErrorCode => ErrorResponse.ErrorCode; - internal ErrorResponse ErrorResponse; + internal IProtocolError ErrorResponse; /// /// Constructs a new with the specified - /// packet. + /// . /// /// - /// The packet which + /// The which /// caused this exception to be thrown. /// - internal EdgeDBErrorException(ErrorResponse error) + internal EdgeDBErrorException(IProtocolError error) : base(error.Message, typeof(ServerErrorCodes).GetField(error.ErrorCode.ToString())?.IsDefined(typeof(ShouldRetryAttribute), false) ?? false, typeof(ServerErrorCodes).GetField(error.ErrorCode.ToString())?.IsDefined(typeof(ShouldReconnectAttribute), false) ?? false) { - if(error.Attributes.Any(x => x.Code == 0x0002)) - Details = Encoding.UTF8.GetString(error.Attributes.FirstOrDefault(x => x.Code == 0x0002).Value); + if(error.TryGetAttribute(ERROR_DETAILS, out var kv)) + Details = Encoding.UTF8.GetString(kv.Value); - if (error.Attributes.Any(x => x.Code == 0x0101)) - ServerTraceBack = Encoding.UTF8.GetString(error.Attributes.FirstOrDefault(x => x.Code == 0x0101).Value); + if (error.TryGetAttribute(ERROR_TRACEBACK, out kv)) + ServerTraceBack = Encoding.UTF8.GetString(kv.Value); - if (error.Attributes.Any(x => x.Code == 0x0001)) - Hint = Encoding.UTF8.GetString(error.Attributes.FirstOrDefault(x => x.Code == 0x0001).Value); + if (error.TryGetAttribute(ERROR_HINT, out kv)) + Hint = Encoding.UTF8.GetString(kv.Value); ErrorResponse = error; } /// /// Constructs a new with the specified - /// packet and query string. + /// and query string. /// /// - /// The packet which + /// The which /// caused this exception to be thrown. /// /// The query that caused this error to be thrown. - internal EdgeDBErrorException(ErrorResponse error, string? query) + internal EdgeDBErrorException(IProtocolError error, string? query) : this(error) { Query = query; diff --git a/src/EdgeDB.Net.Driver/Models/ExecuteResult.cs b/src/EdgeDB.Net.Driver/Models/ExecuteResult.cs index 620caca2..6e1b41d2 100644 --- a/src/EdgeDB.Net.Driver/Models/ExecuteResult.cs +++ b/src/EdgeDB.Net.Driver/Models/ExecuteResult.cs @@ -1,10 +1,9 @@ -using EdgeDB.Binary.Packets; - namespace EdgeDB { /// /// Represents a generic execution result of a command. /// + [Obsolete("This class is no longer used anywhere within the binding and will be removed in a future version.")] public readonly struct ExecuteResult : IExecuteResult { /// @@ -23,13 +22,14 @@ internal ExecuteResult(bool success, Exception? exc, string? executedQuery) ExecutedQuery = executedQuery; } - IExecuteError? IExecuteResult.ExecutionError => Exception is EdgeDBErrorException x ? x.ErrorResponse : null; + IExecuteError? IExecuteResult.ExecutionError => null; } /// /// An interface representing a generic execution result. /// + [Obsolete("This interface is no longer used anywhere within the binding and will be removed in a future version.")] public interface IExecuteResult { /// @@ -56,6 +56,7 @@ public interface IExecuteResult /// /// Represents a generic execution error. /// + [Obsolete("This interface is no longer used anywhere within the binding and will be removed in a future version.")] public interface IExecuteError { /// diff --git a/src/EdgeDB.Net.Driver/Utils/BinaryUtils.cs b/src/EdgeDB.Net.Driver/Utils/BinaryUtils.cs index 48ff5e73..fe622b19 100644 --- a/src/EdgeDB.Net.Driver/Utils/BinaryUtils.cs +++ b/src/EdgeDB.Net.Driver/Utils/BinaryUtils.cs @@ -1,4 +1,5 @@ using EdgeDB.Binary; +using EdgeDB.Binary.Protocol; using System; using System.Collections.Generic; using System.Linq; @@ -10,6 +11,13 @@ namespace EdgeDB.Utils { internal sealed unsafe class BinaryUtils { + public static unsafe void ReadPrimitive(Stream stream, ref T target) + where T : unmanaged + { + stream.Read(new(Unsafe.AsPointer(ref target), sizeof(T))); + CorrectEndianness(ref target); + } + internal static int SizeOfString(string? str) => str is null ? 4 : Encoding.UTF8.GetByteCount(str) + 4; internal static int SizeOfByteArray(byte[]? arr) diff --git a/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs b/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs index 83ac2586..402b1191 100644 --- a/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs +++ b/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs @@ -25,6 +25,12 @@ public static bool IsSubclassOfRawGeneric(Type generic, Type? toCheck) return false; } + public static bool IsSubclassOfInterfaceGeneric(Type generic, Type? toCheck) + { + var interfaces = toCheck!.GetInterfaces(); + return interfaces.Any(x => IsSubclassOfRawGeneric(generic, x)); + } + public static bool TryGetRawGeneric(Type generic, Type? toCheck, out Type? genericReference) { genericReference = null; diff --git a/src/EdgeDB.Net.Driver/Utils/Scram.cs b/src/EdgeDB.Net.Driver/Utils/Scram.cs index 8b3430ee..10a77907 100644 --- a/src/EdgeDB.Net.Driver/Utils/Scram.cs +++ b/src/EdgeDB.Net.Driver/Utils/Scram.cs @@ -1,5 +1,4 @@ using EdgeDB.Binary; -using EdgeDB.Binary.Packets; using EdgeDB.Binary.Codecs; using System.Security.Cryptography; using System.Text; @@ -45,12 +44,6 @@ public string BuildInitialMessage(string username) return $"n,,{_rawFirstMessage}"; } - - public AuthenticationSASLInitialResponse BuildInitialMessagePacket(EdgeDBBinaryClient client, string username, string method) - => new( - _stringCodec.Serialize(client, BuildInitialMessage(username)), - method); - public (string final, byte[] expectedSig) BuildFinalMessage(string initialResponse, string password) { var parsedMessage = ParseServerMessage(initialResponse); @@ -83,22 +76,9 @@ public AuthenticationSASLInitialResponse BuildInitialMessagePacket(EdgeDBBinaryC return ($"{final},p={Convert.ToBase64String(clientProof)}", serverProof); } - public (AuthenticationSASLResponse FinalMessage, byte[] ExpectedSig) BuildFinalMessagePacket( - EdgeDBBinaryClient client, - in AuthenticationStatus status, - string password) - { - var (final, sig) = BuildFinalMessage(_stringCodec.Deserialize(client, status.SASLDataBuffer)!, password); - - return (new AuthenticationSASLResponse(_stringCodec.Serialize(client, final)), sig); - } - - public static byte[] ParseServerFinalMessage(EdgeDBBinaryClient client, in AuthenticationStatus status) + public static byte[] ParseServerSig(string data) { - var msg = _stringCodec.Deserialize(client, status.SASLDataBuffer)!; - - var parsed = ParseServerMessage(msg); - + var parsed = ParseServerMessage(data); return Convert.FromBase64String(parsed["v"]); } diff --git a/tests/EdgeDB.Tests.Benchmarks/DeserializerBenchmarks.cs b/tests/EdgeDB.Tests.Benchmarks/DeserializerBenchmarks.cs index be901a5d..770b35b5 100644 --- a/tests/EdgeDB.Tests.Benchmarks/DeserializerBenchmarks.cs +++ b/tests/EdgeDB.Tests.Benchmarks/DeserializerBenchmarks.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Attributes; using EdgeDB.Binary; +using EdgeDB.Binary.Protocol; using EdgeDB.Utils; using System; using System.Collections.Generic; @@ -52,11 +53,14 @@ static DeserializerBenchmarks() [ParamsSource(nameof(ValuesForPacket))] public (ServerMessageType Type, byte[] Data) Packet { get; set; } - [Benchmark] - public IReceiveable? Deserialize() - { - var data = Packet.Data.AsMemory(); - return PacketSerializer.DeserializePacket(Packet.Type, ref data, Packet.Data.Length, null!); // client as null is OK as its only used for logging unknown packet - } + //[Benchmark] + //public IReceiveable? Deserialize() + //{ + // var data = Packet.Data.AsMemory(); + + // var factory = IProtocolProvider. + + // return PacketSerializer.DeserializePacket(Packet.Type, ref data, Packet.Data.Length, null!); // client as null is OK as its only used for logging unknown packet + //} } } diff --git a/tests/EdgeDB.Tests.Benchmarks/PacketWritingBenchmark.cs b/tests/EdgeDB.Tests.Benchmarks/PacketWritingBenchmark.cs index acfce3df..4ac8c354 100644 --- a/tests/EdgeDB.Tests.Benchmarks/PacketWritingBenchmark.cs +++ b/tests/EdgeDB.Tests.Benchmarks/PacketWritingBenchmark.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using EdgeDB.Binary; -using EdgeDB.Binary.Packets; using System; using System.Collections.Generic; using System.Linq; diff --git a/tests/EdgeDB.Tests.Benchmarks/TypeBuilderBenchmarks.cs b/tests/EdgeDB.Tests.Benchmarks/TypeBuilderBenchmarks.cs index 20f1e030..53114d50 100644 --- a/tests/EdgeDB.Tests.Benchmarks/TypeBuilderBenchmarks.cs +++ b/tests/EdgeDB.Tests.Benchmarks/TypeBuilderBenchmarks.cs @@ -1,50 +1,50 @@ using BenchmarkDotNet.Attributes; -using EdgeDB.Binary.Packets; using EdgeDB.Binary.Codecs; using EdgeDB.Binary; using Microsoft.Extensions.Logging.Abstractions; +using EdgeDB.Binary.Protocol.V1._0.Packets; namespace EdgeDB.Tests.Benchmarks { [MemoryDiagnoser] public class TypeBuilderBenchmarks { - public class Person - { - public string? Name { get; set; } + //public class Person + //{ + // public string? Name { get; set; } - public string? Email { get; set; } - } + // public string? Email { get; set; } + //} - private static readonly EdgeDBBinaryClient client = new EdgeDBTcpClient(new(), new(), null!); - internal static Binary.Codecs.ObjectCodec Codec; - internal static Data Data; - static TypeBuilderBenchmarks() - { - Data = new Data(new byte[] { 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x0F, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x3A, 0x3A, 0x50, 0x65, 0x72, 0x73, 0x6F, 0x6E, 0x00, 0x00, 0x0B, 0x86, 0x00, 0x00, 0x00, 0x10, 0x4D, 0x3A, 0xEC, 0xD0, 0x0A, 0xA1, 0x11, 0xED, 0x87, 0x37, 0xB3, 0xDB, 0x16, 0x26, 0xE0, 0x22, 0x00, 0x00, 0x0B, 0x86, 0x00, 0x00, 0x00, 0x10, 0x97, 0x2B, 0xCF, 0x8A, 0x0A, 0xA8, 0x11, 0xED, 0x93, 0x76, 0xBB, 0xA4, 0xA8, 0xC6, 0xC3, 0xF0, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x0A, 0x4A, 0x6F, 0x68, 0x6E, 0x20, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x10, 0x6A, 0x6F, 0x68, 0x6E, 0x40, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D }); + //private static readonly EdgeDBBinaryClient client = new EdgeDBTcpClient(new(), new(), null!); + //internal static Binary.Codecs.ObjectCodec Codec; + //internal static Data Data; + //static TypeBuilderBenchmarks() + //{ + // Data = new Data(new byte[] { 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x0F, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, 0x3A, 0x3A, 0x50, 0x65, 0x72, 0x73, 0x6F, 0x6E, 0x00, 0x00, 0x0B, 0x86, 0x00, 0x00, 0x00, 0x10, 0x4D, 0x3A, 0xEC, 0xD0, 0x0A, 0xA1, 0x11, 0xED, 0x87, 0x37, 0xB3, 0xDB, 0x16, 0x26, 0xE0, 0x22, 0x00, 0x00, 0x0B, 0x86, 0x00, 0x00, 0x00, 0x10, 0x97, 0x2B, 0xCF, 0x8A, 0x0A, 0xA8, 0x11, 0xED, 0x93, 0x76, 0xBB, 0xA4, 0xA8, 0xC6, 0xC3, 0xF0, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x0A, 0x4A, 0x6F, 0x68, 0x6E, 0x20, 0x53, 0x6D, 0x69, 0x74, 0x68, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x10, 0x6A, 0x6F, 0x68, 0x6E, 0x40, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D }); - Codec = new Binary.Codecs.ObjectCodec(new ICodec[] - { - new Binary.Codecs.TextCodec(), - new Binary.Codecs.UUIDCodec(), - new Binary.Codecs.UUIDCodec(), - new Binary.Codecs.TextCodec(), - new Binary.Codecs.TextCodec(), - }, new string[] - { - "__tname__", - "__tid__", - "id", - "name", - "email" - }); - Codec = Codec.GetOrCreateTypeCodec(typeof(Person)); - } + // //Codec = new Binary.Codecs.ObjectCodec(in Guid.Empty, new ICodec[] + // //{ + // // new Binary.Codecs.TextCodec(), + // // new Binary.Codecs.UUIDCodec(), + // // new Binary.Codecs.UUIDCodec(), + // // new Binary.Codecs.TextCodec(), + // // new Binary.Codecs.TextCodec(), + // //}, new string[] + // //{ + // // "__tname__", + // // "__tid__", + // // "id", + // // "name", + // // "email" + // //}); + // //Codec = Codec.GetOrCreateTypeCodec(typeof(Person)); + //} - [Benchmark] - public Person? DeserializePersonNew() - { - return (Person?)TypeBuilder.BuildObject(client, typeof(Person), Codec, ref Data); - } + //[Benchmark] + //public Person? DeserializePersonNew() + //{ + // return (Person?)TypeBuilder.BuildObject(client, typeof(Person), Codec, Data.PayloadBuffer); + //} } } diff --git a/tests/EdgeDB.Tests.Unit/SCRAMTests.cs b/tests/EdgeDB.Tests.Unit/SCRAMTests.cs index 5d4e5b47..fa20d9b6 100644 --- a/tests/EdgeDB.Tests.Unit/SCRAMTests.cs +++ b/tests/EdgeDB.Tests.Unit/SCRAMTests.cs @@ -1,5 +1,4 @@ using EdgeDB.Binary; -using EdgeDB.Binary.Packets; using EdgeDB.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -27,22 +26,18 @@ public void TestSCRAM() { var scram = new Scram(Convert.FromBase64String(SCRAM_CLIENT_NONCE)); - var clientFirst = scram.BuildInitialMessagePacket(_client, SCRAM_USERNAME, SCRAM_METHOD); - Assert.AreEqual($"{SCRAM_METHOD} n,,n={SCRAM_USERNAME},r={SCRAM_CLIENT_NONCE}", clientFirst.ToString()); + var clientFirst = scram.BuildInitialMessage(SCRAM_USERNAME); + Assert.AreEqual($"n,,n={SCRAM_USERNAME},r={SCRAM_CLIENT_NONCE}", clientFirst); var serverFirst = CreateServerFirstMessage(); - var clientFinal = scram.BuildFinalMessagePacket(_client, in serverFirst, SCRAM_PASSWORD); + var clientFinal = scram.BuildFinalMessage(serverFirst, SCRAM_PASSWORD); - Assert.AreEqual("6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=", Convert.ToBase64String(clientFinal.ExpectedSig)); - Assert.AreEqual($"c=biws,r={SCRAM_SERVER_NONCE},p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", clientFinal.FinalMessage.ToString()); + Assert.AreEqual($"c=biws,r={SCRAM_SERVER_NONCE},p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=", clientFinal.final); } - private static AuthenticationStatus CreateServerFirstMessage() + private static string CreateServerFirstMessage() { - var inner = Encoding.UTF8.GetBytes($"r={SCRAM_SERVER_NONCE},s={SCRAM_SALT},i=4096"); - var data = new byte[] { 0x00, 0x00, 0x00, 0x0b }.Concat(BitConverter.GetBytes(inner.Length).Reverse()).Concat(inner).ToArray(); - var reader = new PacketReader(data); - return new AuthenticationStatus(ref reader); + return $"r={SCRAM_SERVER_NONCE},s={SCRAM_SALT},i=4096"; } } } diff --git a/tests/EdgeDB.Tests.Unit/ScalarCodecsTests.cs b/tests/EdgeDB.Tests.Unit/ScalarCodecsTests.cs index e7a7b168..2a372fd5 100644 --- a/tests/EdgeDB.Tests.Unit/ScalarCodecsTests.cs +++ b/tests/EdgeDB.Tests.Unit/ScalarCodecsTests.cs @@ -57,10 +57,10 @@ public void TestBytesCodec() [TestMethod] public void TestDatetimeCodec() { - var codec = CodecBuilder.GetScalarCodec(); + var codec = CodecBuilder.GetScalarCodec(); var data = new byte[] { 0x00, 0x02, 0x2b, 0x35, 0x9b, 0xc4, 0x10, 0x00, }; - var expected = DateTimeOffset.Parse("2019-05-06T12:00+00:00"); + var expected = new DataTypes.DateTime(DateTimeOffset.Parse("2019-05-06T12:00+00:00")); TestCodec(codec, expected, data); } diff --git a/tools/EdgeDB.BinaryDebugger/DebuggerClient.cs b/tools/EdgeDB.BinaryDebugger/DebuggerClient.cs index 95830559..2736bec5 100644 --- a/tools/EdgeDB.BinaryDebugger/DebuggerClient.cs +++ b/tools/EdgeDB.BinaryDebugger/DebuggerClient.cs @@ -156,7 +156,10 @@ private void OnRead(int read, byte[] buffer, int offset, int count) { var memory = copyBuffer.AsMemory()[..read]; var raw = HexConverter.ToHex(memory.ToArray()); - var packet = PacketSerializer.DeserializePacket(Header.Value.Type, ref memory, Header.Value.Length, this); + + var factory = ProtocolProvider.GetPacketFactory(Header.Value.Type)!; + + var packet = PacketSerializer.DeserializePacket(in factory, in memory); Writer.WriteLine($"READING\n" + $"=======\n" +