Skip to content

Commit f6a9b60

Browse files
feat: add option to override either integer or string to NodaTime.Instant in SQLite
1 parent 6d6bbb1 commit f6a9b60

32 files changed

Lines changed: 673 additions & 181 deletions

Drivers/DbDriver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public static string TransformQueryForSliceArgs(string originalSql, int sliceSiz
6969
throw new InvalidOperationException("Transaction is provided, but its connection is null.");
7070
""";
7171

72-
protected static readonly SqlMapperImplFunc NodaInstantTypeHandler = _ => $$"""
72+
protected static readonly SqlMapperImplFunc DateTimeNodaInstantTypeHandler = _ => $$"""
7373
private class NodaInstantTypeHandler : SqlMapper.TypeHandler<Instant>
7474
{
7575
public override Instant Parse(object value)

Drivers/MySqlConnectorDriver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public sealed partial class MySqlConnectorDriver(
144144
},
145145
usingDirectives: ["System", "NodaTime", "NodaTime.Extensions"],
146146
sqlMapper: "SqlMapper.AddTypeHandler(typeof(Instant), new NodaInstantTypeHandler());",
147-
sqlMapperImpl: NodaInstantTypeHandler
147+
sqlMapperImpl: DateTimeNodaInstantTypeHandler
148148
),
149149

150150

Drivers/NpgsqlDriver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public NpgsqlDriver(
157157
},
158158
usingDirectives: ["System", "NodaTime", "NodaTime.Extensions"],
159159
sqlMapper: "SqlMapper.AddTypeHandler(typeof(Instant), new NodaInstantTypeHandler());",
160-
sqlMapperImpl: NodaInstantTypeHandler
160+
sqlMapperImpl: DateTimeNodaInstantTypeHandler
161161
),
162162

163163
/* Unstructured data types */

Drivers/SqliteDriver.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,25 @@ public sealed partial class SqliteDriver(
1919

2020
private const string DateTimeStringFormat = "yyyy-MM-dd HH:mm:ss"; // Default format for DateTime strings - TODO make configurable via Options
2121

22+
private static readonly SqlMapperImplFunc NodaInstantTypeHandler = _ => $$"""
23+
private class NodaInstantTypeHandler : SqlMapper.TypeHandler<Instant>
24+
{
25+
public override Instant Parse(object value)
26+
{
27+
if (value is string s)
28+
return InstantPattern.CreateWithInvariantCulture("{{DateTimeStringFormat}}").Parse(s).Value;
29+
if (value is long l)
30+
return Instant.FromUnixTimeSeconds(l);
31+
throw new DataException($"Cannot convert {value?.GetType()} to Instant");
32+
}
33+
34+
public override void SetValue(IDbDataParameter parameter, Instant value)
35+
{
36+
parameter.Value = value;
37+
}
38+
}
39+
""";
40+
2241
protected override Dictionary<string, ColumnMapping> ColumnMappings { get; } =
2342
new()
2443
{
@@ -71,6 +90,25 @@ public sealed partial class SqliteDriver(
7190
sqlMapper: "SqlMapper.AddTypeHandler(typeof(DateTime), new DateTimeTypeHandler());",
7291
sqlMapperImpl: DateTimeTypeHandler
7392
),
93+
["Instant"] = new(
94+
[],
95+
readerFn: (ordinal, dbType) =>
96+
{
97+
if (IntegerDbTypes.Contains(dbType.ToLower()))
98+
return $"Instant.FromUnixTimeSeconds({Variable.Reader.AsVarName()}.GetInt32({ordinal}))";
99+
return $"InstantPattern.CreateWithInvariantCulture(\"{DateTimeStringFormat}\").Parse({Variable.Reader.AsVarName()}.GetString({ordinal})).Value";
100+
},
101+
writerFn: (el, dbType, notNull, isDapper, isLegacy) =>
102+
{
103+
var nullValue = isDapper ? "null" : "(object)DBNull.Value";
104+
if (IntegerDbTypes.Contains(dbType.ToLower()))
105+
return $"{el} != null ? (long?) {el}.Value.ToUnixTimeSeconds() : {nullValue}";
106+
return $"{el} != null ? InstantPattern.CreateWithInvariantCulture(\"{DateTimeStringFormat}\").Format({el}.Value) : {nullValue}";
107+
},
108+
usingDirectives: ["NodaTime", "NodaTime.Extensions", "NodaTime.Text"],
109+
sqlMapper: "SqlMapper.AddTypeHandler(typeof(Instant), new NodaInstantTypeHandler());",
110+
sqlMapperImpl: NodaInstantTypeHandler
111+
),
74112
["bool"] = new(
75113
[],
76114
readerFn: (ordinal, dbType) =>

end2end/EndToEndScaffold/Templates/SqliteTests.cs

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,35 @@ public static class SqliteTests
99
[KnownTestType.SqliteDataTypes] = new TestImpl
1010
{
1111
Impl = $$"""
12+
private static IEnumerable<TestCaseData> SqliteTypesTestCases
13+
{
14+
get
15+
{
16+
yield return new TestCaseData(
17+
-54355,
18+
9787.66m,
19+
"Songs of Love and Hate",
20+
new byte[] { 0x15, 0x20, 0x33 },
21+
true,
22+
false,
23+
DateTime.SpecifyKind(DateTime.Parse("2020-01-01 14:15:16"), DateTimeKind.Utc),
24+
DateTime.SpecifyKind(DateTime.Parse("2025-01-01 17:18:19"), DateTimeKind.Utc),
25+
Instant.FromUtc(2025, 10, 15, 19, 55, 2),
26+
Instant.FromUtc(1993, 9, 27, 03, 55, 2)
27+
).SetName("SqliteTypes with values");
28+
29+
yield return new TestCaseData(
30+
null, null, null, new byte[] { }, null, null, null, null, null, null
31+
).SetName("SqliteTypes with empty values");
32+
33+
yield return new TestCaseData(
34+
null, null, null, null, null, null, null, null, null, null
35+
).SetName("SqliteTypes with null values");
36+
}
37+
}
38+
1239
[Test]
13-
[TestCase(-54355, 9787.66, "Songs of Love and Hate", new byte[] { 0x15, 0x20, 0x33 }, true, false, "2020-01-01 14:15:16", "2025-01-01 17:18:19")]
14-
[TestCase(null, null, null, new byte[] { }, null, null, null, null)]
15-
[TestCase(null, null, null, null, null, null, null, null)]
40+
[TestCaseSource(nameof(SqliteTypesTestCases))]
1641
public async Task TestSqliteTypes(
1742
int? cInteger,
1843
decimal? cReal,
@@ -21,13 +46,10 @@ public async Task TestSqliteTypes(
2146
bool? cTextBoolOverride,
2247
bool? cIntegerBoolOverride,
2348
DateTime? cTextDatetimeOverride,
24-
DateTime? cIntegerDatetimeOverride)
49+
DateTime? cIntegerDatetimeOverride,
50+
Instant? cTextNodaInstantOverride,
51+
Instant? cIntegerNodaInstantOverride)
2552
{
26-
if (cTextDatetimeOverride.HasValue && cTextDatetimeOverride.Value.Kind != DateTimeKind.Utc)
27-
cTextDatetimeOverride = DateTime.SpecifyKind(cTextDatetimeOverride.Value, DateTimeKind.Utc);
28-
if (cIntegerDatetimeOverride.HasValue && cIntegerDatetimeOverride.Value.Kind != DateTimeKind.Utc)
29-
cIntegerDatetimeOverride = DateTime.SpecifyKind(cIntegerDatetimeOverride.Value, DateTimeKind.Utc);
30-
3153
await QuerySql.InsertSqliteTypes(new QuerySql.InsertSqliteTypesArgs
3254
{
3355
CInteger = cInteger,
@@ -37,7 +59,9 @@ await QuerySql.InsertSqliteTypes(new QuerySql.InsertSqliteTypesArgs
3759
CTextBoolOverride = cTextBoolOverride,
3860
CIntegerBoolOverride = cIntegerBoolOverride,
3961
CTextDatetimeOverride = cTextDatetimeOverride,
40-
CIntegerDatetimeOverride = cIntegerDatetimeOverride
62+
CIntegerDatetimeOverride = cIntegerDatetimeOverride,
63+
CTextNodaInstantOverride = cTextNodaInstantOverride,
64+
CIntegerNodaInstantOverride = cIntegerNodaInstantOverride
4165
});
4266
4367
var expected = new QuerySql.GetSqliteTypesRow
@@ -49,7 +73,9 @@ await QuerySql.InsertSqliteTypes(new QuerySql.InsertSqliteTypesArgs
4973
CTextBoolOverride = cTextBoolOverride,
5074
CIntegerBoolOverride = cIntegerBoolOverride,
5175
CTextDatetimeOverride = cTextDatetimeOverride,
52-
CIntegerDatetimeOverride = cIntegerDatetimeOverride
76+
CIntegerDatetimeOverride = cIntegerDatetimeOverride,
77+
CTextNodaInstantOverride = cTextNodaInstantOverride,
78+
CIntegerNodaInstantOverride = cIntegerNodaInstantOverride
5379
};
5480
var actual = await QuerySql.GetSqliteTypes();
5581
AssertSingularEquals(expected, actual{{Consts.UnknownRecordValuePlaceholder}});
@@ -62,24 +88,10 @@ void AssertSingularEquals(QuerySql.GetSqliteTypesRow x, QuerySql.GetSqliteTypesR
6288
Assert.That(x.CBlob, Is.EqualTo(y.CBlob));
6389
Assert.That(x.CTextBoolOverride, Is.EqualTo(y.CTextBoolOverride));
6490
Assert.That(x.CIntegerBoolOverride, Is.EqualTo(y.CIntegerBoolOverride));
65-
AssertDateTimeEquals(x.CTextDatetimeOverride, y.CTextDatetimeOverride);
66-
AssertDateTimeEquals(x.CIntegerDatetimeOverride, y.CIntegerDatetimeOverride);
67-
}
68-
69-
void AssertDateTimeEquals(DateTime? x, DateTime? y)
70-
{
71-
Assert.That(x.HasValue, Is.EqualTo(y.HasValue));
72-
if (!x.HasValue)
73-
return;
74-
75-
var xv = x.Value;
76-
var yv = y.Value;
77-
Assert.That(xv.Year, Is.EqualTo(yv.Year));
78-
Assert.That(xv.Month, Is.EqualTo(yv.Month));
79-
Assert.That(xv.Day, Is.EqualTo(yv.Day));
80-
Assert.That(xv.Hour, Is.EqualTo(yv.Hour));
81-
Assert.That(xv.Minute, Is.EqualTo(yv.Minute));
82-
Assert.That(xv.Second, Is.EqualTo(yv.Second));
91+
Assert.That(x.CTextDatetimeOverride, Is.EqualTo(y.CTextDatetimeOverride));
92+
Assert.That(x.CIntegerDatetimeOverride, Is.EqualTo(y.CIntegerDatetimeOverride));
93+
Assert.That(x.CTextNodaInstantOverride, Is.EqualTo(y.CTextNodaInstantOverride));
94+
Assert.That(x.CIntegerNodaInstantOverride, Is.EqualTo(y.CIntegerNodaInstantOverride));
8395
}
8496
}
8597
"""

end2end/EndToEndTests/SqliteDapperTester.generated.cs

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -328,17 +328,21 @@ void AssertSingularEquals(QuerySql.GetAuthorByNamePatternRow x, QuerySql.GetAuth
328328
}
329329
}
330330

331+
private static IEnumerable<TestCaseData> SqliteTypesTestCases
332+
{
333+
get
334+
{
335+
yield return new TestCaseData(-54355, 9787.66m, "Songs of Love and Hate", new byte[] { 0x15, 0x20, 0x33 }, true, false, DateTime.SpecifyKind(DateTime.Parse("2020-01-01 14:15:16"), DateTimeKind.Utc), DateTime.SpecifyKind(DateTime.Parse("2025-01-01 17:18:19"), DateTimeKind.Utc), Instant.FromUtc(2025, 10, 15, 19, 55, 2), Instant.FromUtc(1993, 9, 27, 03, 55, 2)).SetName("SqliteTypes with values");
336+
yield return new TestCaseData(null, null, null, new byte[] { }, null, null, null, null, null, null).SetName("SqliteTypes with empty values");
337+
yield return new TestCaseData(null, null, null, null, null, null, null, null, null, null).SetName("SqliteTypes with null values");
338+
}
339+
}
340+
331341
[Test]
332-
[TestCase(-54355, 9787.66, "Songs of Love and Hate", new byte[] { 0x15, 0x20, 0x33 }, true, false, "2020-01-01 14:15:16", "2025-01-01 17:18:19")]
333-
[TestCase(null, null, null, new byte[] { }, null, null, null, null)]
334-
[TestCase(null, null, null, null, null, null, null, null)]
335-
public async Task TestSqliteTypes(int? cInteger, decimal? cReal, string cText, byte[] cBlob, bool? cTextBoolOverride, bool? cIntegerBoolOverride, DateTime? cTextDatetimeOverride, DateTime? cIntegerDatetimeOverride)
342+
[TestCaseSource(nameof(SqliteTypesTestCases))]
343+
public async Task TestSqliteTypes(int? cInteger, decimal? cReal, string cText, byte[] cBlob, bool? cTextBoolOverride, bool? cIntegerBoolOverride, DateTime? cTextDatetimeOverride, DateTime? cIntegerDatetimeOverride, Instant? cTextNodaInstantOverride, Instant? cIntegerNodaInstantOverride)
336344
{
337-
if (cTextDatetimeOverride.HasValue && cTextDatetimeOverride.Value.Kind != DateTimeKind.Utc)
338-
cTextDatetimeOverride = DateTime.SpecifyKind(cTextDatetimeOverride.Value, DateTimeKind.Utc);
339-
if (cIntegerDatetimeOverride.HasValue && cIntegerDatetimeOverride.Value.Kind != DateTimeKind.Utc)
340-
cIntegerDatetimeOverride = DateTime.SpecifyKind(cIntegerDatetimeOverride.Value, DateTimeKind.Utc);
341-
await QuerySql.InsertSqliteTypes(new QuerySql.InsertSqliteTypesArgs { CInteger = cInteger, CReal = cReal, CText = cText, CBlob = cBlob, CTextBoolOverride = cTextBoolOverride, CIntegerBoolOverride = cIntegerBoolOverride, CTextDatetimeOverride = cTextDatetimeOverride, CIntegerDatetimeOverride = cIntegerDatetimeOverride });
345+
await QuerySql.InsertSqliteTypes(new QuerySql.InsertSqliteTypesArgs { CInteger = cInteger, CReal = cReal, CText = cText, CBlob = cBlob, CTextBoolOverride = cTextBoolOverride, CIntegerBoolOverride = cIntegerBoolOverride, CTextDatetimeOverride = cTextDatetimeOverride, CIntegerDatetimeOverride = cIntegerDatetimeOverride, CTextNodaInstantOverride = cTextNodaInstantOverride, CIntegerNodaInstantOverride = cIntegerNodaInstantOverride });
342346
var expected = new QuerySql.GetSqliteTypesRow
343347
{
344348
CInteger = cInteger,
@@ -348,7 +352,9 @@ public async Task TestSqliteTypes(int? cInteger, decimal? cReal, string cText, b
348352
CTextBoolOverride = cTextBoolOverride,
349353
CIntegerBoolOverride = cIntegerBoolOverride,
350354
CTextDatetimeOverride = cTextDatetimeOverride,
351-
CIntegerDatetimeOverride = cIntegerDatetimeOverride
355+
CIntegerDatetimeOverride = cIntegerDatetimeOverride,
356+
CTextNodaInstantOverride = cTextNodaInstantOverride,
357+
CIntegerNodaInstantOverride = cIntegerNodaInstantOverride
352358
};
353359
var actual = await QuerySql.GetSqliteTypes();
354360
AssertSingularEquals(expected, actual);
@@ -360,23 +366,10 @@ void AssertSingularEquals(QuerySql.GetSqliteTypesRow x, QuerySql.GetSqliteTypesR
360366
Assert.That(x.CBlob, Is.EqualTo(y.CBlob));
361367
Assert.That(x.CTextBoolOverride, Is.EqualTo(y.CTextBoolOverride));
362368
Assert.That(x.CIntegerBoolOverride, Is.EqualTo(y.CIntegerBoolOverride));
363-
AssertDateTimeEquals(x.CTextDatetimeOverride, y.CTextDatetimeOverride);
364-
AssertDateTimeEquals(x.CIntegerDatetimeOverride, y.CIntegerDatetimeOverride);
365-
}
366-
367-
void AssertDateTimeEquals(DateTime? x, DateTime? y)
368-
{
369-
Assert.That(x.HasValue, Is.EqualTo(y.HasValue));
370-
if (!x.HasValue)
371-
return;
372-
var xv = x.Value;
373-
var yv = y.Value;
374-
Assert.That(xv.Year, Is.EqualTo(yv.Year));
375-
Assert.That(xv.Month, Is.EqualTo(yv.Month));
376-
Assert.That(xv.Day, Is.EqualTo(yv.Day));
377-
Assert.That(xv.Hour, Is.EqualTo(yv.Hour));
378-
Assert.That(xv.Minute, Is.EqualTo(yv.Minute));
379-
Assert.That(xv.Second, Is.EqualTo(yv.Second));
369+
Assert.That(x.CTextDatetimeOverride, Is.EqualTo(y.CTextDatetimeOverride));
370+
Assert.That(x.CIntegerDatetimeOverride, Is.EqualTo(y.CIntegerDatetimeOverride));
371+
Assert.That(x.CTextNodaInstantOverride, Is.EqualTo(y.CTextNodaInstantOverride));
372+
Assert.That(x.CIntegerNodaInstantOverride, Is.EqualTo(y.CIntegerNodaInstantOverride));
380373
}
381374
}
382375

0 commit comments

Comments
 (0)