Skip to content

Commit 1efeb21

Browse files
committed
Ensure that CreateSchema are NativeAOT compliant. #1367
1 parent 675cbbc commit 1efeb21

4 files changed

Lines changed: 59 additions & 4 deletions

File tree

‎src/Providers/RepoDb.Core/RepoDb.NativeAotTests/RepoDb.NativeAotTests.csproj‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
<NoWarn>$(NoWarn);MA0002;MA0004;MA0006;MA0011;MA0048;MA0051</NoWarn>
2020
</PropertyGroup>
2121
<ItemGroup>
22+
<!-- Analyze every schema extension, including methods outside the smoke test's runtime paths. -->
23+
<TrimmerRootAssembly Include="RepoDb.Schema.Core" RootMode="All" />
2224
<ProjectReference Include="..\..\RepoDb.Sqlite.Microsoft\RepoDb.Sqlite.Microsoft\RepoDb.Sqlite.Microsoft.csproj" />
2325
<ProjectReference Include="..\..\..\Schema\RepoDb.Schema.Sqlite.Microsoft\RepoDb.Schema.Sqlite.Microsoft\RepoDb.Schema.Sqlite.Microsoft.csproj" />
2426
</ItemGroup>

‎src/Providers/RepoDb.Core/RepoDb.NativeAotTests/SchemaDefinitionTests.cs‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,28 @@
1212
using System.Text.Json;
1313
using System.Threading.Tasks;
1414
using Microsoft.Data.Sqlite;
15+
using RepoDb.Attributes;
1516
using RepoDb.Schema;
1617
using RepoDb.Schema.Enumerations;
18+
using RepoDb.Schema.Models;
1719
using RepoDb.Schema.Sqlite.Microsoft;
1820

1921
namespace RepoDb.NativeAotTests
2022
{
2123
public static partial class Program
2224
{
25+
[Map("Invoice")]
26+
private sealed class SchemaInvoice
27+
{
28+
public long Id { get; set; }
29+
}
30+
2331
/// <summary>
2432
/// Exercises the async exporters with real catalog reads and reflection-based JSON serialization disabled.
2533
/// </summary>
2634
private static async Task TestSchemaDefinitionsAsync()
2735
{
28-
await RunAsync("Async schema definitions (JSON, XML, YAML; every relationship behavior)", async () =>
36+
await RunAsync("Schema save/read/copy extensions (sync/async; every relationship behavior)", async () =>
2937
{
3038
if (JsonSerializer.IsReflectionEnabledByDefault) throw new Exception("Reflection-based JSON serialization must be disabled.");
3139
using var connection = new SqliteConnection("Data Source=:memory:");
@@ -58,7 +66,48 @@ Amount decimal(18,4) NOT NULL DEFAULT 0, Name nvarchar(100),
5866
else await connection.SaveDefinitionAsYamlAsync("Invoice", filename, behavior);
5967
if (!expected.SequenceEqual(File.ReadAllBytes(filename)))
6068
throw new Exception("Async schema definition differs from synchronous output: " + format + "/" + behavior);
69+
SchemaDefinitionResult synchronous;
70+
SchemaDefinitionResult asynchronous;
71+
if (format == "json")
72+
{
73+
synchronous = connection.ReadDefinitionFromJson(filename);
74+
asynchronous = await connection.ReadDefinitionFromJsonAsync(filename);
75+
}
76+
else if (format == "xml")
77+
{
78+
synchronous = connection.ReadDefinitionFromXml(filename);
79+
asynchronous = await connection.ReadDefinitionFromXmlAsync(filename);
80+
}
81+
else
82+
{
83+
synchronous = connection.ReadDefinitionFromYaml(filename);
84+
asynchronous = await connection.ReadDefinitionFromYamlAsync(filename);
85+
}
86+
var count = behavior == RelatedTableBehavior.TableOnly ? 1
87+
: behavior == RelatedTableBehavior.ParentsAndChildren ? 3 : 2;
88+
if (synchronous.Tables.Count != count || asynchronous.Tables.Count != count ||
89+
!System.Text.Json.Nodes.JsonNode.DeepEquals(synchronous.Definition, asynchronous.Definition))
90+
throw new Exception("Schema readers did not preserve the saved definition.");
91+
var invoice = asynchronous.Tables.Single(table => table.Table.Name == "Invoice");
92+
if (invoice.Columns.Count != 4 || invoice.PrimaryKey == null || invoice.Indexes.Count != 1 ||
93+
invoice.CheckConstraints.Count != 1 || invoice.ForeignKeys.Count != (behavior == RelatedTableBehavior.TableOnly ? 0 : 1))
94+
throw new Exception("Schema readers did not reconstruct the table metadata.");
6195
}
96+
97+
SchemaComposerMapper.Add<SqliteConnection>(new SqliteSchemaComposer(), true);
98+
using var target = new SqliteConnection("Data Source=:memory:");
99+
await target.OpenAsync();
100+
var copy = connection.CopyTableSchemaTo<SchemaInvoice>(target, relationshipBehavior: behavior);
101+
var copyAsync = await connection.CopyTableSchemaToAsync<SchemaInvoice>(target,
102+
tableExistenceBehavior: TableExistsBehavior.Drop, relationshipBehavior: behavior);
103+
if (copy.Errors.Count != 0 || copyAsync.Errors.Count != 0 || copy.Items.Count != copyAsync.Items.Count)
104+
throw new Exception("Generic schema copy failed.");
105+
var multiple = connection.CopyTableSchemaTo(new[] { "Invoice" }, target,
106+
tableExistenceBehavior: TableExistsBehavior.Skip, relationshipBehavior: behavior);
107+
var multipleAsync = await connection.CopyTableSchemaToAsync(new[] { "Invoice" }, target,
108+
tableExistenceBehavior: TableExistsBehavior.Skip, relationshipBehavior: behavior);
109+
if (multiple.Errors.Count != 0 || multipleAsync.Errors.Count != 0)
110+
throw new Exception("Multiple-table schema copy failed.");
62111
}
63112
}
64113
finally

‎src/Schema/RepoDb.Schema.Core/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ await connection.SaveDefinitionAsJsonAsync("sales.Invoice", "invoice.json",
3434

3535
The async methods use `ISchemaReader`'s asynchronous catalog operations sequentially, forwarding cancellation and the scoped query settings. Provider readers use asynchronous trace callbacks. Definition construction and serialization run in memory; file writes and flushing use asynchronous IO. Cancellation is checked before committing the complete temporary file. Query, serialization, write, or cancellation failures leave an existing destination intact and remove the temporary file. Replacement itself is a synchronous filesystem operation; cancellation after the commit is not reported as a failed save.
3636

37-
The exporters use explicit JSON nodes and XML construction, without reflection-based object serialization, runtime code generation, or `Task.Run`. The NativeAOT smoke project at `src/Providers/RepoDb.Core/RepoDb.NativeAotTests` exercises all three async exporters and every relationship behavior against SQLite, with `JsonSerializerIsReflectionEnabledByDefault=false`, and compares the output with synchronous saves. Publish it with `dotnet publish -c Release -r win-x64` from that project directory, then run the published executable. Native publishing requires the platform's native toolchain (on Windows, the Visual C++ build tools and Windows SDK). Application-level NativeAOT compatibility also depends on the selected database provider and any custom schema reader or trace implementation.
37+
The exporters use explicit JSON nodes and XML construction, without reflection-based object serialization, runtime code generation, or `Task.Run`. The NativeAOT smoke project at `src/Providers/RepoDb.Core/RepoDb.NativeAotTests` roots the entire schema core assembly for compilation and exercises synchronous/asynchronous saves, reads, and generic and multiple-table copies against SQLite, for every relationship behavior. It disables reflection-based JSON serialization and compares async saves with synchronous output. Schema creation binds dictionary parameters instead of reflecting over anonymous objects. Publish the smoke project with `dotnet publish -c Release -r win-x64` from that project directory, then run the published executable. Native publishing requires the platform's native toolchain (on Windows, the Visual C++ build tools and Windows SDK). Application-level NativeAOT compatibility also depends on the selected database provider and any custom schema reader or trace implementation.
3838

3939
The provider's reader determines which catalog objects are available. The format captures columns, primary keys, indexes, unique constraints, checks, defaults, identity settings, computed expressions, column comments, collations, and foreign keys. Objects not exposed by `ISchemaReader`, such as triggers and sequence definitions, are outside version 1.
4040

‎src/Schema/RepoDb.Schema.Core/RepoDb.Schema.Core/Extensions/CreateSchema.cs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
#endregion
88

99
using System;
10+
using System.Collections.Generic;
1011
using System.Data;
12+
using System.Diagnostics.CodeAnalysis;
1113
using System.Globalization;
1214
using System.Threading;
1315
using System.Threading.Tasks;
@@ -47,6 +49,7 @@ internal static class CreateSchemaExtension
4749
/// <param name="transaction">The transaction to be used.</param>
4850
/// <returns><c>true</c> if the schema was created; <c>false</c> if it already existed.</returns>
4951
/// <exception cref="ArgumentNullException">The connection or the name of the schema is <c>null</c> (or white space).</exception>
52+
[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", Justification = "Parameters are a Dictionary<string, object>, and the non-query has no parameters; neither path reflects over an object.")]
5053
public static bool CreateSchema(IDbConnection connection,
5154
string schemaName,
5255
string parameterPrefix,
@@ -60,7 +63,7 @@ public static bool CreateSchema(IDbConnection connection,
6063
{
6164
Validate(connection, schemaName);
6265
var exists = connection.ExecuteScalar(string.Format(CultureInfo.InvariantCulture, existsSql, parameterPrefix + ParameterName),
63-
new { SchemaName = schemaName },
66+
new Dictionary<string, object> { [ParameterName] = schemaName },
6467
commandTimeout: commandTimeout,
6568
traceKey: traceKey,
6669
transaction: transaction,
@@ -93,6 +96,7 @@ public static bool CreateSchema(IDbConnection connection,
9396
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
9497
/// <returns>A task that represents the asynchronous operation. The task result is <c>true</c> if the schema was created; <c>false</c> if it already existed.</returns>
9598
/// <exception cref="ArgumentNullException">The connection or the name of the schema is <c>null</c> (or white space).</exception>
99+
[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", Justification = "Parameters are a Dictionary<string, object>, and the non-query has no parameters; neither path reflects over an object.")]
96100
public static async Task<bool> CreateSchemaAsync(IDbConnection connection,
97101
string schemaName,
98102
string parameterPrefix,
@@ -107,7 +111,7 @@ public static async Task<bool> CreateSchemaAsync(IDbConnection connection,
107111
{
108112
Validate(connection, schemaName);
109113
var exists = await connection.ExecuteScalarAsync(string.Format(CultureInfo.InvariantCulture, existsSql, parameterPrefix + ParameterName),
110-
new { SchemaName = schemaName },
114+
new Dictionary<string, object> { [ParameterName] = schemaName },
111115
commandTimeout: commandTimeout,
112116
traceKey: traceKey,
113117
transaction: transaction,

0 commit comments

Comments
 (0)