From e4f11a57470b21dfc61d358d05d9bb6ced1781a3 Mon Sep 17 00:00:00 2001 From: ADRNV Date: Sun, 12 Apr 2026 22:52:10 +0300 Subject: [PATCH 1/5] Update .NET versions --- QMap.Benchmarks/QMap.Benchmarks.csproj | 2 +- QMap.Core/QMap.Core.csproj | 2 +- QMap.Mapping/QMap.Mapping.csproj | 2 +- QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj | 2 +- QMap.SqlBuilder/QMap.SqlBuilder.csproj | 2 +- QMap.SqlLite/QMap.Sqlite.csproj | 2 +- QMap.SqlServer/QMap.SqlServer.csproj | 2 +- QMap.Tests/QMap.Tests.csproj | 2 +- QMap/QMap.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/QMap.Benchmarks/QMap.Benchmarks.csproj b/QMap.Benchmarks/QMap.Benchmarks.csproj index f988a97..a18a833 100644 --- a/QMap.Benchmarks/QMap.Benchmarks.csproj +++ b/QMap.Benchmarks/QMap.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable diff --git a/QMap.Core/QMap.Core.csproj b/QMap.Core/QMap.Core.csproj index 244eced..4b4aeb9 100644 --- a/QMap.Core/QMap.Core.csproj +++ b/QMap.Core/QMap.Core.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/QMap.Mapping/QMap.Mapping.csproj b/QMap.Mapping/QMap.Mapping.csproj index 9ff5dad..e87d5b2 100644 --- a/QMap.Mapping/QMap.Mapping.csproj +++ b/QMap.Mapping/QMap.Mapping.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj b/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj index 7a3e09d..76662ab 100644 --- a/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj +++ b/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/QMap.SqlBuilder/QMap.SqlBuilder.csproj b/QMap.SqlBuilder/QMap.SqlBuilder.csproj index e845b5d..97254b7 100644 --- a/QMap.SqlBuilder/QMap.SqlBuilder.csproj +++ b/QMap.SqlBuilder/QMap.SqlBuilder.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/QMap.SqlLite/QMap.Sqlite.csproj b/QMap.SqlLite/QMap.Sqlite.csproj index c1e4281..3f404b3 100644 --- a/QMap.SqlLite/QMap.Sqlite.csproj +++ b/QMap.SqlLite/QMap.Sqlite.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/QMap.SqlServer/QMap.SqlServer.csproj b/QMap.SqlServer/QMap.SqlServer.csproj index 9ff5dad..e87d5b2 100644 --- a/QMap.SqlServer/QMap.SqlServer.csproj +++ b/QMap.SqlServer/QMap.SqlServer.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable diff --git a/QMap.Tests/QMap.Tests.csproj b/QMap.Tests/QMap.Tests.csproj index 8d4183c..605f354 100644 --- a/QMap.Tests/QMap.Tests.csproj +++ b/QMap.Tests/QMap.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/QMap/QMap.csproj b/QMap/QMap.csproj index c08b800..45fb896 100644 --- a/QMap/QMap.csproj +++ b/QMap/QMap.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable From 671764b304a1e22858aa44ae0d1d3c40250e1de5 Mon Sep 17 00:00:00 2001 From: ADRNV Date: Sun, 12 Apr 2026 22:54:42 +0300 Subject: [PATCH 2/5] Add selction by object keys --- CLAUDE.md | 17 ++++ QMap.Core/Mapping/IProjectionMapper.cs | 9 +++ QMap.Mapping/ProjectionMapper.cs | 78 +++++++++++++++++++ .../Abstractions/ISelectBuilder.cs | 1 + QMap.SqlBuilder/QueryBuilderExtensions.cs | 6 ++ QMap.SqlBuilder/StatementsBuilders.cs | 24 +++++- QMap/QMapConnectionExtension.cs | 17 ++++ 7 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md create mode 100644 QMap.Core/Mapping/IProjectionMapper.cs create mode 100644 QMap.Mapping/ProjectionMapper.cs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b819377 --- /dev/null +++ b/CLAUDE.md @@ -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. Нельзя удалять что-то из существующей арихитектуры, только расширять \ No newline at end of file diff --git a/QMap.Core/Mapping/IProjectionMapper.cs b/QMap.Core/Mapping/IProjectionMapper.cs new file mode 100644 index 0000000..7afd97a --- /dev/null +++ b/QMap.Core/Mapping/IProjectionMapper.cs @@ -0,0 +1,9 @@ +using System.Data; + +namespace QMap.Core.Mapping +{ + public interface IProjectionMapper + { + IEnumerable Map(IDataReader dataReader); + } +} diff --git a/QMap.Mapping/ProjectionMapper.cs b/QMap.Mapping/ProjectionMapper.cs new file mode 100644 index 0000000..536cb93 --- /dev/null +++ b/QMap.Mapping/ProjectionMapper.cs @@ -0,0 +1,78 @@ +using QMap.Core.Mapping; +using System.Data; +using System.Linq.Expressions; +using System.Reflection; + +namespace QMap.Mapping +{ + public class ProjectionMapper : IProjectionMapper + { + private readonly Func _mapFunc; + + public ProjectionMapper(Expression> selector) + { + _mapFunc = BuildMapFunc(selector); + } + + public IEnumerable Map(IDataReader dataReader) + { + while (dataReader.Read()) + yield return _mapFunc(dataReader); + } + + private static Func BuildMapFunc(Expression> 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>(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(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); + } + } +} diff --git a/QMap.SqlBuilder/Abstractions/ISelectBuilder.cs b/QMap.SqlBuilder/Abstractions/ISelectBuilder.cs index 0e7d78c..486f860 100644 --- a/QMap.SqlBuilder/Abstractions/ISelectBuilder.cs +++ b/QMap.SqlBuilder/Abstractions/ISelectBuilder.cs @@ -8,5 +8,6 @@ public interface ISelectBuilder : IQueryBuilder public ISelectBuilder BuidSelect(Expression type); + public ISelectBuilder BuidSelect(Expression> selector); } } diff --git a/QMap.SqlBuilder/QueryBuilderExtensions.cs b/QMap.SqlBuilder/QueryBuilderExtensions.cs index 81e8d79..a00d37a 100644 --- a/QMap.SqlBuilder/QueryBuilderExtensions.cs +++ b/QMap.SqlBuilder/QueryBuilderExtensions.cs @@ -45,6 +45,12 @@ public static ISelectBuilder Select(this IQueryBuilder queryBuilder, Type entity .BuidSelect(entity); } + public static ISelectBuilder Select(this IQueryBuilder queryBuilder, Expression> selector) + { + return new SelectBuilder(queryBuilder.SqlDialect) + .BuidSelect(selector); + } + public static IUpdateBuilder Update(this IQueryBuilder queryBuilder, IQMapConnection connection, out Dictionary parameters, Expression> propertySelectors, TProperty value) { var builder = new UpdateBuilder(queryBuilder.SqlDialect) diff --git a/QMap.SqlBuilder/StatementsBuilders.cs b/QMap.SqlBuilder/StatementsBuilders.cs index beae7cd..8031467 100644 --- a/QMap.SqlBuilder/StatementsBuilders.cs +++ b/QMap.SqlBuilder/StatementsBuilders.cs @@ -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(Expression> selector) + { + var columns = ExtractColumns(selector.Body); + Sql += $"select {string.Join(", ", columns)} "; + + return this; + } + + private static IEnumerable ExtractColumns(Expression body) => + body switch + { + NewExpression newExpr => newExpr.Arguments + .OfType() + .Select(m => m.Member.Name), + MemberInitExpression memberInit => memberInit.Bindings + .OfType() + .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(); diff --git a/QMap/QMapConnectionExtension.cs b/QMap/QMapConnectionExtension.cs index edd6102..ae8d65a 100644 --- a/QMap/QMapConnectionExtension.cs +++ b/QMap/QMapConnectionExtension.cs @@ -124,6 +124,23 @@ public static void Insert(this IQMapConnection connection, T entity) command.ExecuteNonQuery(); } + public static IEnumerable Select( + this IQMapConnection connection, + Expression> selector) where TSource : class, new() + { + var projectionMapper = new ProjectionMapper(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(this IQMapConnection connection, LambdaExpression predicate, IEntityMapper? customMapper = null) where T : class, new() { var command = connection.CreateCommand(); From d26280299f897cc514cd1d5fabef6682b1d5d25f Mon Sep 17 00:00:00 2001 From: ADRNV Date: Sun, 12 Apr 2026 22:55:08 +0300 Subject: [PATCH 3/5] Add tests --- QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs | 49 +++++++++++++++ QMap.Tests.Share/QMap.Tests.Share.csproj | 2 +- QMap.Tests/QMapConnectionExtensionTests.cs | 63 +++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs b/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs index cc0170e..9abfe72 100644 --- a/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs +++ b/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs @@ -189,6 +189,55 @@ public void DeleteSqlBuidWitoutSyntaxErrosInAllParsers() }); } + [Trait("SQL", "Select")] + [Fact] + public void SelectWithProjection_GeneratesSpecifiedColumns() + { + StatementsBuilders queryBuilder = new(new SqlDialectBase()); + + var sql = queryBuilder + .Select((TypesTestEntity e) => new { e.Id, e.StringField }) + .From(typeof(TypesTestEntity)) + .Build(); + + Assert.Contains("Id", sql); + Assert.Contains("StringField", sql); + Assert.DoesNotContain("*", sql); + } + + [Trait("SQL", "Select")] + [Fact] + public void SelectWithProjection_BuildsWithoutSyntaxErrorsInAllParsers() + { + StatementsBuilders queryBuilder = new(new TSqlDialect()); + + var sql = queryBuilder + .Select((TypesTestEntity e) => new { e.Id, e.StringField }) + .From(typeof(TypesTestEntity)) + .Build(); + + _parsers.ToList().ForEach(p => + { + var errors = p.Parse(sql); + Assert.Null(errors); + }); + } + + [Trait("SQL", "Select")] + [Fact] + public void SelectWithSingleProperty_GeneratesCorrectColumn() + { + StatementsBuilders queryBuilder = new(new SqlDialectBase()); + + var sql = queryBuilder + .Select((TypesTestEntity e) => e.StringField) + .From(typeof(TypesTestEntity)) + .Build(); + + Assert.Contains("StringField", sql); + Assert.DoesNotContain("*", sql); + } + [Trait("SQL", "Delete")] [Fact] public void DeleteFullSqlBuidWitoutSyntaxErrosInAllParsers() diff --git a/QMap.Tests.Share/QMap.Tests.Share.csproj b/QMap.Tests.Share/QMap.Tests.Share.csproj index 73ce4ec..08a9ba0 100644 --- a/QMap.Tests.Share/QMap.Tests.Share.csproj +++ b/QMap.Tests.Share/QMap.Tests.Share.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable diff --git a/QMap.Tests/QMapConnectionExtensionTests.cs b/QMap.Tests/QMapConnectionExtensionTests.cs index e5dbcf8..e7f040f 100644 --- a/QMap.Tests/QMapConnectionExtensionTests.cs +++ b/QMap.Tests/QMapConnectionExtensionTests.cs @@ -416,6 +416,69 @@ public void DeleteNotRevemoveNotMatchedEntityes() }); } + [Theory] + [InlineData(1)] + [InlineData(3)] + public void SelectProjection_ReturnsCorrectRowCount(int count) + { + _connectionFactories.ForEach(c => + { + var entities = new Fixture() + .Build() + .Without(t => t.Id) + .CreateMany(count); + + var context = c.GetDbContext(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + context.TypesTestEntity.AddRange(entities); + context.SaveChanges(); + + using var connection = c.Create(); + connection.Open(); + + var result = connection + .Select((TypesTestEntity e) => new { e.Id, e.StringField }) + .ToList(); + + Assert.Equal(count, result.Count); + + connection.Close(); + context.Database.EnsureDeleted(); + }); + } + + [Fact] + public void SelectProjection_MapsValuesCorrectly() + { + _connectionFactories.ForEach(c => + { + var entity = new Fixture() + .Build() + .Without(t => t.Id) + .Create(); + + var context = c.GetDbContext(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + context.TypesTestEntity.Add(entity); + context.SaveChanges(); + + using var connection = c.Create(); + connection.Open(); + + var result = connection + .Select((TypesTestEntity e) => new { e.StringField, e.IntField }) + .Single(); + + Assert.Equal(entity.StringField, result.StringField); + Assert.Equal(entity.IntField, result.IntField); + + connection.Close(); + context.Database.EnsureDeleted(); + }); + } + [Fact] public void Select_Should_Not_Drop_Statemant() { From 3b3dec097b7abff9a91a834a2a3a5a46583bfb3b Mon Sep 17 00:00:00 2001 From: "DESKTOP-IOHGBT9\\AnyHDD" Date: Thu, 7 May 2026 22:03:37 +0300 Subject: [PATCH 4/5] Move tests to dir --- .../QMap.Benchmarks}/Benchmarks/BenchmarkService.cs | 0 .../QMap.Benchmarks}/Benchmarks/IBenchmarkService.cs | 0 .../QMap.Benchmarks}/Benchmarks/SqlServer/QMapInsertBenchmark.cs | 0 .../QMap.Benchmarks}/Benchmarks/SqlServer/QMapQueryBenchmark.cs | 0 .../QMap.Benchmarks}/DI/SqlServerDependency.cs | 0 {QMap.Benchmarks => Tests/QMap.Benchmarks}/Program.cs | 0 {QMap.Benchmarks => Tests/QMap.Benchmarks}/QMap.Benchmarks.csproj | 0 {QMap.Benchmarks => Tests/QMap.Benchmarks}/TestConfiguration.json | 0 .../QMap.SqlBuilder.Tests}/QMap.SqlBuilder.Tests.csproj | 0 .../QMap.SqlBuilder.Tests}/SqlBuilderParseTests.cs | 0 .../QMap.SqlBuilder.Tests}/SqlBuilderSqlInjectionTests.cs | 0 .../QMap.SqlBuilder.Tests}/SqlBuilderTests.cs | 0 {QMap.SqlBuilder.Tests => Tests/QMap.SqlBuilder.Tests}/Startup.cs | 0 .../QMap.Tests.Share}/Common/DataBase/BaseDbContext.cs | 0 .../QMap.Tests.Share}/Common/DataBase/TestContext.cs | 0 .../QMap.Tests.Share}/Common/DataBase/TypesTestEntity.cs | 0 .../QMap.Tests.Share}/Common/DataBase/WrongEntity.cs | 0 .../Common/Fakes/Connections/FakeConnectionExtensions.cs | 0 .../Common/Fakes/Connections/FakeQMapConnectionAdapter.cs | 0 .../QMap.Tests.Share}/Common/IQMapConnectionFactory.cs | 0 .../QMap.Tests.Share}/Common/QMapConnectionFactoryBase.cs | 0 .../QMap.Tests.Share}/Common/SqlServerConnectionFactory.cs | 0 .../QMap.Tests.Share}/Common/SqliteConnectionFactory.cs | 0 .../QMap.Tests.Share}/Helpers/Sql/IParser.cs | 0 .../QMap.Tests.Share}/Helpers/Sql/TSqlParser.cs | 0 .../QMap.Tests.Share}/QMap.Tests.Share.csproj | 0 {QMap.Tests => Tests/QMap.Tests}/QMap.Tests.csproj | 0 {QMap.Tests => Tests/QMap.Tests}/QMapConnectionExtensionTests.cs | 0 {QMap.Tests => Tests/QMap.Tests}/Startup.cs | 0 {QMap.Tests => Tests/QMap.Tests}/TestConfiguration.json | 0 30 files changed, 0 insertions(+), 0 deletions(-) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/Benchmarks/BenchmarkService.cs (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/Benchmarks/IBenchmarkService.cs (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/Benchmarks/SqlServer/QMapInsertBenchmark.cs (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/Benchmarks/SqlServer/QMapQueryBenchmark.cs (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/DI/SqlServerDependency.cs (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/Program.cs (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/QMap.Benchmarks.csproj (100%) rename {QMap.Benchmarks => Tests/QMap.Benchmarks}/TestConfiguration.json (100%) rename {QMap.SqlBuilder.Tests => Tests/QMap.SqlBuilder.Tests}/QMap.SqlBuilder.Tests.csproj (100%) rename {QMap.SqlBuilder.Tests => Tests/QMap.SqlBuilder.Tests}/SqlBuilderParseTests.cs (100%) rename {QMap.SqlBuilder.Tests => Tests/QMap.SqlBuilder.Tests}/SqlBuilderSqlInjectionTests.cs (100%) rename {QMap.SqlBuilder.Tests => Tests/QMap.SqlBuilder.Tests}/SqlBuilderTests.cs (100%) rename {QMap.SqlBuilder.Tests => Tests/QMap.SqlBuilder.Tests}/Startup.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/DataBase/BaseDbContext.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/DataBase/TestContext.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/DataBase/TypesTestEntity.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/DataBase/WrongEntity.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/Fakes/Connections/FakeConnectionExtensions.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/Fakes/Connections/FakeQMapConnectionAdapter.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/IQMapConnectionFactory.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/QMapConnectionFactoryBase.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/SqlServerConnectionFactory.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Common/SqliteConnectionFactory.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Helpers/Sql/IParser.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/Helpers/Sql/TSqlParser.cs (100%) rename {QMap.Tests.Share => Tests/QMap.Tests.Share}/QMap.Tests.Share.csproj (100%) rename {QMap.Tests => Tests/QMap.Tests}/QMap.Tests.csproj (100%) rename {QMap.Tests => Tests/QMap.Tests}/QMapConnectionExtensionTests.cs (100%) rename {QMap.Tests => Tests/QMap.Tests}/Startup.cs (100%) rename {QMap.Tests => Tests/QMap.Tests}/TestConfiguration.json (100%) diff --git a/QMap.Benchmarks/Benchmarks/BenchmarkService.cs b/Tests/QMap.Benchmarks/Benchmarks/BenchmarkService.cs similarity index 100% rename from QMap.Benchmarks/Benchmarks/BenchmarkService.cs rename to Tests/QMap.Benchmarks/Benchmarks/BenchmarkService.cs diff --git a/QMap.Benchmarks/Benchmarks/IBenchmarkService.cs b/Tests/QMap.Benchmarks/Benchmarks/IBenchmarkService.cs similarity index 100% rename from QMap.Benchmarks/Benchmarks/IBenchmarkService.cs rename to Tests/QMap.Benchmarks/Benchmarks/IBenchmarkService.cs diff --git a/QMap.Benchmarks/Benchmarks/SqlServer/QMapInsertBenchmark.cs b/Tests/QMap.Benchmarks/Benchmarks/SqlServer/QMapInsertBenchmark.cs similarity index 100% rename from QMap.Benchmarks/Benchmarks/SqlServer/QMapInsertBenchmark.cs rename to Tests/QMap.Benchmarks/Benchmarks/SqlServer/QMapInsertBenchmark.cs diff --git a/QMap.Benchmarks/Benchmarks/SqlServer/QMapQueryBenchmark.cs b/Tests/QMap.Benchmarks/Benchmarks/SqlServer/QMapQueryBenchmark.cs similarity index 100% rename from QMap.Benchmarks/Benchmarks/SqlServer/QMapQueryBenchmark.cs rename to Tests/QMap.Benchmarks/Benchmarks/SqlServer/QMapQueryBenchmark.cs diff --git a/QMap.Benchmarks/DI/SqlServerDependency.cs b/Tests/QMap.Benchmarks/DI/SqlServerDependency.cs similarity index 100% rename from QMap.Benchmarks/DI/SqlServerDependency.cs rename to Tests/QMap.Benchmarks/DI/SqlServerDependency.cs diff --git a/QMap.Benchmarks/Program.cs b/Tests/QMap.Benchmarks/Program.cs similarity index 100% rename from QMap.Benchmarks/Program.cs rename to Tests/QMap.Benchmarks/Program.cs diff --git a/QMap.Benchmarks/QMap.Benchmarks.csproj b/Tests/QMap.Benchmarks/QMap.Benchmarks.csproj similarity index 100% rename from QMap.Benchmarks/QMap.Benchmarks.csproj rename to Tests/QMap.Benchmarks/QMap.Benchmarks.csproj diff --git a/QMap.Benchmarks/TestConfiguration.json b/Tests/QMap.Benchmarks/TestConfiguration.json similarity index 100% rename from QMap.Benchmarks/TestConfiguration.json rename to Tests/QMap.Benchmarks/TestConfiguration.json diff --git a/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj b/Tests/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj similarity index 100% rename from QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj rename to Tests/QMap.SqlBuilder.Tests/QMap.SqlBuilder.Tests.csproj diff --git a/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs b/Tests/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs similarity index 100% rename from QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs rename to Tests/QMap.SqlBuilder.Tests/SqlBuilderParseTests.cs diff --git a/QMap.SqlBuilder.Tests/SqlBuilderSqlInjectionTests.cs b/Tests/QMap.SqlBuilder.Tests/SqlBuilderSqlInjectionTests.cs similarity index 100% rename from QMap.SqlBuilder.Tests/SqlBuilderSqlInjectionTests.cs rename to Tests/QMap.SqlBuilder.Tests/SqlBuilderSqlInjectionTests.cs diff --git a/QMap.SqlBuilder.Tests/SqlBuilderTests.cs b/Tests/QMap.SqlBuilder.Tests/SqlBuilderTests.cs similarity index 100% rename from QMap.SqlBuilder.Tests/SqlBuilderTests.cs rename to Tests/QMap.SqlBuilder.Tests/SqlBuilderTests.cs diff --git a/QMap.SqlBuilder.Tests/Startup.cs b/Tests/QMap.SqlBuilder.Tests/Startup.cs similarity index 100% rename from QMap.SqlBuilder.Tests/Startup.cs rename to Tests/QMap.SqlBuilder.Tests/Startup.cs diff --git a/QMap.Tests.Share/Common/DataBase/BaseDbContext.cs b/Tests/QMap.Tests.Share/Common/DataBase/BaseDbContext.cs similarity index 100% rename from QMap.Tests.Share/Common/DataBase/BaseDbContext.cs rename to Tests/QMap.Tests.Share/Common/DataBase/BaseDbContext.cs diff --git a/QMap.Tests.Share/Common/DataBase/TestContext.cs b/Tests/QMap.Tests.Share/Common/DataBase/TestContext.cs similarity index 100% rename from QMap.Tests.Share/Common/DataBase/TestContext.cs rename to Tests/QMap.Tests.Share/Common/DataBase/TestContext.cs diff --git a/QMap.Tests.Share/Common/DataBase/TypesTestEntity.cs b/Tests/QMap.Tests.Share/Common/DataBase/TypesTestEntity.cs similarity index 100% rename from QMap.Tests.Share/Common/DataBase/TypesTestEntity.cs rename to Tests/QMap.Tests.Share/Common/DataBase/TypesTestEntity.cs diff --git a/QMap.Tests.Share/Common/DataBase/WrongEntity.cs b/Tests/QMap.Tests.Share/Common/DataBase/WrongEntity.cs similarity index 100% rename from QMap.Tests.Share/Common/DataBase/WrongEntity.cs rename to Tests/QMap.Tests.Share/Common/DataBase/WrongEntity.cs diff --git a/QMap.Tests.Share/Common/Fakes/Connections/FakeConnectionExtensions.cs b/Tests/QMap.Tests.Share/Common/Fakes/Connections/FakeConnectionExtensions.cs similarity index 100% rename from QMap.Tests.Share/Common/Fakes/Connections/FakeConnectionExtensions.cs rename to Tests/QMap.Tests.Share/Common/Fakes/Connections/FakeConnectionExtensions.cs diff --git a/QMap.Tests.Share/Common/Fakes/Connections/FakeQMapConnectionAdapter.cs b/Tests/QMap.Tests.Share/Common/Fakes/Connections/FakeQMapConnectionAdapter.cs similarity index 100% rename from QMap.Tests.Share/Common/Fakes/Connections/FakeQMapConnectionAdapter.cs rename to Tests/QMap.Tests.Share/Common/Fakes/Connections/FakeQMapConnectionAdapter.cs diff --git a/QMap.Tests.Share/Common/IQMapConnectionFactory.cs b/Tests/QMap.Tests.Share/Common/IQMapConnectionFactory.cs similarity index 100% rename from QMap.Tests.Share/Common/IQMapConnectionFactory.cs rename to Tests/QMap.Tests.Share/Common/IQMapConnectionFactory.cs diff --git a/QMap.Tests.Share/Common/QMapConnectionFactoryBase.cs b/Tests/QMap.Tests.Share/Common/QMapConnectionFactoryBase.cs similarity index 100% rename from QMap.Tests.Share/Common/QMapConnectionFactoryBase.cs rename to Tests/QMap.Tests.Share/Common/QMapConnectionFactoryBase.cs diff --git a/QMap.Tests.Share/Common/SqlServerConnectionFactory.cs b/Tests/QMap.Tests.Share/Common/SqlServerConnectionFactory.cs similarity index 100% rename from QMap.Tests.Share/Common/SqlServerConnectionFactory.cs rename to Tests/QMap.Tests.Share/Common/SqlServerConnectionFactory.cs diff --git a/QMap.Tests.Share/Common/SqliteConnectionFactory.cs b/Tests/QMap.Tests.Share/Common/SqliteConnectionFactory.cs similarity index 100% rename from QMap.Tests.Share/Common/SqliteConnectionFactory.cs rename to Tests/QMap.Tests.Share/Common/SqliteConnectionFactory.cs diff --git a/QMap.Tests.Share/Helpers/Sql/IParser.cs b/Tests/QMap.Tests.Share/Helpers/Sql/IParser.cs similarity index 100% rename from QMap.Tests.Share/Helpers/Sql/IParser.cs rename to Tests/QMap.Tests.Share/Helpers/Sql/IParser.cs diff --git a/QMap.Tests.Share/Helpers/Sql/TSqlParser.cs b/Tests/QMap.Tests.Share/Helpers/Sql/TSqlParser.cs similarity index 100% rename from QMap.Tests.Share/Helpers/Sql/TSqlParser.cs rename to Tests/QMap.Tests.Share/Helpers/Sql/TSqlParser.cs diff --git a/QMap.Tests.Share/QMap.Tests.Share.csproj b/Tests/QMap.Tests.Share/QMap.Tests.Share.csproj similarity index 100% rename from QMap.Tests.Share/QMap.Tests.Share.csproj rename to Tests/QMap.Tests.Share/QMap.Tests.Share.csproj diff --git a/QMap.Tests/QMap.Tests.csproj b/Tests/QMap.Tests/QMap.Tests.csproj similarity index 100% rename from QMap.Tests/QMap.Tests.csproj rename to Tests/QMap.Tests/QMap.Tests.csproj diff --git a/QMap.Tests/QMapConnectionExtensionTests.cs b/Tests/QMap.Tests/QMapConnectionExtensionTests.cs similarity index 100% rename from QMap.Tests/QMapConnectionExtensionTests.cs rename to Tests/QMap.Tests/QMapConnectionExtensionTests.cs diff --git a/QMap.Tests/Startup.cs b/Tests/QMap.Tests/Startup.cs similarity index 100% rename from QMap.Tests/Startup.cs rename to Tests/QMap.Tests/Startup.cs diff --git a/QMap.Tests/TestConfiguration.json b/Tests/QMap.Tests/TestConfiguration.json similarity index 100% rename from QMap.Tests/TestConfiguration.json rename to Tests/QMap.Tests/TestConfiguration.json From a75e4fbaf9b9fbcc51c821a5dd86c5cbe08536b8 Mon Sep 17 00:00:00 2001 From: ADRNV Date: Fri, 8 May 2026 19:05:36 +0300 Subject: [PATCH 5/5] Add module tests from SqlBuilder --- QMap.sln | 17 +- .../DeleteBuilderTests.cs | 67 +++++++ .../FromBuilderTests.cs | 70 ++++++++ .../InsertBuilderTests.cs | 84 +++++++++ .../QMap.SqlBuilder.Module.Tests.csproj | 28 +++ .../SelectBuilderTests.cs | 53 ++++++ .../TestEntity.cs | 8 + .../UpdateBuilderTests.cs | 78 ++++++++ .../WhereBuilderTests.cs | 166 ++++++++++++++++++ 9 files changed, 567 insertions(+), 4 deletions(-) create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/DeleteBuilderTests.cs create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/FromBuilderTests.cs create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/InsertBuilderTests.cs create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/QMap.SqlBuilder.Module.Tests.csproj create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/SelectBuilderTests.cs create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/TestEntity.cs create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/UpdateBuilderTests.cs create mode 100644 Tests/QMap.SqlBuilder.Module.Tests/WhereBuilderTests.cs diff --git a/QMap.sln b/QMap.sln index 45754c6..3c66c4a 100644 --- a/QMap.sln +++ b/QMap.sln @@ -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 @@ -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 @@ -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} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/DeleteBuilderTests.cs b/Tests/QMap.SqlBuilder.Module.Tests/DeleteBuilderTests.cs new file mode 100644 index 0000000..05e11e9 --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/DeleteBuilderTests.cs @@ -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(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(out var _); + + Assert.Throws(() => deleteBuilder.Build()); + } + + [Fact] + public void BuildDeleteFrom_ContainsFromKeyword() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Delete(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(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> expr = e => e.Id > 0; + var result = builder + .Delete(out var _) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("where", sql, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/FromBuilderTests.cs b/Tests/QMap.SqlBuilder.Module.Tests/FromBuilderTests.cs new file mode 100644 index 0000000..e651f36 --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/FromBuilderTests.cs @@ -0,0 +1,70 @@ +namespace QMap.SqlBuilder.Module.Tests; + +using QMap.Core.Dialects; +using QMap.SqlBuilder; + +public class FromBuilderTests +{ + [Fact] + public void BuildFromWithSelect_ContainsFromKeyword() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.Contains("from", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildFromWithSelect_ContainsEntityTypeName() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.Contains("TestEntity", sql); + } + + [Fact] + public void BuildFromWithSelect_ContainsGeneratedAlias() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.NotEmpty(sql); + Assert.Contains("TestEntity", sql); + } + + [Fact] + public void BuildFromWithDelete_ContainsDeleteAndFrom() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Delete(out var _) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.Contains("delete", sql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("from", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Build_ReturnsSql() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.NotEmpty(sql); + Assert.NotNull(sql); + } +} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/InsertBuilderTests.cs b/Tests/QMap.SqlBuilder.Module.Tests/InsertBuilderTests.cs new file mode 100644 index 0000000..42eeecc --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/InsertBuilderTests.cs @@ -0,0 +1,84 @@ +namespace QMap.SqlBuilder.Module.Tests; + +using QMap.Core.Dialects; +using QMap.SqlBuilder; +using QMap.Tests.Share.Common.Fakes.Connections; + +public class InsertBuilderTests +{ + [Fact] + public void BuildInsert_ContainsInsertIntoKeyword() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + var entity = new TestEntity { Id = 1, Name = "test", Value = 10 }; + + var sql = builder.BuildInsert(connectionFake, out var _, entity); + + Assert.Contains("insert into", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildInsert_ContainsEntityTypeName() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + var entity = new TestEntity { Id = 1, Name = "test", Value = 10 }; + + var sql = builder.BuildInsert(connectionFake, out var _, entity); + + Assert.Contains("TestEntity", sql); + } + + [Fact] + public void BuildInsert_ContainsAllPublicPropertyNames() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + var entity = new TestEntity { Id = 1, Name = "test", Value = 10 }; + + var sql = builder.BuildInsert(connectionFake, out var _, entity); + + Assert.Contains("Id", sql); + Assert.Contains("Name", sql); + Assert.Contains("Value", sql); + } + + [Fact] + public void BuildInsert_PopulatesParameters() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + var entity = new TestEntity { Id = 1, Name = "test", Value = 10 }; + + var sql = builder.BuildInsert(connectionFake, out var parameters, entity); + + Assert.NotEmpty(parameters); + Assert.Equal(3, parameters.Count); + } + + [Fact] + public void BuildInsertExcept_OmitsExcludedProperty() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + var entity = new TestEntity { Id = 1, Name = "test", Value = 10 }; + + var sql = builder.BuildInsert(connectionFake, out var _, entity, (e) => e.Id); + + Assert.DoesNotContain("Id", sql); + } + + [Fact] + public void BuildInsertExcept_IncludesNonExcludedProperties() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + var entity = new TestEntity { Id = 1, Name = "test", Value = 10 }; + + var sql = builder.BuildInsert(connectionFake, out var _, entity, (e) => e.Id); + + Assert.Contains("Name", sql); + Assert.Contains("Value", sql); + } +} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/QMap.SqlBuilder.Module.Tests.csproj b/Tests/QMap.SqlBuilder.Module.Tests/QMap.SqlBuilder.Module.Tests.csproj new file mode 100644 index 0000000..a893ee4 --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/QMap.SqlBuilder.Module.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/Tests/QMap.SqlBuilder.Module.Tests/SelectBuilderTests.cs b/Tests/QMap.SqlBuilder.Module.Tests/SelectBuilderTests.cs new file mode 100644 index 0000000..c1ed60e --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/SelectBuilderTests.cs @@ -0,0 +1,53 @@ +namespace QMap.SqlBuilder.Module.Tests; + +using QMap.Core.Dialects; +using QMap.SqlBuilder; + +public class SelectBuilderTests +{ + [Fact] + public void SelectByType_ContainsSelectStar() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder.Select(typeof(TestEntity)).From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.Contains("select *", sql); + } + + [Fact] + public void SelectWithAnonymousProjection_ContainsAllColumns() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Select((TestEntity e) => new { e.Id, e.Name }) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.Contains("Id", sql); + Assert.Contains("Name", sql); + Assert.DoesNotContain("*", sql); + } + + [Fact] + public void SelectWithSingleMemberProjection_ContainsSingleColumn() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var result = builder + .Select((TestEntity e) => e.Id) + .From(typeof(TestEntity)); + var sql = result.Build(); + + Assert.Contains("Id", sql); + Assert.DoesNotContain("*", sql); + } + + [Fact] + public void SelectBuild_ThrowsInvalidOperationException() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + var selectBuilder = builder.Select(typeof(TestEntity)); + + Assert.Throws(() => selectBuilder.Build()); + } +} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/TestEntity.cs b/Tests/QMap.SqlBuilder.Module.Tests/TestEntity.cs new file mode 100644 index 0000000..6ca8f0d --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/TestEntity.cs @@ -0,0 +1,8 @@ +namespace QMap.SqlBuilder.Module.Tests; + +internal sealed class TestEntity +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public int Value { get; set; } +} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/UpdateBuilderTests.cs b/Tests/QMap.SqlBuilder.Module.Tests/UpdateBuilderTests.cs new file mode 100644 index 0000000..734725b --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/UpdateBuilderTests.cs @@ -0,0 +1,78 @@ +namespace QMap.SqlBuilder.Module.Tests; + +using System.Linq.Expressions; +using QMap.Core.Dialects; +using QMap.SqlBuilder; +using QMap.Tests.Share.Common.Fakes.Connections; + +public class UpdateBuilderTests +{ + [Fact] + public void BuildUpdate_ContainsUpdateKeyword() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + + Expression> propExpr = e => e.Value; + var result = builder + .Update(connectionFake, out var _, propExpr, 20); + var sql = result.Sql; + + Assert.Contains("update", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildUpdate_ContainsSetKeyword() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + + Expression> propExpr = e => e.Value; + var result = builder + .Update(connectionFake, out var _, propExpr, 20); + var sql = result.Sql; + + Assert.Contains("set", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildUpdate_ContainsPropertyName() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + + Expression> propExpr = e => e.Value; + var result = builder + .Update(connectionFake, out var _, propExpr, 20); + var sql = result.Sql; + + Assert.Contains("Value", sql); + } + + [Fact] + public void BuildUpdate_RegistersParameterWithCorrectValue() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + + Expression> propExpr = e => e.Value; + var result = builder + .Update(connectionFake, out var parameters, propExpr, 20); + + Assert.NotEmpty(parameters); + } + + [Fact] + public void BuildUpdate_NonMemberExpression_ThrowsInvalidOperationException() + { + var connectionFake = FakeConnectionExtensions.Create(); + var builder = new StatementsBuilders(connectionFake.Dialect); + + Assert.Throws(() => + { + Expression> propExpr = e => e.ToString(); + var result = builder + .Update(connectionFake, out var _, propExpr, "test"); + }); + } +} diff --git a/Tests/QMap.SqlBuilder.Module.Tests/WhereBuilderTests.cs b/Tests/QMap.SqlBuilder.Module.Tests/WhereBuilderTests.cs new file mode 100644 index 0000000..5560f1c --- /dev/null +++ b/Tests/QMap.SqlBuilder.Module.Tests/WhereBuilderTests.cs @@ -0,0 +1,166 @@ +namespace QMap.SqlBuilder.Module.Tests; + +using System.Linq.Expressions; +using QMap.Core.Dialects; +using QMap.SqlBuilder; + +public class WhereBuilderTests +{ + [Fact] + public void BuildWhere_ContainsWhereKeyword() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id == 1; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("where", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildWhere_EqualOperator_ContainsEqualSign() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id == 1; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("=", sql); + } + + [Fact] + public void BuildWhere_NotEqualOperator_ContainsNotEqualSign() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id != 5; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("!=", sql); + } + + [Fact] + public void BuildWhere_GreaterThanOperator_ContainsOperator() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id > 0; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains(">", sql); + } + + [Fact] + public void BuildWhere_LessThanOperator_ContainsOperator() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id < 100; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("<", sql); + } + + [Fact] + public void BuildWhere_AndAlsoOperator_ContainsAndKeyword() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id == 1 && e.Value == 2; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("and", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildWhere_OrOperator_ContainsOrKeyword() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id == 1 || e.Value == 2; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("or", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildWhere_CapturedIntVariable_RegistersParameter() + { + var capturedId = 42; + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id == capturedId; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var parameters); + + Assert.NotEmpty(parameters); + } + + [Fact] + public void BuildWhere_CapturedStringVariable_RegistersParameter() + { + var capturedName = "test"; + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Name == capturedName; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var parameters); + + Assert.NotEmpty(parameters); + } + + [Fact] + public void BuildWhere_SqlInjectionString_ParameterizedNotLiteral() + { + var injectionString = "' DROP TABLE TestEntity; --'"; + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Name == injectionString; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("@", sql); + Assert.DoesNotContain("DROP TABLE", sql, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BuildWhere_ContainsSqlFromFromBuilder() + { + var builder = new StatementsBuilders(new SqlDialectBase()); + Expression> expr = e => e.Id == 1; + var result = builder + .Select(typeof(TestEntity)) + .From(typeof(TestEntity)) + .Where(expr, out var _); + var sql = result.Build(); + + Assert.Contains("select", sql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("from", sql, StringComparison.OrdinalIgnoreCase); + Assert.Contains("where", sql, StringComparison.OrdinalIgnoreCase); + } +}