Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Это проекта микро ORM на подобии Dapper. но для разных провайдеров база данных таких как: SQL server, SQlite
Основаня идея:
1. Построение выражений при помощи методов-расширения, на подобии DSL
2. Интерпритация выражений в SQL запросы
3. Маппинг результатов запроса в типизированые объекты
4. Возможно легко расширять ORM, для других БД
Структура проекта
1. QMap - предствляет из себя интерфейс взаимодействия с IDbConnection. Содержит конечные реализации для запросов.
2. QMap.Core - содержит в себе общие абстракции над IDbConnection, для создания единого интерфейса взаимодействия
3. QMap.Mapping - отвечает за маппинг "сырых" строк из БД в типизированные коллекции
4. QMap.SqlBuilder - содержит абстракции для каждой ноды SQL синтаксиса которая нуждается в интепритации выражения/параметрезации. Интепретирует вырважения Expressions в SQL код
5. QMap.SqlLite - библиотека-адаптер для работы с SQLite. Содержит конкретную реализацию интерфейсов из QMap.Core для специфики работы с SQLite
Правила работы с проектом.
1. Переиспользовать существующий код API
2. Использование встроенных абстракций и следование общему интерфейсу
3. После добавления нового функционала в QMap.SqlBuilder или QMap нужно написать тесты
4. Нельзя удалять что-то из существующей арихитектуры, только расширять
9 changes: 9 additions & 0 deletions QMap.Core/Mapping/IProjectionMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Data;

namespace QMap.Core.Mapping
{
public interface IProjectionMapper<TResult>
{
IEnumerable<TResult> Map(IDataReader dataReader);
}
}
2 changes: 1 addition & 1 deletion QMap.Core/QMap.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
78 changes: 78 additions & 0 deletions QMap.Mapping/ProjectionMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using QMap.Core.Mapping;
using System.Data;
using System.Linq.Expressions;
using System.Reflection;

namespace QMap.Mapping
{
public class ProjectionMapper<TSource, TResult> : IProjectionMapper<TResult>
{
private readonly Func<IDataReader, TResult> _mapFunc;

public ProjectionMapper(Expression<Func<TSource, TResult>> selector)
{
_mapFunc = BuildMapFunc(selector);
}

public IEnumerable<TResult> Map(IDataReader dataReader)
{
while (dataReader.Read())
yield return _mapFunc(dataReader);
}

private static Func<IDataReader, TResult> BuildMapFunc(Expression<Func<TSource, TResult>> selector)
{
var readerParam = Expression.Parameter(typeof(IDataReader), "reader");
var visitor = new DataReaderProjectionVisitor(selector.Parameters[0], readerParam);
var newBody = visitor.Visit(selector.Body);

return Expression.Lambda<Func<IDataReader, TResult>>(newBody, readerParam).Compile();
}

private class DataReaderProjectionVisitor : ExpressionVisitor
{
private readonly ParameterExpression _sourceParam;
private readonly ParameterExpression _readerParam;

private static readonly MethodInfo _readValueMethod =
typeof(ProjectionHelper).GetMethod(nameof(ProjectionHelper.ReadValue))!;

public DataReaderProjectionVisitor(ParameterExpression sourceParam, ParameterExpression readerParam)
{
_sourceParam = sourceParam;
_readerParam = readerParam;
}

protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression == _sourceParam && node.Member is PropertyInfo prop)
{
return Expression.Call(
_readValueMethod.MakeGenericMethod(prop.PropertyType),
_readerParam,
Expression.Constant(prop.Name));
}

return base.VisitMember(node);
}
}
}

internal static class ProjectionHelper
{
public static T ReadValue<T>(IDataReader reader, string columnName)
{
var ordinal = reader.GetOrdinal(columnName);
var value = reader.GetValue(ordinal);

if (value == DBNull.Value || value is null)
return default!;

if (value is T typed)
return typed;

var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
return (T)Convert.ChangeType(value, targetType);
}
}
}
2 changes: 1 addition & 1 deletion QMap.Mapping/QMap.Mapping.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions QMap.SqlBuilder/Abstractions/ISelectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public interface ISelectBuilder : IQueryBuilder

public ISelectBuilder BuidSelect(Expression type);

public ISelectBuilder BuidSelect<T, TResult>(Expression<Func<T, TResult>> selector);
}
}
2 changes: 1 addition & 1 deletion QMap.SqlBuilder/QMap.SqlBuilder.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
6 changes: 6 additions & 0 deletions QMap.SqlBuilder/QueryBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public static ISelectBuilder Select(this IQueryBuilder queryBuilder, Type entity
.BuidSelect(entity);
}

