Skip to content

Add interpolated SQL queries with DTO and tuple results - #208

Open
jogibear9988 wants to merge 4 commits into
codex/dto-select-predicatesfrom
codex/interpolated-dto-queries
Open

jogibear9988 wants to merge 4 commits into
codex/dto-select-predicatesfrom
codex/interpolated-dto-queries

Conversation

@jogibear9988

@jogibear9988 jogibear9988 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Add interpolated SQL queries that quote identifiers using the current provider, parameterize ordinary values, and return fully materialized DTOs or mapper-produced results.

This implements the SQL interpolation proposal in #202 using FormattableString. It supports both explicit identifier wrappers and :column / :table format specifiers; neither syntax requires a custom interpolated string handler.

Public API

Extension methods in DotNetProjects.Migrator.Framework:

public static IReadOnlyList<T> Query<T>(
    this ITransformationProvider provider,
    FormattableString sql)
    where T : class, new();

public static IReadOnlyList<T> Query<T>(
    this ITransformationProvider provider,
    FormattableString sql,
    Func<IDataReader, T> mapper);

public readonly record struct SqlColumn(string Name);
public readonly record struct SqlTable(string Name);

public static class Sql
{
    public static SqlColumn Col(string name) => new(name);
    public static SqlTable Table(string name) => new(name);
}

DTO and identifier-wrapper example

using DotNetProjects.Migrator.Framework;
using DtoColumn = System.ComponentModel.DataAnnotations.Schema.ColumnAttribute;

public sealed class ScreenDto
{
    public int Id { get; set; }
    public bool Archive { get; set; }
    [DtoColumn("GenericDefinition")]
    public string Definition { get; set; }
}

var screens = provider.Query<ScreenDto>($"""
    SELECT {Sql.Col("Id")}, {Sql.Col("Archive")}, {Sql.Col("GenericDefinition")}
    FROM {Sql.Table("Common_Screens")}
    WHERE {Sql.Col("Archive")} = {false}
    """);

DTO results reuse the column mapping from #206: the result column GenericDefinition populates Definition via its attribute.

Format-specifier example

var screens = provider.Query<ScreenDto>($"""
    SELECT {"Id":column}, {"Archive":column}, {"GenericDefinition":column}
    FROM {"Common_Screens":table}
    WHERE {"Archive":column} = {false}
    """);

Identifier strings use explicit markers. An ordinary interpolated string becomes a parameter, not raw SQL.

Tuple / custom mapper example

var screens = provider.Query($"""
    SELECT {Sql.Col("Id")}, {Sql.Col("GenericDefinition")}
    FROM {Sql.Table("Common_Screens")}
    WHERE {Sql.Col("Archive")} = {false}
    """,
    r => (
        Id: Convert.ToInt32(r.GetValue(0)),
        Definition: r.IsDBNull(1) ? null : r.GetString(1)));

The mapper overload also supports constructor-based DTOs and has no class, new() constraint. In a Classic migration use Database.Query(...); in a fluent migration use Context.Query(...) for an immediate read.

Behavior and limits

  • Ordinary scalar values, strings and nulls become native driver parameters. Unsupported complex objects/collections and unknown format specifiers fail before executing a command.
  • Sql.Col quotes a column identifier atom; Sql.Table quotes a table path, including schema qualification. For a trusted SQL alias use s.{Sql.Col("Id")}.
  • Repeated placeholders bind separately in SQL occurrence order, including for positional drivers. Escaped braces and alignment use the .NET composite-format parser.
  • Results are materialized; commands/readers are disposed on binding, execution or mapper failure as well as success. Active transactions, command timeouts and provider conversions are retained.
  • DTO queries report missing columns with DTO/property context. SQL aliases must match mapped column names.
  • SQL literal text is trusted migration code. SQL syntax, joins, collation and null semantics remain explicit; Column = {null} does not become IS NULL.
  • Pass interpolation directly or store it in a FormattableString. Converting it to string loses arguments; there is deliberately no string overload.
  • A custom handler is deferred: FormattableString already supports both proposed syntaxes without that additional complexity.

Part 2 of the implementation for #202. Depends on #206; merge #206 first, then retarget this PR to master. This targets codex/dto-select-predicates, keeping the diff limited to interpolation and shared DTO materialization.

Validation:

  • Solution build passed.
  • 888 unit tests and 498 SQLite tests passed, including 20 interpolation/binding/disposal tests.
  • Documentation tests/link checks passed; all 110 published C# samples compiled and 8 SQLite parity checks passed.
  • Native-driver coverage for both identifier syntaxes, DTO/tuple mapping and repeated parameters was added to the existing eleven-database CI fixture. Non-SQLite live databases were not run locally.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 19:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 19:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants