From 368302324e78556d57003fffa6e4e5fc407d891d Mon Sep 17 00:00:00 2001 From: Ilan Uzan Date: Sat, 20 Jul 2024 23:28:30 +0300 Subject: [PATCH] add copy command support to Npgsql (#72) --- CodeGenerator/CodeGenerator.cs | 5 +- Drivers/DbDriver.cs | 5 +- Drivers/Generators/CopyFromDeclareGen.cs | 83 +++++++++++++++++++ Drivers/Generators/ExecDeclareGen.cs | 7 +- Drivers/Generators/ExecLastIdDeclareGen.cs | 7 +- Drivers/Generators/ManyDeclareGen.cs | 8 +- Drivers/Generators/OneDeclareGen.cs | 4 +- Drivers/MySqlConnectorDriver.cs | 4 +- Drivers/NpgsqlDriver.cs | 33 +++++--- Drivers/Variable.cs | 2 + EndToEndTests/{Consts.cs => DataGenerator.cs} | 5 +- EndToEndTests/ISqlDriverTester.cs | 10 +-- EndToEndTests/MySqlConnectorTester.cs | 32 +++---- EndToEndTests/NpgsqlTester.cs | 60 ++++++++++---- EndToEndTests/Tests.cs | 9 +- Extensions/ListExtensions.cs | 5 ++ Extensions/StringExtensions.cs | 5 ++ Makefile | 2 +- NpgsqlExample/QuerySql.cs | 36 +++++--- examples/authors/postgresql/query.sql | 22 ++--- sqlc.local.yaml | 6 +- 21 files changed, 250 insertions(+), 100 deletions(-) create mode 100644 Drivers/Generators/CopyFromDeclareGen.cs rename EndToEndTests/{Consts.cs => DataGenerator.cs} (80%) diff --git a/CodeGenerator/CodeGenerator.cs b/CodeGenerator/CodeGenerator.cs index 93b27efc..11abc00f 100644 --- a/CodeGenerator/CodeGenerator.cs +++ b/CodeGenerator/CodeGenerator.cs @@ -238,7 +238,10 @@ private MemberDeclarationSyntax AddMethodDeclaration(Query query) query.Params, query.Columns), ":many" => DbDriver.ManyDeclare(query.Name, queryTextConstant, argInterface, returnInterface, query.Params, query.Columns), - ":execlastid" => DbDriver.ExecLastIdDeclare(query.Name, queryTextConstant, argInterface, query.Params), + ":execlastid" => ((MySqlConnectorDriver)DbDriver) + .ExecLastIdDeclare(query.Name, queryTextConstant, argInterface, query.Params), + ":copyfrom" => ((NpgsqlDriver)DbDriver) + .CopyFromDeclare(query.Name, queryTextConstant, argInterface, query.Params), _ => throw new InvalidDataException() }; diff --git a/Drivers/DbDriver.cs b/Drivers/DbDriver.cs index 1fc68517..5e0a57f4 100644 --- a/Drivers/DbDriver.cs +++ b/Drivers/DbDriver.cs @@ -59,7 +59,7 @@ public string GetColumnReader(Column column, int ordinal) public abstract string TransformQueryText(Query query); - public abstract (string, string) EstablishConnection(); + public abstract (string, string) EstablishConnection(bool isCopyCommand = false); // TODO fix codesmell - should act upon the query object public abstract string CreateSqlCommand(string sqlTextConstant); @@ -72,9 +72,6 @@ public abstract MemberDeclarationSyntax ManyDeclare(string funcName, string sqlT public abstract MemberDeclarationSyntax ExecDeclare(string funcName, string text, string argInterface, IList parameters); - public abstract MemberDeclarationSyntax ExecLastIdDeclare(string funcName, string queryTextConstant, - string argInterface, IList parameters); - public static bool IsCsharpPrimitive(string csharpType) { var csharpPrimitives = new HashSet { "long", "double", "int", "float", "bool" }; diff --git a/Drivers/Generators/CopyFromDeclareGen.cs b/Drivers/Generators/CopyFromDeclareGen.cs new file mode 100644 index 00000000..ae9640be --- /dev/null +++ b/Drivers/Generators/CopyFromDeclareGen.cs @@ -0,0 +1,83 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Plugin; +using System.Collections.Generic; +using System.Linq; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SqlcGenCsharp.Drivers.Generators; + +public class CopyFromDeclareGen(DbDriver dbDriver) +{ + public MemberDeclarationSyntax Generate(string funcName, string queryTextConstant, string argInterface, + IList parameters) + { + return ParseMemberDeclaration($$""" + public async Task {{funcName}}(List<{{argInterface}}> args) + { + {{GetMethodBody(queryTextConstant, parameters)}} + } + """)!; + } + + private string GetMethodBody(string queryTextConstant, IEnumerable parameters) + { + var (establishConnection, connectionOpen) = dbDriver.EstablishConnection(true); + var beginBinaryImport = $"{Variable.Connection.Name()}.BeginBinaryImportAsync({queryTextConstant}"; + + return dbDriver.DotnetFramework.LatestDotnetSupported() + ? GetAsModernDotnet() + : GetAsLegacyDotnet(); + + string GetAsModernDotnet() + { + var addRowsToCopyCommand = AddRowsToCopyCommand(); + return $$""" + { + await using {{establishConnection}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} + await {{Variable.Connection.Name()}}.OpenAsync(); + await using var {{Variable.Writer.Name()}} = await {{beginBinaryImport}}); + {{addRowsToCopyCommand}} + await {{Variable.Writer.Name()}}.CompleteAsync(); + await {{Variable.Connection.Name()}}.CloseAsync(); + } + """; + } + + string GetAsLegacyDotnet() + { + var addRowsToCopyCommand = AddRowsToCopyCommand(); + return $$""" + { + using ({{establishConnection}}) + { + {{connectionOpen.AppendSemicolonUnlessEmpty()}} + await {{Variable.Connection.Name()}}.OpenAsync(); + using (var {{Variable.Writer.Name()}} = await {{beginBinaryImport}})) + { + {{addRowsToCopyCommand}} + await {{Variable.Writer.Name()}}.CompleteAsync(); + } + await {{Variable.Connection.Name()}}.CloseAsync(); + } + } + """; + } + + string AddRowsToCopyCommand() + { + var constructRow = new List() + .Append($"await {Variable.Writer.Name()}.StartRowAsync();") + .Concat(parameters + .Select(p => + $"await {Variable.Writer.Name()}.WriteAsync({Variable.Row.Name()}.{p.Column.Name.FirstCharToUpper()});")) + .JoinByNewLine(); + return $$""" + foreach (var {{Variable.Row.Name()}} in args) + { + {{constructRow}} + } + """; + } + } +} \ No newline at end of file diff --git a/Drivers/Generators/ExecDeclareGen.cs b/Drivers/Generators/ExecDeclareGen.cs index 30d35972..79686f65 100644 --- a/Drivers/Generators/ExecDeclareGen.cs +++ b/Drivers/Generators/ExecDeclareGen.cs @@ -10,8 +10,9 @@ public class ExecDeclareGen(DbDriver dbDriver) public MemberDeclarationSyntax Generate(string funcName, string queryTextConstant, string argInterface, IList parameters) { + var parametersStr = CommonGen.GetParameterListAsString(argInterface, parameters); return ParseMemberDeclaration($$""" - public async Task {{funcName}}({{CommonGen.GetParameterListAsString(argInterface, parameters)}}) + public async Task {{funcName}}({{parametersStr}}) { {{GetMethodBody(queryTextConstant, parameters)}} } @@ -34,7 +35,7 @@ string GetWithUsingAsStatement() return $$""" { await using {{establishConnection}}; - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} await using {{createSqlCommand}}; {{commandParameters.JoinByNewLine()}} {{executeScalar}} @@ -48,7 +49,7 @@ string GetWithUsingAsBlock() { using ({{establishConnection}}) { - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} using ({{createSqlCommand}}) { {{commandParameters.JoinByNewLine()}} diff --git a/Drivers/Generators/ExecLastIdDeclareGen.cs b/Drivers/Generators/ExecLastIdDeclareGen.cs index 6a521a31..9d98f60a 100644 --- a/Drivers/Generators/ExecLastIdDeclareGen.cs +++ b/Drivers/Generators/ExecLastIdDeclareGen.cs @@ -10,8 +10,9 @@ public class ExecLastIdDeclareGen(DbDriver dbDriver) public MemberDeclarationSyntax Generate(string funcName, string queryTextConstant, string argInterface, IList parameters) { + var parametersStr = CommonGen.GetParameterListAsString(argInterface, parameters); return ParseMemberDeclaration($$""" - public async Task {{funcName}}({{CommonGen.GetParameterListAsString(argInterface, parameters)}}) + public async Task {{funcName}}({{parametersStr}}) { {{GetMethodBody(queryTextConstant, parameters)}} } @@ -34,7 +35,7 @@ string GetWithUsingAsStatement() return $$""" { await using {{establishConnection}}; - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} await using {{createSqlCommand}}; {{commandParameters.JoinByNewLine()}} {{executeScalarAndReturnCreated.JoinByNewLine()}} @@ -48,7 +49,7 @@ string GetWithUsingAsBlock() { using ({{establishConnection}}) { - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} using ({{createSqlCommand}}) { {{commandParameters.JoinByNewLine()}} diff --git a/Drivers/Generators/ManyDeclareGen.cs b/Drivers/Generators/ManyDeclareGen.cs index 32cdfd10..6afa425d 100644 --- a/Drivers/Generators/ManyDeclareGen.cs +++ b/Drivers/Generators/ManyDeclareGen.cs @@ -13,10 +13,10 @@ public class ManyDeclareGen(DbDriver dbDriver) public MemberDeclarationSyntax Generate(string funcName, string queryTextConstant, string argInterface, string returnInterface, IList parameters, IEnumerable columns) { - var parameterList = CommonGen.GetParameterListAsString(argInterface, parameters); + var parametersStr = CommonGen.GetParameterListAsString(argInterface, parameters); var returnType = $"Task>"; return ParseMemberDeclaration($$""" - public async {{returnType}} {{funcName}}({{parameterList}}) + public async {{returnType}} {{funcName}}({{parametersStr}}) { {{GetMethodBody(queryTextConstant, returnInterface, columns, parameters)}} } @@ -48,7 +48,7 @@ string GetWithUsingAsStatement() return $$""" { await using {{establishConnection}}; - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} await using {{createSqlCommand}}; {{commandParameters.JoinByNewLine()}} {{initDataReader}}; @@ -65,7 +65,7 @@ string GetWithUsingAsBlock() { using ({{establishConnection}}) { - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} using ({{createSqlCommand}}) { {{commandParameters.JoinByNewLine()}} diff --git a/Drivers/Generators/OneDeclareGen.cs b/Drivers/Generators/OneDeclareGen.cs index 47c194c2..f28d9876 100644 --- a/Drivers/Generators/OneDeclareGen.cs +++ b/Drivers/Generators/OneDeclareGen.cs @@ -42,7 +42,7 @@ string GetWithUsingAsStatement() return $$""" { await using {{establishConnection}}; - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} await using {{createSqlCommand}}; {{commandParameters.JoinByNewLine()}} {{initDataReader}}; @@ -61,7 +61,7 @@ string GetWithUsingAsBlock() { using ({{establishConnection}}) { - {{connectionOpen}}; + {{connectionOpen.AppendSemicolonUnlessEmpty()}} using ({{createSqlCommand}}) { {{commandParameters.JoinByNewLine()}} diff --git a/Drivers/MySqlConnectorDriver.cs b/Drivers/MySqlConnectorDriver.cs index 57cc5bf7..3c4f48da 100644 --- a/Drivers/MySqlConnectorDriver.cs +++ b/Drivers/MySqlConnectorDriver.cs @@ -58,7 +58,7 @@ public override UsingDirectiveSyntax[] GetUsingDirectives() .ToArray(); } - public override (string, string) EstablishConnection() + public override (string, string) EstablishConnection(bool isCopyCommand = false) { return ( $"var {Variable.Connection.Name()} = new MySqlConnection({Variable.ConnectionString.Name()})", @@ -89,7 +89,7 @@ public override MemberDeclarationSyntax ExecDeclare(string funcName, string quer return new ExecDeclareGen(this).Generate(funcName, queryTextConstant, argInterface, parameters); } - public override MemberDeclarationSyntax ExecLastIdDeclare(string funcName, string queryTextConstant, + public MemberDeclarationSyntax ExecLastIdDeclare(string funcName, string queryTextConstant, string argInterface, IList parameters) { return new ExecLastIdDeclareGen(this).Generate(funcName, queryTextConstant, argInterface, parameters); diff --git a/Drivers/NpgsqlDriver.cs b/Drivers/NpgsqlDriver.cs index 99839ae8..6f7e17b1 100644 --- a/Drivers/NpgsqlDriver.cs +++ b/Drivers/NpgsqlDriver.cs @@ -47,11 +47,14 @@ public override UsingDirectiveSyntax[] GetUsingDirectives() .ToArray(); } - public override (string, string) EstablishConnection() + public override (string, string) EstablishConnection(bool isCopyCommand = false) { - return ( - $"var {Variable.Connection.Name()} = NpgsqlDataSource.Create({Variable.ConnectionString.Name()})", - string.Empty); + if (isCopyCommand) + return ( + $"var ds = NpgsqlDataSource.Create({Variable.ConnectionString.Name()})", + $"var {Variable.Connection.Name()} = ds.CreateConnection()" + ); + return ($"var {Variable.Connection.Name()} = NpgsqlDataSource.Create({Variable.ConnectionString.Name()})", ""); } public override string CreateSqlCommand(string sqlTextConstant) @@ -61,6 +64,9 @@ public override string CreateSqlCommand(string sqlTextConstant) public override string TransformQueryText(Query query) { + if (query.Cmd == ":copyfrom") + return GetCopyCommand(); + var queryText = query.Text; for (var i = 0; i < query.Params.Count; i++) { @@ -68,8 +74,13 @@ public override string TransformQueryText(Query query) queryText = Regex.Replace(queryText, $@"\$\s*{i + 1}", $"@{currentParameter.Column.Name.FirstCharToLower()}"); } - return queryText; + + string GetCopyCommand() + { + var copyParams = query.Params.Select(p => p.Column.Name).JoinByComma(); + return $"COPY {query.InsertIntoTable.Name} ({copyParams}) FROM STDIN (FORMAT BINARY)"; + } } public override MemberDeclarationSyntax OneDeclare(string funcName, string queryTextConstant, string argInterface, @@ -85,16 +96,16 @@ public override MemberDeclarationSyntax ExecDeclare(string funcName, string quer return new ExecDeclareGen(this).Generate(funcName, queryTextConstant, argInterface, parameters); } - public override MemberDeclarationSyntax ExecLastIdDeclare(string funcName, string queryTextConstant, - string argInterface, IList parameters) - { - return new ExecLastIdDeclareGen(this).Generate(funcName, queryTextConstant, argInterface, parameters); - } - public override MemberDeclarationSyntax ManyDeclare(string funcName, string queryTextConstant, string argInterface, string returnInterface, IList parameters, IEnumerable columns) { return new ManyDeclareGen(this).Generate(funcName, queryTextConstant, argInterface, returnInterface, parameters, columns); } + + public MemberDeclarationSyntax CopyFromDeclare(string funcName, string queryTextConstant, string argInterface, + IList parameters) + { + return new CopyFromDeclareGen(this).Generate(funcName, queryTextConstant, argInterface, parameters); + } } \ No newline at end of file diff --git a/Drivers/Variable.cs b/Drivers/Variable.cs index 5dd2d2df..c24f94fc 100644 --- a/Drivers/Variable.cs +++ b/Drivers/Variable.cs @@ -5,6 +5,8 @@ public enum Variable ConnectionString, Connection, Reader, + Row, + Writer, Command, Result } diff --git a/EndToEndTests/Consts.cs b/EndToEndTests/DataGenerator.cs similarity index 80% rename from EndToEndTests/Consts.cs rename to EndToEndTests/DataGenerator.cs index 5066189d..5fd01869 100644 --- a/EndToEndTests/Consts.cs +++ b/EndToEndTests/DataGenerator.cs @@ -1,6 +1,9 @@ +using NpgsqlExample; +using System; + namespace SqlcGenCsharpTests; -public static class Consts +public static class DataGenerator { public const string BojackAuthor = "Bojack Horseman"; public const string BojackTheme = "Back in the 90s he was in a very famous TV show"; diff --git a/EndToEndTests/ISqlDriverTester.cs b/EndToEndTests/ISqlDriverTester.cs index 558cc533..aecbc825 100644 --- a/EndToEndTests/ISqlDriverTester.cs +++ b/EndToEndTests/ISqlDriverTester.cs @@ -2,18 +2,18 @@ namespace SqlcGenCsharpTests; -public interface ISqlDriverTester +public abstract class SqlDriverTester { - async Task TestFlow() + public async Task TestBasicFlow() { var firstInsertedId = await CreateFirstAuthorAndTest(); await CreateSecondAuthorAndTest(); await DeleteFirstAuthorAndTest(firstInsertedId); } - protected Task CreateFirstAuthorAndTest(); + protected abstract Task CreateFirstAuthorAndTest(); - protected Task CreateSecondAuthorAndTest(); + protected abstract Task CreateSecondAuthorAndTest(); - protected Task DeleteFirstAuthorAndTest(long idToDelete); + protected abstract Task DeleteFirstAuthorAndTest(long idToDelete); } \ No newline at end of file diff --git a/EndToEndTests/MySqlConnectorTester.cs b/EndToEndTests/MySqlConnectorTester.cs index eb51e97a..f1c1eb13 100644 --- a/EndToEndTests/MySqlConnectorTester.cs +++ b/EndToEndTests/MySqlConnectorTester.cs @@ -6,54 +6,54 @@ namespace SqlcGenCsharpTests; -public class MySqlConnectorTester : ISqlDriverTester +public class MySqlConnectorTester : SqlDriverTester { private static string ConnectionStringEnv => "MYSQL_CONNECTION_STRING"; private QuerySql QuerySql { get; } = new(Environment.GetEnvironmentVariable(ConnectionStringEnv)!); - public async Task CreateFirstAuthorAndTest() + protected override async Task CreateFirstAuthorAndTest() { var createAuthorReturnIdArgs = new QuerySql.CreateAuthorReturnIdArgs { - Name = Consts.BojackAuthor, - Bio = Consts.BojackTheme + Name = DataGenerator.BojackAuthor, + Bio = DataGenerator.BojackTheme }; var insertedId = await QuerySql.CreateAuthorReturnId(createAuthorReturnIdArgs); var getBojackAuthorArgs = new QuerySql.GetAuthorArgs { Id = insertedId }; var bojackAuthor = await QuerySql.GetAuthor(getBojackAuthorArgs); Assert.That(bojackAuthor is { - Name: Consts.BojackAuthor, - Bio: Consts.BojackTheme + Name: DataGenerator.BojackAuthor, + Bio: DataGenerator.BojackTheme }); return insertedId; } - public async Task CreateSecondAuthorAndTest() + protected override async Task CreateSecondAuthorAndTest() { var createAuthorArgs = new QuerySql.CreateAuthorArgs { - Name = Consts.DrSeussAuthor, - Bio = Consts.DrSeussQuote + Name = DataGenerator.DrSeussAuthor, + Bio = DataGenerator.DrSeussQuote }; await QuerySql.CreateAuthor(createAuthorArgs); var actualAuthors = await QuerySql.ListAuthors(); Assert.That(actualAuthors[0] is { - Name: Consts.BojackAuthor, - Bio: Consts.BojackTheme + Name: DataGenerator.BojackAuthor, + Bio: DataGenerator.BojackTheme }); Assert.That(actualAuthors[1] is { - Name: Consts.DrSeussAuthor, - Bio: Consts.DrSeussQuote + Name: DataGenerator.DrSeussAuthor, + Bio: DataGenerator.DrSeussQuote }); ClassicAssert.AreEqual(2, actualAuthors.Count); } - public async Task DeleteFirstAuthorAndTest(long idToDelete) + protected override async Task DeleteFirstAuthorAndTest(long idToDelete) { var deleteAuthorArgs = new QuerySql.DeleteAuthorArgs { @@ -63,8 +63,8 @@ public async Task DeleteFirstAuthorAndTest(long idToDelete) var authorRows = await QuerySql.ListAuthors(); Assert.That(authorRows[0] is { - Name: Consts.DrSeussAuthor, - Bio: Consts.DrSeussQuote + Name: DataGenerator.DrSeussAuthor, + Bio: DataGenerator.DrSeussQuote }); ClassicAssert.AreEqual(1, authorRows.Count); } diff --git a/EndToEndTests/NpgsqlTester.cs b/EndToEndTests/NpgsqlTester.cs index 16541e4a..912afa05 100644 --- a/EndToEndTests/NpgsqlTester.cs +++ b/EndToEndTests/NpgsqlTester.cs @@ -2,29 +2,32 @@ using NUnit.Framework; using NUnit.Framework.Legacy; using System; +using System.Linq; using System.Threading.Tasks; namespace SqlcGenCsharpTests; -public class NpgsqlTester : ISqlDriverTester +public class NpgsqlTester : SqlDriverTester { + private static readonly Random Randomizer = new(); + private static string ConnectionStringEnv => "POSTGRES_CONNECTION_STRING"; private QuerySql QuerySql { get; } = new(Environment.GetEnvironmentVariable(ConnectionStringEnv)!); - public async Task CreateFirstAuthorAndTest() + protected override async Task CreateFirstAuthorAndTest() { var bojackCreateAuthorArgs = new QuerySql.CreateAuthorArgs { - Name = Consts.BojackAuthor, - Bio = Consts.BojackTheme + Name = DataGenerator.BojackAuthor, + Bio = DataGenerator.BojackTheme }; var createdBojackAuthor = await QuerySql.CreateAuthor(bojackCreateAuthorArgs); Assert.That(createdBojackAuthor is { - Name: Consts.BojackAuthor, - Bio: Consts.BojackTheme + Name: DataGenerator.BojackAuthor, + Bio: DataGenerator.BojackTheme }); var bojackInsertedId = GetId(createdBojackAuthor); @@ -35,8 +38,8 @@ public async Task CreateFirstAuthorAndTest() var singleAuthor = await QuerySql.GetAuthor(getAuthorArgs); Assert.That(singleAuthor is { - Name: Consts.BojackAuthor, - Bio: Consts.BojackTheme + Name: DataGenerator.BojackAuthor, + Bio: DataGenerator.BojackTheme }); return bojackInsertedId; @@ -53,29 +56,29 @@ long GetId(QuerySql.CreateAuthorRow? createdAuthorRow) } } - public async Task CreateSecondAuthorAndTest() + protected override async Task CreateSecondAuthorAndTest() { var createAuthorArgs = new QuerySql.CreateAuthorArgs { - Name = Consts.DrSeussAuthor, - Bio = Consts.DrSeussQuote + Name = DataGenerator.DrSeussAuthor, + Bio = DataGenerator.DrSeussQuote }; await QuerySql.CreateAuthor(createAuthorArgs); var authors = await QuerySql.ListAuthors(); Assert.That(authors[0] is { - Name: Consts.BojackAuthor, - Bio: Consts.BojackTheme + Name: DataGenerator.BojackAuthor, + Bio: DataGenerator.BojackTheme }); Assert.That(authors[1] is { - Name: Consts.DrSeussAuthor, - Bio: Consts.DrSeussQuote + Name: DataGenerator.DrSeussAuthor, + Bio: DataGenerator.DrSeussQuote }); ClassicAssert.AreEqual(2, authors.Count); } - public async Task DeleteFirstAuthorAndTest(long idToDelete) + protected override async Task DeleteFirstAuthorAndTest(long idToDelete) { var deleteAuthorArgs = new QuerySql.DeleteAuthorArgs { @@ -85,9 +88,30 @@ public async Task DeleteFirstAuthorAndTest(long idToDelete) var authorRows = await QuerySql.ListAuthors(); Assert.That(authorRows[0] is { - Name: Consts.DrSeussAuthor, - Bio: Consts.DrSeussQuote + Name: DataGenerator.DrSeussAuthor, + Bio: DataGenerator.DrSeussQuote }); ClassicAssert.AreEqual(1, authorRows.Count); } + + public async Task TestCopyFlow() + { + const int batchSize = 100; + var beforeCountRows = QuerySql.ListAuthors().Result.Count; + var createAuthorBatchArgs = Enumerable.Range(0, batchSize) + .Select(_ => GenerateRandom()) + .ToList(); + await QuerySql.CreateAuthorBatch(createAuthorBatchArgs); + var afterCountRows = QuerySql.ListAuthors().Result.Count; + ClassicAssert.AreEqual(beforeCountRows + batchSize, afterCountRows); + + QuerySql.CreateAuthorBatchArgs GenerateRandom() + { + return new QuerySql.CreateAuthorBatchArgs + { + Name = $"Author-{Randomizer.Next()}", + Bio = $"Bio-{Randomizer.Next()}" + }; + } + } } \ No newline at end of file diff --git a/EndToEndTests/Tests.cs b/EndToEndTests/Tests.cs index 0cae40ea..b1eaf531 100644 --- a/EndToEndTests/Tests.cs +++ b/EndToEndTests/Tests.cs @@ -8,14 +8,15 @@ public class Tests [Test] public async Task TestFlowOnMySql() { - ISqlDriverTester tester = new MySqlConnectorTester(); - await tester.TestFlow(); + var tester = new MySqlConnectorTester(); + await tester.TestBasicFlow(); } [Test] public async Task TestFlowOnPostgres() { - ISqlDriverTester tester = new NpgsqlTester(); - await tester.TestFlow(); + var tester = new NpgsqlTester(); + await tester.TestBasicFlow(); + await tester.TestCopyFlow(); } } \ No newline at end of file diff --git a/Extensions/ListExtensions.cs b/Extensions/ListExtensions.cs index b5d06b52..6c6b6956 100644 --- a/Extensions/ListExtensions.cs +++ b/Extensions/ListExtensions.cs @@ -16,4 +16,9 @@ public static string JoinByNewLine(this IEnumerable me) { return string.Join("\n", me); } + + public static string JoinByComma(this IEnumerable me) + { + return string.Join(", ", me); + } } \ No newline at end of file diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs index 2574c264..ce050f66 100644 --- a/Extensions/StringExtensions.cs +++ b/Extensions/StringExtensions.cs @@ -21,4 +21,9 @@ public static string FirstCharToLower(this string input) _ => string.Concat(input[0].ToString().ToLower(), input.AsSpan(1)) }; } + + public static string AppendSemicolonUnlessEmpty(this string input) + { + return input == string.Empty ? "" : $"{input};"; + } } \ No newline at end of file diff --git a/Makefile b/Makefile index 2fbececc..e3f1d2a4 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ protobuf-generate: # tests are run against generated code - can be generated either via a "process" or "wasm" SQLC plugins run-tests: - ./scripts/run_tests.sh + ./scripts/tests/run_end2end.sh # process type plugin dotnet-build-process: protobuf-generate dotnet-format diff --git a/NpgsqlExample/QuerySql.cs b/NpgsqlExample/QuerySql.cs index 2f412834..a7e570d5 100644 --- a/NpgsqlExample/QuerySql.cs +++ b/NpgsqlExample/QuerySql.cs @@ -9,14 +9,13 @@ namespace NpgsqlExample; public class QuerySql(string connectionString) { - private const string GetAuthorSql = "SELECT id, name, bio FROM authors WHERE id = @id LIMIT 1 "; + private const string GetAuthorSql = "SELECT id, name, bio FROM authors WHERE id = @id LIMIT 1"; public readonly record struct GetAuthorRow(long Id, string Name, string? Bio); public readonly record struct GetAuthorArgs(long Id); public async Task GetAuthor(GetAuthorArgs args) { { await using var connection = NpgsqlDataSource.Create(connectionString); - ; await using var command = connection.CreateCommand(GetAuthorSql); command.Parameters.AddWithValue("@id", args.Id); var reader = await command.ExecuteReaderAsync(); @@ -34,13 +33,12 @@ public class QuerySql(string connectionString) } } - private const string ListAuthorsSql = "SELECT id, name, bio FROM authors ORDER BY name "; + private const string ListAuthorsSql = "SELECT id, name, bio FROM authors ORDER BY name"; public readonly record struct ListAuthorsRow(long Id, string Name, string? Bio); public async Task> ListAuthors() { { await using var connection = NpgsqlDataSource.Create(connectionString); - ; await using var command = connection.CreateCommand(ListAuthorsSql); var reader = await command.ExecuteReaderAsync(); var result = new List(); @@ -53,14 +51,13 @@ public async Task> ListAuthors() } } - private const string CreateAuthorSql = "INSERT INTO authors ( name , bio ) VALUES ( @name, @bio ) RETURNING id, name, bio "; + private const string CreateAuthorSql = "INSERT INTO authors (name, bio) VALUES (@name, @bio) RETURNING id, name, bio"; public readonly record struct CreateAuthorRow(long Id, string Name, string? Bio); public readonly record struct CreateAuthorArgs(string Name, string? Bio); public async Task CreateAuthor(CreateAuthorArgs args) { { await using var connection = NpgsqlDataSource.Create(connectionString); - ; await using var command = connection.CreateCommand(CreateAuthorSql); command.Parameters.AddWithValue("@name", args.Name); command.Parameters.AddWithValue("@bio", args.Bio); @@ -79,26 +76,45 @@ public async Task> ListAuthors() } } - private const string DeleteAuthorSql = "DELETE FROM authors WHERE id = @id "; + private const string DeleteAuthorSql = "DELETE FROM authors WHERE id = @id"; public readonly record struct DeleteAuthorArgs(long Id); public async Task DeleteAuthor(DeleteAuthorArgs args) { { await using var connection = NpgsqlDataSource.Create(connectionString); - ; await using var command = connection.CreateCommand(DeleteAuthorSql); command.Parameters.AddWithValue("@id", args.Id); await command.ExecuteScalarAsync(); } } - private const string TestSql = "SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types LIMIT 1 "; + private const string CreateAuthorBatchSql = "COPY authors (name, bio) FROM STDIN (FORMAT BINARY)"; + public readonly record struct CreateAuthorBatchArgs(string Name, string? Bio); + public async Task CreateAuthorBatch(List args) + { + { + await using var ds = NpgsqlDataSource.Create(connectionString); + var connection = ds.CreateConnection(); + await connection.OpenAsync(); + await using var writer = await connection.BeginBinaryImportAsync(CreateAuthorBatchSql); + foreach (var row in args) + { + await writer.StartRowAsync(); + await writer.WriteAsync(row.Name); + await writer.WriteAsync(row.Bio); + } + + await writer.CompleteAsync(); + await connection.CloseAsync(); + } + } + + private const string TestSql = "SELECT c_bit, c_smallint, c_boolean, c_integer, c_bigint, c_serial, c_decimal, c_numeric, c_real, c_double_precision, c_date, c_time, c_timestamp, c_char, c_varchar, c_bytea, c_text, c_json FROM node_postgres_types LIMIT 1"; public readonly record struct TestRow(byte[]? C_bit, int? C_smallint, bool? C_boolean, int? C_integer, int? C_bigint, long? C_serial, float? C_decimal, float? C_numeric, float? C_real, float? C_double_precision, string? C_date, string? C_time, string? C_timestamp, string? C_char, string? C_varchar, byte[]? C_bytea, string? C_text, object? C_json); public async Task Test() { { await using var connection = NpgsqlDataSource.Create(connectionString); - ; await using var command = connection.CreateCommand(TestSql); var reader = await command.ExecuteReaderAsync(); if (await reader.ReadAsync()) diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index da3f3654..649ef009 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -1,23 +1,17 @@ -- name: GetAuthor :one -SELECT * FROM authors -WHERE id = $1 LIMIT 1; +SELECT * FROM authors WHERE id = $1 LIMIT 1; -- name: ListAuthors :many -SELECT * FROM authors -ORDER BY name; +SELECT * FROM authors ORDER BY name; -- name: CreateAuthor :one -INSERT INTO authors ( - name, bio -) VALUES ( - $1, $2 -) -RETURNING *; +INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING *; -- name: DeleteAuthor :exec -DELETE FROM authors -WHERE id = $1; +DELETE FROM authors WHERE id = $1; + +-- name: CreateAuthorBatch :copyfrom +INSERT INTO authors (name, bio) VALUES ($1, $2); /* name: Test :one */ -SELECT * FROM node_postgres_types -LIMIT 1; \ No newline at end of file +SELECT * FROM node_postgres_types LIMIT 1; \ No newline at end of file diff --git a/sqlc.local.yaml b/sqlc.local.yaml index 68be5cdc..a39dd2bf 100644 --- a/sqlc.local.yaml +++ b/sqlc.local.yaml @@ -12,6 +12,8 @@ sql: out: NpgsqlExample options: driver: Npgsql + targetFramework: net8.0 + generateCsproj: true - schema: "examples/authors/mysql/schema.sql" queries: "examples/authors/mysql/query.sql" engine: "mysql" @@ -19,4 +21,6 @@ sql: - plugin: csharp out: MySqlConnectorExample options: - driver: MySqlConnector \ No newline at end of file + driver: MySqlConnector + targetFramework: net8.0 + generateCsproj: true \ No newline at end of file