public static ISelectBuilder Select<T, TResult>(this IQueryBuilder queryBuilder, Expression<Func<T, TResult>> selector)
{
return new SelectBuilder(queryBuilder.SqlDialect)
.BuidSelect(selector);
}

public static IUpdateBuilder Update<T, TProperty>(this IQueryBuilder queryBuilder, IQMapConnection connection, out Dictionary<string, object> parameters, Expression<Func<T, TProperty>> propertySelectors, TProperty value)
{
var builder = new UpdateBuilder(queryBuilder.SqlDialect)
Expand Down
24 changes: 23 additions & 1 deletion QMap.SqlBuilder/StatementsBuilders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,34 @@ public ISelectBuilder BuidSelect(Type type)

public ISelectBuilder BuidSelect(Expression type)
{
//TODO Add selecting by members list and expression
Sql += "select * ";

return this;
}

public ISelectBuilder BuidSelect<T, TResult>(Expression<Func<T, TResult>> selector)
{
var columns = ExtractColumns(selector.Body);
Sql += $"select {string.Join(", ", columns)} ";

return this;
}

private static IEnumerable<string> ExtractColumns(Expression body) =>
body switch
{
NewExpression newExpr => newExpr.Arguments
.OfType<MemberExpression>()
.Select(m => m.Member.Name),
MemberInitExpression memberInit => memberInit.Bindings
.OfType<MemberAssignment>()
.Where(b => b.Expression is MemberExpression)
.Select(b => ((MemberExpression)b.Expression).Member.Name),
MemberExpression single => new[] { single.Member.Name },
_ => throw new InvalidOperationException(
"Unsupported selector. Use: e => new { e.Prop1, e.Prop2 } or e => e.Prop")
};

public string Build()
{
throw new InvalidOperationException();
Expand Down
2 changes: 1 addition & 1 deletion QMap.SqlLite/QMap.Sqlite.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion QMap.SqlServer/QMap.SqlServer.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
17 changes: 13 additions & 4 deletions QMap.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.SqlServer", "QMap.SqlS
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A11BDBE0-176D-4484-9F21-B64FB6ECC919}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.Tests", "QMap.Tests\QMap.Tests.csproj", "{B4CA2E19-2197-4B07-AEE9-54CBAB5595A9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.Tests", "Tests\QMap.Tests\QMap.Tests.csproj", "{B4CA2E19-2197-4B07-AEE9-54CBAB5595A9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.SqlBuilder", "QMap.SqlBuilder\QMap.SqlBuilder.csproj", "{21EB1C84-35DB-4632-924B-9AA659A17BA4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.SqlBuilder.Tests", "QMap.SqlBuilder.Tests\QMap.SqlBuilder.Tests.csproj", "{3312932A-ADC5-4682-ABE3-703EAA200DE8}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.SqlBuilder.Tests", "Tests\QMap.SqlBuilder.Tests\QMap.SqlBuilder.Tests.csproj", "{3312932A-ADC5-4682-ABE3-703EAA200DE8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{C0627B39-40F3-4110-9EF5-0130B47AD2F4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.Benchmarks", "QMap.Benchmarks\QMap.Benchmarks.csproj", "{FF000355-039D-42EB-A120-888762294DC4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.Benchmarks", "Tests\QMap.Benchmarks\QMap.Benchmarks.csproj", "{FF000355-039D-42EB-A120-888762294DC4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.Tests.Share", "QMap.Tests.Share\QMap.Tests.Share.csproj", "{02B93221-DE00-4425-9A62-E0DFED3518EE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.Tests.Share", "Tests\QMap.Tests.Share\QMap.Tests.Share.csproj", "{02B93221-DE00-4425-9A62-E0DFED3518EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QMap.Sqlite", "QMap.SqlLite\QMap.Sqlite.csproj", "{40F5394D-278C-47F9-BB3C-30A9DB0A79B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QMap.SqlBuilder.Module.Tests", "Tests\QMap.SqlBuilder.Module.Tests\QMap.SqlBuilder.Module.Tests.csproj", "{9792BF90-6850-4F52-8959-DF5A93CA01A0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
DataBaseConfig|Any CPU = DataBaseConfig|Any CPU
Expand Down Expand Up @@ -93,6 +95,12 @@ Global
{40F5394D-278C-47F9-BB3C-30A9DB0A79B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40F5394D-278C-47F9-BB3C-30A9DB0A79B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40F5394D-278C-47F9-BB3C-30A9DB0A79B8}.Release|Any CPU.Build.0 = Release|Any CPU
{9792BF90-6850-4F52-8959-DF5A93CA01A0}.DataBaseConfig|Any CPU.ActiveCfg = Debug|Any CPU
{9792BF90-6850-4F52-8959-DF5A93CA01A0}.DataBaseConfig|Any CPU.Build.0 = Debug|Any CPU
{9792BF90-6850-4F52-8959-DF5A93CA01A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9792BF90-6850-4F52-8959-DF5A93CA01A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9792BF90-6850-4F52-8959-DF5A93CA01A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9792BF90-6850-4F52-8959-DF5A93CA01A0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -103,6 +111,7 @@ Global
{C0627B39-40F3-4110-9EF5-0130B47AD2F4} = {A11BDBE0-176D-4484-9F21-B64FB6ECC919}
{FF000355-039D-42EB-A120-888762294DC4} = {C0627B39-40F3-4110-9EF5-0130B47AD2F4}
{02B93221-DE00-4425-9A62-E0DFED3518EE} = {A11BDBE0-176D-4484-9F21-B64FB6ECC919}
{9792BF90-6850-4F52-8959-DF5A93CA01A0} = {A11BDBE0-176D-4484-9F21-B64FB6ECC919}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {746D0150-3F94-42E7-AC77-7B644637E041}
Expand Down
2 changes: 1 addition & 1 deletion QMap/QMap.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
17 changes: 17 additions & 0 deletions QMap/QMapConnectionExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ public static void Insert<T>(this IQMapConnection connection, T entity)
command.ExecuteNonQuery();
}

public static IEnumerable<TResult> Select<TSource, TResult>(
this IQMapConnection connection,
Expression<Func<TSource, TResult>> selector) where TSource : class, new()
{
var projectionMapper = new ProjectionMapper<TSource, TResult>(selector);

var command = connection.CreateCommand();
var sql = new StatementsBuilders(connection.Dialect)
.Select(selector)
.From(typeof(TSource))
.Build();

command.CommandText = sql;

return projectionMapper.Map(command.ExecuteReader());
}

public static void Delete<T>(this IQMapConnection connection, LambdaExpression predicate, IEntityMapper? customMapper = null) where T : class, new()
{
var command = connection.CreateCommand();
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
67 changes: 67 additions & 0 deletions Tests/QMap.SqlBuilder.Module.Tests/DeleteBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
namespace QMap.SqlBuilder.Module.Tests;

using System.Linq.Expressions;
using QMap.Core.Dialects;
using QMap.SqlBuilder;

public class DeleteBuilderTests
{
[Fact]
public void BuildDelete_ContainsDeleteKeyword()
{
var builder = new StatementsBuilders(new SqlDialectBase());
var result = builder
.Delete<TestEntity>(out var _)
.From(typeof(TestEntity));
var sql = result.Build();

Assert.Contains("delete", sql, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void BuildDelete_Build_ThrowsInvalidOperationException()
{
var builder = new StatementsBuilders(new SqlDialectBase());
var deleteBuilder = builder.Delete<TestEntity>(out var _);

Assert.Throws<InvalidOperationException>(() => deleteBuilder.Build());
}

[Fact]
public void BuildDeleteFrom_ContainsFromKeyword()
{
var builder = new StatementsBuilders(new SqlDialectBase());
var result = builder
.Delete<TestEntity>(out var _)
.From(typeof(TestEntity));
var sql = result.Build();

Assert.Contains("from", sql, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void BuildDeleteFrom_ContainsEntityTypeName()
{
var builder = new StatementsBuilders(new SqlDialectBase());
var result = builder
.Delete<TestEntity>(out var _)
.From(typeof(TestEntity));
var sql = result.Build();

Assert.Contains("TestEntity", sql);
}

[Fact]
public void BuildDeleteWithWhere_ContainsWhereKeyword()
{
var builder = new StatementsBuilders(new SqlDialectBase());
Expression<Func<TestEntity, bool>> expr = e => e.Id > 0;
var result = builder
.Delete<TestEntity>(out var _)
.From(typeof(TestEntity))
.Where<TestEntity>(expr, out var _);
var sql = result.Build();

Assert.Contains("where", sql, StringComparison.OrdinalIgnoreCase);
}
}
Loading
Loading