Skip to content

Commit df25a2a

Browse files
[release] 1. fix mysql :copyfrom implementation (#215)
2. fix copyfrom tests for mysql and postgres - testing more types and verifying that the data was loaded correctly 3. fix warnings in generated code by handling nulls correctly and adding required modifier when needed
1 parent 4619275 commit df25a2a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1402
-1232
lines changed

CodeGenerator/Generators/DataClassesGen.cs

+21-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.CodeAnalysis.CSharp.Syntax;
33
using Plugin;
44
using SqlcGenCsharp.Drivers;
5+
using System;
56
using System.Collections.Generic;
67
using System.Linq;
78
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@@ -18,28 +19,18 @@ public MemberDeclarationSyntax Generate(string name, ClassMember classMember, IL
1819
return GenerateAsCLass(className, columns);
1920
}
2021

21-
private RecordDeclarationSyntax GenerateAsRecord(string className, IList<Column> columns)
22+
private MemberDeclarationSyntax GenerateAsRecord(string className, IList<Column> columns)
2223
{
23-
return RecordDeclaration(Token(SyntaxKind.StructKeyword), className)
24-
.AddModifiers(
25-
Token(SyntaxKind.PublicKeyword),
26-
Token(SyntaxKind.ReadOnlyKeyword),
27-
Token(SyntaxKind.RecordKeyword)
28-
)
29-
.WithParameterList(ColumnsToParameterList())
30-
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
31-
32-
ParameterListSyntax ColumnsToParameterList()
33-
{
34-
var seenEmbed = new Dictionary<string, int>();
35-
return ParameterList(SeparatedList(columns
36-
.Select(column => Parameter(Identifier(GetFieldName(column, seenEmbed)))
37-
.WithType(ParseTypeName(dbDriver.GetCsharpType(column))))));
38-
}
24+
var seenEmbed = new Dictionary<string, int>();
25+
var recordParameters = columns
26+
.Select(column => $"{dbDriver.GetCsharpType(column)} {GetFieldName(column, seenEmbed)}")
27+
.JoinByComma();
28+
return ParseMemberDeclaration($"public readonly record struct {className} ({recordParameters});")!;
3929
}
4030

4131
private ClassDeclarationSyntax GenerateAsCLass(string className, IList<Column> columns)
4232
{
33+
var modernDotnetSupported = dbDriver.Options.DotnetFramework.LatestDotnetSupported();
4334
return ClassDeclaration(className)
4435
.AddModifiers(Token(SyntaxKind.PublicKeyword))
4536
.AddMembers(ColumnsToProperties())
@@ -48,8 +39,19 @@ private ClassDeclarationSyntax GenerateAsCLass(string className, IList<Column> c
4839
MemberDeclarationSyntax[] ColumnsToProperties()
4940
{
5041
var seenEmbed = new Dictionary<string, int>();
51-
return columns.Select(column => ParseMemberDeclaration(
52-
$"public {dbDriver.GetCsharpType(column)} {GetFieldName(column, seenEmbed)} {{ get; set; }}"))
42+
return columns.Select(column =>
43+
{
44+
var csharpType = dbDriver.GetCsharpType(column);
45+
var requiredModifierNeeded = modernDotnetSupported && // required modifier supported by .Net framework
46+
column.NotNull && // the field is not null, hence required
47+
!dbDriver.IsTypeNullableForAllRuntimes(csharpType); // TODO document
48+
var optionalRequiredModifier = requiredModifierNeeded ? "required" : string.Empty;
49+
var setterMethod = modernDotnetSupported ? "init" : "set";
50+
return ParseMemberDeclaration(
51+
$$"""
52+
public {{optionalRequiredModifier}} {{csharpType}} {{GetFieldName(column, seenEmbed)}} { get; {{setterMethod}}; }
53+
""");
54+
})
5355
.Cast<MemberDeclarationSyntax>()
5456
.ToArray();
5557
}

CodeGenerator/Generators/QueriesGen.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ internal class QueriesGen(DbDriver dbDriver, Options options, string namespaceNa
1717
"UnusedAutoPropertyAccessor.Global",
1818
"NotAccessedPositionalProperty.Global",
1919
"ConvertToUsingDeclaration",
20-
"UseAwaitUsing"
20+
"UseAwaitUsing",
21+
"UseObjectOrCollectionInitializer"
2122
];
2223

2324
private RootGen RootGen { get; } = new(options);

Drivers/DbDriver.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ public abstract class DbDriver(Options options, Dictionary<string, Table> tables
1515

1616
public Dictionary<string, Table> Tables { get; } = tables;
1717

18-
private HashSet<string> NullableTypesInAllRuntimes { get; } = ["long", "double", "int", "float", "bool", "DateTime"];
19-
2018
protected abstract List<ColumnMapping> ColumnMappings { get; }
2119

2220
public virtual UsingDirectiveSyntax[] GetUsingDirectives()
@@ -91,6 +89,9 @@ public string GetColumnReader(Column column, int ordinal)
9189

9290
public abstract string CreateSqlCommand(string sqlTextConstant);
9391

92+
private HashSet<string> NullableTypesInAllRuntimes { get; } = ["long", "double", "int", "float", "bool", "DateTime"];
93+
94+
// TODO move out from driver + rename
9495
public bool IsTypeNullableForAllRuntimes(string csharpType)
9596
{
9697
return NullableTypesInAllRuntimes.Contains(csharpType.Replace("?", ""));

Drivers/Generators/CommonGen.cs

+24-11
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,48 @@ public static string GetMethodParameterList(string argInterface, IEnumerable<Par
1313
: $"{argInterface} {Variable.Args.AsVarName()}")}";
1414
}
1515

16-
public string AddParametersToCommand(IEnumerable<Parameter> parameters)
16+
public static string AddParametersToCommand(IEnumerable<Parameter> parameters)
1717
{
1818
return parameters.Select(p =>
1919
{
2020
var commandVar = Variable.Command.AsVarName();
2121
var param = p.Column.Name.ToPascalCase();
22-
var nullCheck = dbDriver.Options.DotnetFramework.LatestDotnetSupported() && !p.Column.NotNull ? "!" : "";
22+
var argsVar = Variable.Args.AsVarName();
2323
return p.Column.IsSqlcSlice
2424
? $$"""
25-
for (int i = 0; i < {{Variable.Args.AsVarName()}}.{{param}}.Length; i++)
26-
{{commandVar}}.Parameters.AddWithValue($"@{{param}}Arg{i}", {{Variable.Args.AsVarName()}}.{{param}}[i]);
25+
for (int i = 0; i < {{argsVar}}.{{param}}.Length; i++)
26+
{{commandVar}}.Parameters.AddWithValue($"@{{param}}Arg{i}", {{argsVar}}.{{param}}[i]);
2727
"""
28-
: $"{commandVar}.Parameters.AddWithValue(\"@{p.Column.Name}\", args.{param}{nullCheck});";
28+
: $$"""
29+
if ({{argsVar}}.{{param}} != null)
30+
{{commandVar}}.Parameters.AddWithValue("@{{p.Column.Name}}", {{argsVar}}.{{param}});
31+
""";
2932
}).JoinByNewLine();
3033
}
3134

3235
public static string ConstructDapperParamsDict(IList<Parameter> parameters)
3336
{
3437
if (!parameters.Any()) return string.Empty;
3538
var initParamsDict = $"var {Variable.QueryParams.AsVarName()} = new Dictionary<string, object>();";
39+
var argsVar = Variable.Args.AsVarName();
40+
var queryParamsVar = Variable.QueryParams.AsVarName();
41+
3642
var dapperParamsCommands = parameters.Select(p =>
3743
{
3844
var param = p.Column.Name.ToPascalCase();
39-
return p.Column.IsSqlcSlice
40-
? $$"""
41-
for (int i = 0; i < {{Variable.Args.AsVarName()}}.{{param}}.Length; i++)
42-
{{Variable.QueryParams.AsVarName()}}.Add($"@{{param}}Arg{i}", {{Variable.Args.AsVarName()}}.{{param}}[i]);
43-
"""
44-
: $"{Variable.QueryParams.AsVarName()}.Add(\"{p.Column.Name}\", {Variable.Args.AsVarName()}.{param});";
45+
if (p.Column.IsSqlcSlice)
46+
return $$"""
47+
for (int i = 0; i < {{argsVar}}.{{param}}.Length; i++)
48+
{{queryParamsVar}}.Add($"@{{param}}Arg{i}", {{argsVar}}.{{param}}[i]);
49+
""";
50+
51+
var addParamToDict = $"{queryParamsVar}.Add(\"{p.Column.Name}\", {argsVar}.{param});";
52+
return p.Column.NotNull
53+
? addParamToDict
54+
: $"""
55+
if ({argsVar}.{param} != null)
56+
{addParamToDict}
57+
""";
4558
});
4659

4760
return $$"""

Drivers/MySqlConnectorDriver.cs

+33-14
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public partial class MySqlConnectorDriver(Options options, Dictionary<string, Ta
3838
{ "time", null },
3939
{ "tinytext", null },
4040
{ "varchar", null },
41-
{ "var_string", null }
41+
{ "var_string", null },
42+
{ "json", null }
4243
}, ordinal => $"reader.GetString({ordinal})"),
4344
new("DateTime",
4445
new Dictionary<string, string?> { { "date", null }, { "datetime", null }, { "timestamp", null } }, ordinal => $"reader.GetDateTime({ordinal})"),
@@ -54,25 +55,30 @@ public partial class MySqlConnectorDriver(Options options, Dictionary<string, Ta
5455
new("double",
5556
new Dictionary<string, string?> { { "double", null }, { "float", null } }, ordinal => $"reader.GetDouble({ordinal})"),
5657
new("object",
57-
new Dictionary<string, string?> { { "json", null } }, ordinal => $"reader.GetString({ordinal})")
58+
new Dictionary<string, string?> { }, ordinal => $"reader.GetValue({ordinal})")
5859
];
5960

6061
public override UsingDirectiveSyntax[] GetUsingDirectives()
6162
{
6263
return base.GetUsingDirectives()
63-
.Append(UsingDirective(ParseName("MySqlConnector")))
64-
.Append(UsingDirective(ParseName("System.Globalization")))
65-
.Append(UsingDirective(ParseName("System.IO")))
66-
.Append(UsingDirective(ParseName("CsvHelper")))
67-
.Append(UsingDirective(ParseName("CsvHelper.Configuration")))
64+
.Concat(
65+
[
66+
UsingDirective(ParseName("MySqlConnector")),
67+
UsingDirective(ParseName("System.Globalization")),
68+
UsingDirective(ParseName("System.IO")),
69+
UsingDirective(ParseName("CsvHelper")),
70+
UsingDirective(ParseName("CsvHelper.Configuration")),
71+
UsingDirective(ParseName("CsvHelper.TypeConversion")),
72+
UsingDirective(ParseName("System.Text"))
73+
])
6874
.ToArray();
6975
}
7076

7177
public override ConnectionGenCommands EstablishConnection(Query query)
7278
{
7379
return new ConnectionGenCommands(
7480
$"var {Variable.Connection.AsVarName()} = new MySqlConnection({GetConnectionStringField()})",
75-
$"{Variable.Connection.AsVarName()}.Open()"
81+
$"await {Variable.Connection.AsVarName()}.OpenAsync()"
7682
);
7783
}
7884

@@ -137,26 +143,39 @@ public string GetCopyFromImpl(Query query, string queryTextConstant)
137143
const string tempCsvFilename = "input.csv";
138144
const string csvDelimiter = ",";
139145

146+
var csvWriterVar = Variable.CsvWriter.AsVarName();
147+
var loaderVar = Variable.Loader.AsVarName();
148+
var connectionVar = Variable.Connection.AsVarName();
149+
150+
var loaderColumns = query.Params.Select(p => $"\"{p.Column.Name}\"").JoinByComma();
140151
var (establishConnection, connectionOpen) = EstablishConnection(query);
141152
return $$"""
153+
const string supportedDateTimeFormat = "yyyy-MM-dd H:mm:ss";
142154
var {{Variable.Config.AsVarName()}} = new CsvConfiguration(CultureInfo.CurrentCulture) { Delimiter = "{{csvDelimiter}}" };
143-
using (var {{Variable.Writer.AsVarName()}} = new StreamWriter("{{tempCsvFilename}}"))
144-
using (var {{Variable.CsvWriter.AsVarName()}} = new CsvWriter({{Variable.Writer.AsVarName()}}, {{Variable.Config.AsVarName()}}))
145-
await {{Variable.CsvWriter.AsVarName()}}.WriteRecordsAsync({{Variable.Args.AsVarName()}});
155+
using (var {{Variable.Writer.AsVarName()}} = new StreamWriter("{{tempCsvFilename}}", false, new UTF8Encoding(false)))
156+
using (var {{csvWriterVar}} = new CsvWriter({{Variable.Writer.AsVarName()}}, {{Variable.Config.AsVarName()}}))
157+
{
158+
var options = new TypeConverterOptions { Formats = new[] { supportedDateTimeFormat } };
159+
{{csvWriterVar}}.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
160+
{{csvWriterVar}}.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
161+
await {{csvWriterVar}}.WriteRecordsAsync({{Variable.Args.AsVarName()}});
162+
}
146163
147164
using ({{establishConnection}})
148165
{
149166
{{connectionOpen.AppendSemicolonUnlessEmpty()}}
150-
var {{Variable.Loader.AsVarName()}} = new MySqlBulkLoader({{Variable.Connection.AsVarName()}})
167+
var {{loaderVar}} = new MySqlBulkLoader({{connectionVar}})
151168
{
152169
Local = true,
153170
TableName = "{{query.InsertIntoTable.Name}}",
154171
FieldTerminator = "{{csvDelimiter}}",
155172
FileName = "{{tempCsvFilename}}",
173+
FieldQuotationCharacter = '"',
156174
NumberOfLinesToSkip = 1
157175
};
158-
await {{Variable.Loader.AsVarName()}}.LoadAsync();
159-
await {{Variable.Connection.AsVarName()}}.CloseAsync();
176+
{{loaderVar}}.Columns.AddRange(new List<string> { {{loaderColumns}} });
177+
await {{loaderVar}}.LoadAsync();
178+
await {{connectionVar}}.CloseAsync();
160179
}
161180
""";
162181
}

Drivers/NpgsqlDriver.cs

+18-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ public NpgsqlDriver(Options options, Dictionary<string, Table> tables) : base(op
2525
protected sealed override List<ColumnMapping> ColumnMappings { get; } =
2626
[
2727
new("long",
28-
new Dictionary<string, string?> { { "int8", null }, { "bigint", null }, { "bigserial", null } }
28+
new Dictionary<string, string?>
29+
{
30+
{ "int8", "NpgsqlDbType.Bigint" },
31+
{ "bigint", "NpgsqlDbType.Bigint" },
32+
{ "bigserial", "NpgsqlDbType.Bigint" }
33+
}
2934
, ordinal => $"reader.GetInt64({ordinal})"),
3035
new("byte[]",
3136
new Dictionary<string, string?>
@@ -63,16 +68,23 @@ public NpgsqlDriver(Options options, Dictionary<string, Table> tables) : base(op
6368
{
6469
{ "integer", "NpgsqlDbType.Integer" },
6570
{ "int", "NpgsqlDbType.Integer" },
66-
{ "int2", null },
71+
{ "int2", "NpgsqlDbType.Smallint" },
6772
{ "int4", "NpgsqlDbType.Integer" },
68-
{ "serial", null }
73+
{ "serial", "NpgsqlDbType.Integer" }
6974
}, ordinal => $"reader.GetInt32({ordinal})",
7075
ordinal => $"reader.GetFieldValue<int[]>({ordinal})"),
7176
new("float",
72-
new Dictionary<string, string?> { { "numeric", null }, { "float4", null }, { "float8", null } }
73-
, ordinal => $"reader.GetFloat({ordinal})"),
77+
new Dictionary<string, string?>
78+
{
79+
{ "numeric", "NpgsqlDbType.Numeric" },
80+
{ "float4", "NpgsqlDbType.Real" },
81+
{ "float8", "NpgsqlDbType.Real" }
82+
}, ordinal => $"reader.GetFloat({ordinal})"),
7483
new("decimal",
75-
new Dictionary<string, string?> { { "decimal", null } }, ordinal => $"reader.GetDecimal({ordinal})"),
84+
new Dictionary<string, string?>
85+
{
86+
{ "decimal", "NpgsqlDbType.Real" }
87+
}, ordinal => $"reader.GetDecimal({ordinal})"),
7688
new("bool",
7789
new Dictionary<string, string?> { { "bool", null }, { "boolean", null } }, ordinal => $"reader.GetBoolean({ordinal})")
7890
];

0 commit comments

Comments
 (0)