From 1492b76b5fc6f6ce344014b990037e93666f99dd Mon Sep 17 00:00:00 2001 From: ptixed Date: Fri, 12 Apr 2024 12:36:41 +0200 Subject: [PATCH 01/11] move sql server to separate assembly --- Ptixed.Sql.SqlServer/Database.cs | 15 + .../DatabaseExtensions.cs | 195 +++++----- Ptixed.Sql.SqlServer/IDatabase.cs | 8 + .../Ptixed.Sql.SqlServer.csproj | 22 ++ Ptixed.Sql.SqlServer/Query.cs | 14 + .../QueryHelper.cs | 339 +++++++++--------- Ptixed.Sql.Tests/DatabaseFixture.cs | 1 + Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj | 2 +- Ptixed.Sql.Tests/QueryTests.cs | 1 + Ptixed.Sql.sln | 14 +- Ptixed.Sql/ColumnValueSet.cs | 4 +- Ptixed.Sql/IDatabase.cs | 8 +- Ptixed.Sql/Impl/Database.cs | 30 +- Ptixed.Sql/Impl/QueryResult.cs | 8 +- Ptixed.Sql/Meta/Table.cs | 10 +- Ptixed.Sql/Ptixed.Sql.csproj | 4 +- Ptixed.Sql/Query.cs | 41 +-- 17 files changed, 402 insertions(+), 314 deletions(-) create mode 100644 Ptixed.Sql.SqlServer/Database.cs rename {Ptixed.Sql => Ptixed.Sql.SqlServer}/DatabaseExtensions.cs (97%) create mode 100644 Ptixed.Sql.SqlServer/IDatabase.cs create mode 100644 Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj create mode 100644 Ptixed.Sql.SqlServer/Query.cs rename {Ptixed.Sql => Ptixed.Sql.SqlServer}/QueryHelper.cs (81%) diff --git a/Ptixed.Sql.SqlServer/Database.cs b/Ptixed.Sql.SqlServer/Database.cs new file mode 100644 index 0000000..f986084 --- /dev/null +++ b/Ptixed.Sql.SqlServer/Database.cs @@ -0,0 +1,15 @@ +using Ptixed.Sql.Impl; +using System.Data.Common; +using System.Data.SqlClient; + +namespace Ptixed.Sql.SqlServer +{ + public class Database : Ptixed.Sql.Impl.Database, IDatabase + { + public Database(ConnectionConfig config) : base(config) + { + } + + protected override DbConnection CreateConnection(string connectionString) => new SqlConnection(connectionString); + } +} diff --git a/Ptixed.Sql/DatabaseExtensions.cs b/Ptixed.Sql.SqlServer/DatabaseExtensions.cs similarity index 97% rename from Ptixed.Sql/DatabaseExtensions.cs rename to Ptixed.Sql.SqlServer/DatabaseExtensions.cs index 53052bb..2f58288 100644 --- a/Ptixed.Sql/DatabaseExtensions.cs +++ b/Ptixed.Sql.SqlServer/DatabaseExtensions.cs @@ -1,103 +1,104 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql -{ - public static class DatabaseExtensions - { - public static List ToList(this IDatabase db, FormattableString query, params Type[] types) - => db.Query(new Query(query), types).ToList(); - - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7) }); - public static List ToList(this IDatabase db, FormattableString query) - => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8) }); - - public static T Single(this IDatabase db, FormattableString query) - => db.Query(new Query(query)).Single(); - +using System; +using System.Collections.Generic; +using System.Linq; +using Ptixed.Sql.Meta; +using Ptixed.Sql.SqlServer; + +namespace Ptixed.Sql.SqlServer +{ + public static class DatabaseExtensions + { + public static List ToList(this IDatabase db, FormattableString query, params Type[] types) + => db.Query(new Query(query), types).ToList(); + + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8) }); + + public static T Single(this IDatabase db, FormattableString query) + => db.Query(new Query(query)).Single(); + public static T SingleOrDefault(this IDatabase db, FormattableString query) => db.Query(new Query(query)).SingleOrDefault(); public static T FirstOrDefault(this IDatabase db, FormattableString query) => db.Query(new Query(query)).FirstOrDefault(); - public static int NonQuery(this IDatabase db, FormattableString query) - => db.NonQuery(new Query(query)); - - public static T Upsert(this IDatabase db, FormattableString searchCondition, T obj) - => db.Query(QueryHelper.Upsert(new Query(searchCondition), obj)).Single(); - - public static List GetByIds(this IDatabase db, params object[] ids) - { - if (ids.Length == 0) - return new List(); - return db.Query(QueryHelper.GetById(ids)).ToList(); - } - - public static T GetById(this IDatabase db, object id) - { - return GetByIds(db, id).SingleOrDefault(); - } - - public static List Insert(this IDatabase db, params T[] entities) - { - if (entities.Length == 0) - return new List(); - var result = db.Query(QueryHelper.Insert(entities)).ToList(); - - var table = Table.Get(typeof(T)); - - if (table.AutoIncrementColumn != null) - foreach (var (inserted, i) in result.Select((x, i) => (x, i))) - table[entities[i], table.AutoIncrementColumn.LogicalColumn] = table[inserted, table.AutoIncrementColumn.LogicalColumn]; - - return entities.ToList(); - } - - public static T Insert(this IDatabase db, T entity) - { - return Insert(db, new [] { entity })[0]; - } - - public static string Insert(this IDatabase db, string table, IDictionary values) - { - return db.Query(QueryHelper.Insert(table, values)).Single(); - } - - public static int Update(this IDatabase db, params object[] entities) - { - if (entities.Length == 0) - return 0; - return db.NonQuery(QueryHelper.Update(entities)); - } - - public static int Delete(this IDatabase db, params object[] entities) - { - if (entities.Length == 0) - return 0; - return db.NonQuery(QueryHelper.Delete(entities)); - } - - public static int Delete(this IDatabase db, params object[] ids) - { - if (ids.Length == 0) - return 0; - return db.NonQuery(QueryHelper.Delete(ids)); - } - } -} + public static int NonQuery(this IDatabase db, FormattableString query) + => db.NonQuery(new Query(query)); + + public static T Upsert(this IDatabase db, FormattableString searchCondition, T obj) + => db.Query(QueryHelper.Upsert(new Query(searchCondition), obj)).Single(); + + public static List GetByIds(this IDatabase db, params object[] ids) + { + if (ids.Length == 0) + return new List(); + return db.Query(QueryHelper.GetById(ids)).ToList(); + } + + public static T GetById(this IDatabase db, object id) + { + return GetByIds(db, id).SingleOrDefault(); + } + + public static List Insert(this IDatabase db, params T[] entities) + { + if (entities.Length == 0) + return new List(); + var result = db.Query(QueryHelper.Insert(entities)).ToList(); + + var table = Table.Get(typeof(T)); + + if (table.AutoIncrementColumn != null) + foreach (var (inserted, i) in result.Select((x, i) => (x, i))) + table[entities[i], table.AutoIncrementColumn.LogicalColumn] = table[inserted, table.AutoIncrementColumn.LogicalColumn]; + + return entities.ToList(); + } + + public static T Insert(this IDatabase db, T entity) + { + return Insert(db, new [] { entity })[0]; + } + + public static string Insert(this IDatabase db, string table, IDictionary values) + { + return db.Query(QueryHelper.Insert(table, values)).Single(); + } + + public static int Update(this IDatabase db, params object[] entities) + { + if (entities.Length == 0) + return 0; + return db.NonQuery(QueryHelper.Update(entities)); + } + + public static int Delete(this IDatabase db, params object[] entities) + { + if (entities.Length == 0) + return 0; + return db.NonQuery(QueryHelper.Delete(entities)); + } + + public static int Delete(this IDatabase db, params object[] ids) + { + if (ids.Length == 0) + return 0; + return db.NonQuery(QueryHelper.Delete(ids)); + } + } +} diff --git a/Ptixed.Sql.SqlServer/IDatabase.cs b/Ptixed.Sql.SqlServer/IDatabase.cs new file mode 100644 index 0000000..b5b789d --- /dev/null +++ b/Ptixed.Sql.SqlServer/IDatabase.cs @@ -0,0 +1,8 @@ +using System.Data.SqlClient; + +namespace Ptixed.Sql.SqlServer +{ + public interface IDatabase : IDatabase + { + } +} diff --git a/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj b/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj new file mode 100644 index 0000000..b7d1de9 --- /dev/null +++ b/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj @@ -0,0 +1,22 @@ + + + netstandard2.0 + False + Ptixed.Sql.SqlServer + 0.9.0 + Library for interacting with Microsoft SQL databases + ptixed + https://github.com/ptixed/Ptixed.Sql + ptixed sql tsql database db + 0.9.0 + 0.9.0 + + https://raw.githubusercontent.com/ptixed/Ptixed.Sql/master/logo-nuget.png + + + + + + + + diff --git a/Ptixed.Sql.SqlServer/Query.cs b/Ptixed.Sql.SqlServer/Query.cs new file mode 100644 index 0000000..1118621 --- /dev/null +++ b/Ptixed.Sql.SqlServer/Query.cs @@ -0,0 +1,14 @@ +using System; +using System.Data.SqlClient; +using System.Runtime.CompilerServices; + +namespace Ptixed.Sql.SqlServer +{ + public class Query : Query + { + public static Query Unsafe(string query) => new Query(FormattableStringFactory.Create(query)); + + public Query() { } + public Query(FormattableString query) : base(query) { } + } +} diff --git a/Ptixed.Sql/QueryHelper.cs b/Ptixed.Sql.SqlServer/QueryHelper.cs similarity index 81% rename from Ptixed.Sql/QueryHelper.cs rename to Ptixed.Sql.SqlServer/QueryHelper.cs index ffac532..22351cf 100644 --- a/Ptixed.Sql/QueryHelper.cs +++ b/Ptixed.Sql.SqlServer/QueryHelper.cs @@ -1,160 +1,179 @@ -using System.Collections.Generic; -using System.Linq; -using Ptixed.Sql.Impl; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql -{ - public static class QueryHelper - { - public static Query GetById(params object[] ids) - { - if (ids == null || ids.Length == 0) - return null; - - var table = Table.Get(typeof(T)); - - var query = new Query(); - query.Append($"SELECT * FROM {table} WHERE "); - query.Append($" OR ", ids.Select(id => table.GetPrimaryKeyCondition(id))); - return query; - } - - public static Query Insert(params T[] entities) - { - if (entities == null || entities.Length == 0) - return null; - - var table = Table.Get(typeof(T)); - var columns = table.PhysicalColumns.Except(new[] { table.AutoIncrementColumn }).ToList(); - - var query = new Query(); - query.Append($"INSERT INTO {table} ("); - query.Append($", ", columns.Select(x => new Query($"{x}"))); - query.Append($") OUTPUT "); - query.Append($", ", table.PhysicalColumns.Select(column => new Query($"INSERTED.{column}"))); - query.Append($"VALUES "); - query.Append($", ", entities.Select(entity => - { - var values = table.ToQuery(columns, entity) - .Select(column => new Query($"{column.Value}")) - .ToList(); - - var q = new Query(); - q.Append($"("); - q.Append($", ", values); - q.Append($")"); - return q; - })); - return query; - } - - - public static Query Upsert(Query searchCondition, T entity) - { - var table = Table.Get(typeof(T)); - var columns = table.PhysicalColumns.Except(new[] { table.AutoIncrementColumn }).ToList(); - var allcolumns = table.PhysicalColumns.ToList(); - var columnValues = table.ToQuery(columns, entity).ToArray(); - - var query = new Query(); - // MERGE - query.Append($"MERGE {table} AS [Target] \n"); - // USING - query.Append($"USING (SELECT "); - query.Append($", ", table.ToQuery(allcolumns, entity).Select(x => new Query($"{x} = {x.Value}"))); - query.Append($") AS [Source] \n"); - query.Append(searchCondition); - - // UPDATE - query.Append($"\n WHEN MATCHED THEN \n"); - query.Append($" UPDATE SET \n"); - query.Append($", ", columnValues.Select(column => new Query($"[Target].{column} = {column.Value} \n"))); - - // INSERT - query.Append($" WHEN NOT MATCHED THEN \n"); - query.Append($" INSERT ( "); - query.Append($", ", columns.Select(x => new Query($"{x}"))); - query.Append($") \n VALUES ( "); - query.Append($", ", columnValues.Select(column => new Query($"{column.Value}"))); - query.Append($")"); - - query.Append($"OUTPUT "); - query.Append($", ", table.PhysicalColumns.Select(x => new Query($"INSERTED.{x}"))); - query.Append($";"); - - return query; - } - - public static Query Insert(string table, IDictionary values) - { - if (table.Contains(']')) - throw PtixedException.InvalidIdenitfier(table); - foreach (var column in values.Keys) - if (column.Contains(']')) - throw PtixedException.InvalidIdenitfier(column); - - var query = new Query(); - query.Append(Query.Unsafe($"INSERT INTO [{table}] (")); - query.Append($", ", values.Select(x => Query.Unsafe($"[{x.Key}]"))); - query.Append($") VALUES ("); - query.Append($", ", values.Select(x => new Query($"{x.Value}"))); - query.Append($")\n\n"); - query.Append($"SELECT SCOPE_IDENTITY()"); - return query; - } - - public static Query Update(params object[] entities) - { - if (entities == null || entities.Length == 0) - return null; - - return Query.Join($"\n\n", entities.Select(entity => - { - var table = Table.Get(entity.GetType()); - var columns = table.PhysicalColumns.Where(x => !x.IsPrimaryKey).ToList(); - - var values = table.ToQuery(columns, entity) - .Select(column => new Query($"{column} = {column.Value}")) - .ToList(); - - var query = new Query(); - query.Append($"UPDATE {table} SET "); - query.Append($", ", values); - query.Append($" WHERE "); - query.Append(table.GetPrimaryKeyCondition(table[entity, table.PrimaryKey])); - return query; - })); - } - - public static Query Delete(params object[] entities) - { - if (entities == null || entities.Length == 0) - return null; - - return Query.Join($"\n\n", entities.Select(entity => - { - var table = Table.Get(entity.GetType()); - var id = table[entity, table.PrimaryKey]; - - var query = new Query(); - query.Append($"DELETE FROM {table} WHERE "); - query.Append(table.GetPrimaryKeyCondition(id)); - return query; - })); - } - - public static Query Delete(params object[] ids) - { - if (ids == null || ids.Length == 0) - return null; - - var table = Table.Get(typeof(T)); - - var query = new Query(); - query.Append($"DELETE FROM {table} WHERE "); - query.Append($" OR ", ids.Select(id => table.GetPrimaryKeyCondition(id))); - return query; - } - } -} +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Ptixed.Sql.Impl; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.SqlServer +{ + public static class QueryHelper + { + public static Query GetPrimaryKeyCondition(Table table, object key) + { + var query = new Query(); + query.Append($"("); + query.Append($" AND ", table.PrimaryKey.FromValueToQuery(key).Select(column => new Query($"{column} = {column.Value}"))); + query.Append($")"); + return query; + } + + public static Query GetById(params object[] ids) + { + if (ids == null || ids.Length == 0) + return null; + + var table = Table.Get(typeof(T)); + + var query = new Query(); + query.Append($"SELECT * FROM {table} WHERE "); + query.Append($" OR ", ids.Select(id => GetPrimaryKeyCondition(table, id))); + return query; + } + + public static Query Insert(params T[] entities) + { + if (entities == null || entities.Length == 0) + return null; + + var table = Table.Get(typeof(T)); + var columns = table.PhysicalColumns.Except(new[] { table.AutoIncrementColumn }).ToList(); + + var query = new Query(); + query.Append($"INSERT INTO {table} ("); + query.Append($", ", columns.Select(x => new Query($"{x}"))); + query.Append($") OUTPUT "); + query.Append($", ", table.PhysicalColumns.Select(column => new Query($"INSERTED.{column}"))); + query.Append($"VALUES "); + query.Append($", ", entities.Select(entity => + { + var values = table.ToQuery(columns, entity) + .Select(column => new Query($"{column.Value}")) + .ToList(); + + var q = new Query(); + q.Append($"("); + q.Append($", ", values); + q.Append($")"); + return q; + })); + return query; + } + + + public static Query Upsert(Query searchCondition, T entity) + { + var table = Table.Get(typeof(T)); + var columns = table.PhysicalColumns.Except(new[] { table.AutoIncrementColumn }).ToList(); + var allcolumns = table.PhysicalColumns.ToList(); + var columnValues = table.ToQuery(columns, entity).ToArray(); + + var query = new Query(); + // MERGE + query.Append($"MERGE {table} AS [Target] \n"); + // USING + query.Append($"USING (SELECT "); + query.Append($", ", table.ToQuery(allcolumns, entity).Select(x => new Query($"{x} = {x.Value}"))); + query.Append($") AS [Source] \n"); + query.Append(searchCondition); + + // UPDATE + query.Append($"\n WHEN MATCHED THEN \n"); + query.Append($" UPDATE SET \n"); + query.Append($", ", columnValues.Select(column => new Query($"[Target].{column} = {column.Value} \n"))); + + // INSERT + query.Append($" WHEN NOT MATCHED THEN \n"); + query.Append($" INSERT ( "); + query.Append($", ", columns.Select(x => new Query($"{x}"))); + query.Append($") \n VALUES ( "); + query.Append($", ", columnValues.Select(column => new Query($"{column.Value}"))); + query.Append($")"); + + query.Append($"OUTPUT "); + query.Append($", ", table.PhysicalColumns.Select(x => new Query($"INSERTED.{x}"))); + query.Append($";"); + + return query; + } + + public static Query Insert(string table, IDictionary values) + { + if (table.Contains(']')) + throw PtixedException.InvalidIdenitfier(table); + foreach (var column in values.Keys) + if (column.Contains(']')) + throw PtixedException.InvalidIdenitfier(column); + + var query = new Query(); + query.Append(Query.Unsafe($"INSERT INTO [{table}] (")); + query.Append($", ", values.Select(x => Query.Unsafe($"[{x.Key}]"))); + query.Append($") VALUES ("); + query.Append($", ", values.Select(x => new Query($"{x.Value}"))); + query.Append($")\n\n"); + query.Append($"SELECT SCOPE_IDENTITY()"); + return query; + } + + public static Query Update(params object[] entities) + { + if (entities == null || entities.Length == 0) + return null; + + var query = new Query(); + for (var i = 0; i < entities.Length; ++i) + { + var entity = entities[i]; + var table = Table.Get(entity.GetType()); + var columns = table.PhysicalColumns.Where(x => !x.IsPrimaryKey).ToList(); + + var values = table.ToQuery(columns, entity) + .Select(column => new Query($"{column} = {column.Value}")) + .ToList(); + + query.Append($"UPDATE {table} SET "); + query.Append($", ", values); + query.Append($" WHERE "); + query.Append(GetPrimaryKeyCondition(table, table[entity, table.PrimaryKey])); + return query; + + if (i < entities.Length - 1) + query.Append($"\n\n"); + } + return query; + } + + public static Query Delete(params object[] entities) + { + if (entities == null || entities.Length == 0) + return null; + + var query = new Query(); + for (var i = 0; i < entities.Length; ++i) + { + var entity = entities[i]; + var table = Table.Get(entity.GetType()); + var id = table[entity, table.PrimaryKey]; + + query.Append($"DELETE FROM {table} WHERE "); + query.Append(GetPrimaryKeyCondition(table, id)); + + if (i < entities.Length - 1) + query.Append($"\n\n"); + } + return query; + } + + public static Query Delete(params object[] ids) + { + if (ids == null || ids.Length == 0) + return null; + + var table = Table.Get(typeof(T)); + + var query = new Query(); + query.Append($"DELETE FROM {table} WHERE "); + query.Append($" OR ", ids.Select(id => GetPrimaryKeyCondition(table, id))); + return query; + } + } +} diff --git a/Ptixed.Sql.Tests/DatabaseFixture.cs b/Ptixed.Sql.Tests/DatabaseFixture.cs index 93d5b7f..eaa7843 100644 --- a/Ptixed.Sql.Tests/DatabaseFixture.cs +++ b/Ptixed.Sql.Tests/DatabaseFixture.cs @@ -1,5 +1,6 @@ using System; using Ptixed.Sql.Impl; +using Ptixed.Sql.SqlServer; namespace Ptixed.Sql.Tests { diff --git a/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj b/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj index 6b4c0db..597206f 100644 --- a/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj +++ b/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj @@ -12,6 +12,6 @@ - + diff --git a/Ptixed.Sql.Tests/QueryTests.cs b/Ptixed.Sql.Tests/QueryTests.cs index ce9d419..dd61064 100644 --- a/Ptixed.Sql.Tests/QueryTests.cs +++ b/Ptixed.Sql.Tests/QueryTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Ptixed.Sql.Impl; +using Ptixed.Sql.SqlServer; using Ptixed.Sql.Tests.Specimens; using Xunit; diff --git a/Ptixed.Sql.sln b/Ptixed.Sql.sln index 1018bed..4c40ab5 100644 --- a/Ptixed.Sql.sln +++ b/Ptixed.Sql.sln @@ -1,11 +1,11 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.168 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ptixed.Sql", "Ptixed.Sql\Ptixed.Sql.csproj", "{E701E6AA-5383-43FE-B596-92C5AF7557E5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ptixed.Sql", "Ptixed.Sql\Ptixed.Sql.csproj", "{E701E6AA-5383-43FE-B596-92C5AF7557E5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ptixed.Sql.Tests", "Ptixed.Sql.Tests\Ptixed.Sql.Tests.csproj", "{C42F1ED8-9220-440B-86F7-EFB0D7CB1DD8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ptixed.Sql.Tests", "Ptixed.Sql.Tests\Ptixed.Sql.Tests.csproj", "{C42F1ED8-9220-440B-86F7-EFB0D7CB1DD8}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{61269030-403D-4A27-8B6F-52A7F78A0D7C}" ProjectSection(SolutionItems) = preProject @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ptixed.Sql.SqlServer", "Ptixed.Sql.SqlServer\Ptixed.Sql.SqlServer.csproj", "{606E057B-CA06-4A06-966C-60C2A3E3C645}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {C42F1ED8-9220-440B-86F7-EFB0D7CB1DD8}.Debug|Any CPU.Build.0 = Debug|Any CPU {C42F1ED8-9220-440B-86F7-EFB0D7CB1DD8}.Release|Any CPU.ActiveCfg = Release|Any CPU {C42F1ED8-9220-440B-86F7-EFB0D7CB1DD8}.Release|Any CPU.Build.0 = Release|Any CPU + {606E057B-CA06-4A06-966C-60C2A3E3C645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {606E057B-CA06-4A06-966C-60C2A3E3C645}.Debug|Any CPU.Build.0 = Debug|Any CPU + {606E057B-CA06-4A06-966C-60C2A3E3C645}.Release|Any CPU.ActiveCfg = Release|Any CPU + {606E057B-CA06-4A06-966C-60C2A3E3C645}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Ptixed.Sql/ColumnValueSet.cs b/Ptixed.Sql/ColumnValueSet.cs index fd4d1a8..873575e 100644 --- a/Ptixed.Sql/ColumnValueSet.cs +++ b/Ptixed.Sql/ColumnValueSet.cs @@ -1,7 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data.SqlClient; +using System.Data.Common; using System.Linq; using System.Threading; using Ptixed.Sql.Impl; @@ -32,7 +32,7 @@ public ColumnValueSet(Range values) _dict = new Lazy>(() => _values.ToDictionary(x => x.Name, x => x.Value), LazyThreadSafetyMode.None); } - public ColumnValueSet(SqlDataReader reader) + public ColumnValueSet(DbDataReader reader) { var values = new ColumnValue[reader.FieldCount]; for (var i = 0; i < reader.FieldCount; ++i) diff --git a/Ptixed.Sql/IDatabase.cs b/Ptixed.Sql/IDatabase.cs index 0dac7cc..31dfec0 100644 --- a/Ptixed.Sql/IDatabase.cs +++ b/Ptixed.Sql/IDatabase.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; namespace Ptixed.Sql { - public interface IDatabase : IDisposable + public interface IDatabase : IDisposable + where TParameter : DbParameter, new() { MappingConfig MappingConfig { get; } - IEnumerable Query(Query query, params Type[] types); - int NonQuery(params Query[] query); + IEnumerable Query(Query query, params Type[] types); + int NonQuery(params Query[] query); IDatabaseTransaction OpenTransaction(IsolationLevel isolation); diff --git a/Ptixed.Sql/Impl/Database.cs b/Ptixed.Sql/Impl/Database.cs index b50e4a6..9fced7d 100755 --- a/Ptixed.Sql/Impl/Database.cs +++ b/Ptixed.Sql/Impl/Database.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; +using System.Data.Common; using System.Linq; using System.Threading; @@ -10,13 +10,15 @@ namespace Ptixed.Sql.Impl /// /// This class is not thread safe /// - public class Database : IDatabase + public abstract class Database : IDatabase + where TParameter : DbParameter, new() + where TCommand : DbCommand, new() { - private SqlTransaction _transaction; + private DbTransaction _transaction; private IDisposable _result; public readonly ConnectionConfig Config; - public Lazy Connection; + public Lazy Connection; public readonly DiagnosticsClass Diagnostics = new DiagnosticsClass(); @@ -24,7 +26,7 @@ public class Database : IDatabase public class DiagnosticsClass { - public SqlCommand LastCommand; + public TCommand LastCommand; } public Database(ConnectionConfig config) @@ -33,12 +35,12 @@ public Database(ConnectionConfig config) Reset(); } - private SqlCommand NewCommand() + private TCommand NewCommand() { try { _result?.Dispose(); } catch { /* don't care */ } - var command = new SqlCommand() + var command = new TCommand() { Connection = Connection.Value, Transaction = _transaction, @@ -64,15 +66,17 @@ public void Dispose() public void Reset() { Dispose(); - Connection = new Lazy(() => + Connection = new Lazy(() => { - var sql = new SqlConnection(Config.ConnectionString); + var sql = CreateConnection(Config.ConnectionString); sql.Open(); return sql; }, LazyThreadSafetyMode.None); } - public int NonQuery(params Query[] query) + protected abstract DbConnection CreateConnection(string connectionString); + + public int NonQuery(params Query[] query) { if (query.Length == 0) return 0; @@ -81,7 +85,7 @@ public int NonQuery(params Query[] query) return command.ExecuteNonQuery(); } - public IEnumerable Query(Query query, params Type[] types) + public IEnumerable Query(Query query, params Type[] types) { var command = query.ToSql(NewCommand(), Config.Mappping); @@ -99,11 +103,11 @@ public IDatabaseTransaction OpenTransaction(IsolationLevel isolation) private class DatabaseTransaction : IDatabaseTransaction { - private readonly Database _db; + private readonly Database _db; private bool _commited; private bool _rolledback; - public DatabaseTransaction(Database db, IsolationLevel isolation) + public DatabaseTransaction(Database db, IsolationLevel isolation) { _db = db; _db._transaction = db.Connection.Value.BeginTransaction(isolation); diff --git a/Ptixed.Sql/Impl/QueryResult.cs b/Ptixed.Sql/Impl/QueryResult.cs index 564351a..2e9aaf6 100644 --- a/Ptixed.Sql/Impl/QueryResult.cs +++ b/Ptixed.Sql/Impl/QueryResult.cs @@ -1,20 +1,20 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data.SqlClient; +using System.Data.Common; using System.Linq; using Ptixed.Sql.Meta; namespace Ptixed.Sql.Impl { - internal class QueryResult : IEnumerable, IDisposable + public class QueryResult : IEnumerable, IDisposable { private readonly MappingConfig _config; - private readonly SqlDataReader _reader; + private readonly DbDataReader _reader; private readonly Type[] _types; private readonly List _result; - public QueryResult(MappingConfig config, SqlDataReader reader, Type[] types) + public QueryResult(MappingConfig config, DbDataReader reader, Type[] types) { _config = config; _reader = reader; diff --git a/Ptixed.Sql/Meta/Table.cs b/Ptixed.Sql/Meta/Table.cs index a1cbbe6..b2f9a87 100644 --- a/Ptixed.Sql/Meta/Table.cs +++ b/Ptixed.Sql/Meta/Table.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Reflection; using Ptixed.Sql.Impl; @@ -77,15 +78,6 @@ private Table(Type type) public object CreateNew() => _ctor(null); - public Query GetPrimaryKeyCondition(object o) - { - var query = new Query(); - query.Append($"("); - query.Append($" AND ", PrimaryKey.FromValueToQuery(o).Select(column => new Query($"{column} = {column.Value}"))); - query.Append($")"); - return query; - } - public ICollection ToQuery(List columns, object o) { return columns diff --git a/Ptixed.Sql/Ptixed.Sql.csproj b/Ptixed.Sql/Ptixed.Sql.csproj index 99f9bd0..b95bbc9 100644 --- a/Ptixed.Sql/Ptixed.Sql.csproj +++ b/Ptixed.Sql/Ptixed.Sql.csproj @@ -14,7 +14,9 @@ https://raw.githubusercontent.com/ptixed/Ptixed.Sql/master/logo-nuget.png - + + + diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index 791ce53..a8c2351 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -2,14 +2,14 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Data.SqlClient; +using System.Data.Common; using System.Linq; -using System.Runtime.CompilerServices; using System.Text; namespace Ptixed.Sql { - public class Query + public abstract class Query + where TParameter : DbParameter, new() { private readonly List _parts = new List(); public bool IsEmpty => _parts.Count == 0; @@ -19,38 +19,39 @@ public class Query public Query() { } public Query(FormattableString query) => Append(query); - public static Query Unsafe(string query) => new Query(FormattableStringFactory.Create(query)); - - public Query Append(FormattableString query) + public Query Append(FormattableString query) { _parts.Add(query); return this; } - public Query Append(Query query) + public Query Append(Query query) { _parts.AddRange(query._parts); return this; } - public Query Append(FormattableString separator, IEnumerable parts) => Append(Join(separator, parts)); - - public static Query Join(FormattableString separator, IEnumerable parts) + public Query Append(FormattableString separator, IEnumerable> parts) { - var query = new Query(); using (var e = parts.GetEnumerator()) if (e.MoveNext()) { - query.Append(e.Current); - while(e.MoveNext()) - query.Append(separator).Append(e.Current); + Append(e.Current); + while (e.MoveNext()) + Append(separator).Append(e.Current); } - return query; + return this; + } + + public override string ToString() => ToString(new MappingConfig()); + + public string ToString(MappingConfig mc) + { + var index = 0; + return ToSql(ref index, mc).Item1; } - - public override string ToString() => ToSql(new SqlCommand(), new MappingConfig()).CommandText; - public SqlCommand ToSql(SqlCommand command, MappingConfig mapping) + public DbCommand ToSql(DbCommand command, MappingConfig mapping) { var index = 0; var (text, values) = ToSql(ref index, mapping); @@ -59,7 +60,7 @@ public SqlCommand ToSql(SqlCommand command, MappingConfig mapping) foreach (var (value, i) in values.Select((x, i) => (x, i))) { var dbvalue = mapping.ToDb(value?.GetType(), value); - var parameter = dbvalue as SqlParameter ?? new SqlParameter { Value = dbvalue }; + var parameter = dbvalue as DbParameter ?? new TParameter { Value = dbvalue }; parameter.ParameterName = i.ToString(); command.Parameters.Add(parameter); } @@ -83,7 +84,7 @@ public SqlCommand ToSql(SqlCommand command, MappingConfig mapping) case null: formants.Add("NULL"); break; - case Query q: + case Query q: var (text, vs) = q.ToSql(ref index, mapping); values.AddRange(vs); formants.Add(text); From 7e1e324316e0a357da68fca2169604edab585ff6 Mon Sep 17 00:00:00 2001 From: ptixed Date: Fri, 12 Apr 2024 14:13:03 +0200 Subject: [PATCH 02/11] postgres --- Ptixed.Sql.Postgres/Database.cs | 15 + Ptixed.Sql.Postgres/DatabaseExtensions.cs | 96 +++ Ptixed.Sql.Postgres/IDatabase.cs | 8 + .../Ptixed.Sql.Postgres.csproj | 22 + Ptixed.Sql.Postgres/Query.cs | 33 + Ptixed.Sql.Postgres/QueryHelper.cs | 123 +++ Ptixed.Sql.SqlServer/Database.cs | 2 +- Ptixed.Sql.SqlServer/Query.cs | 21 +- Ptixed.Sql.SqlServer/QueryHelper.cs | 9 +- Ptixed.Sql.Tests/AccessorTests.cs | 99 +++ Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs | 55 ++ Ptixed.Sql.Tests/Postgres/QueryTests.cs | 282 +++++++ Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj | 1 + .../EnumToStringConverter.cs | 38 +- .../JsonSqlConverter.cs | 44 +- .../{Specimens => Specimen}/Model.cs | 71 +- .../{Specimens => Specimen}/Model2.cs | 34 +- .../{Specimens => Specimen}/ModelUpsert.cs | 38 +- .../{Specimens => Specimen}/ModelWithNoPk.cs | 64 +- .../{Specimens => Specimen}/Others.cs | 54 +- .../{ => SqlServer}/DatabaseFixture.cs | 109 +-- .../{ => SqlServer}/QueryTests.cs | 772 ++++++++---------- Ptixed.Sql.sln | 6 + Ptixed.Sql/ColumnValue.cs | 2 - Ptixed.Sql/Impl/Database.cs | 2 +- Ptixed.Sql/MappingConfig.cs | 2 + Ptixed.Sql/Meta/PhysicalColumn.cs | 1 - Ptixed.Sql/Meta/Table.cs | 1 - Ptixed.Sql/Query.cs | 15 +- 29 files changed, 1340 insertions(+), 679 deletions(-) create mode 100644 Ptixed.Sql.Postgres/Database.cs create mode 100644 Ptixed.Sql.Postgres/DatabaseExtensions.cs create mode 100644 Ptixed.Sql.Postgres/IDatabase.cs create mode 100644 Ptixed.Sql.Postgres/Ptixed.Sql.Postgres.csproj create mode 100644 Ptixed.Sql.Postgres/Query.cs create mode 100644 Ptixed.Sql.Postgres/QueryHelper.cs create mode 100644 Ptixed.Sql.Tests/AccessorTests.cs create mode 100644 Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs create mode 100644 Ptixed.Sql.Tests/Postgres/QueryTests.cs rename Ptixed.Sql.Tests/{Specimens => Specimen}/EnumToStringConverter.cs (92%) rename Ptixed.Sql.Tests/{Specimens => Specimen}/JsonSqlConverter.cs (93%) rename Ptixed.Sql.Tests/{Specimens => Specimen}/Model.cs (92%) rename Ptixed.Sql.Tests/{Specimens => Specimen}/Model2.cs (84%) rename Ptixed.Sql.Tests/{Specimens => Specimen}/ModelUpsert.cs (86%) rename Ptixed.Sql.Tests/{Specimens => Specimen}/ModelWithNoPk.cs (91%) rename Ptixed.Sql.Tests/{Specimens => Specimen}/Others.cs (90%) rename Ptixed.Sql.Tests/{ => SqlServer}/DatabaseFixture.cs (96%) rename Ptixed.Sql.Tests/{ => SqlServer}/QueryTests.cs (75%) diff --git a/Ptixed.Sql.Postgres/Database.cs b/Ptixed.Sql.Postgres/Database.cs new file mode 100644 index 0000000..84a5477 --- /dev/null +++ b/Ptixed.Sql.Postgres/Database.cs @@ -0,0 +1,15 @@ +using Npgsql; +using Ptixed.Sql.Impl; +using System.Data.Common; + +namespace Ptixed.Sql.Postgres +{ + public class Database : Database, IDatabase + { + public Database(ConnectionConfig config) : base(config) + { + } + + protected override DbConnection CreateConnection(string connectionString) => new NpgsqlConnection(connectionString); + } +} diff --git a/Ptixed.Sql.Postgres/DatabaseExtensions.cs b/Ptixed.Sql.Postgres/DatabaseExtensions.cs new file mode 100644 index 0000000..2aa1e78 --- /dev/null +++ b/Ptixed.Sql.Postgres/DatabaseExtensions.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ptixed.Sql.Meta; +using Ptixed.Sql.Postgres; + +namespace Ptixed.Sql.Postgres +{ + public static class DatabaseExtensions + { + public static List ToList(this IDatabase db, FormattableString query, params Type[] types) + => db.Query(new Query(query), types).ToList(); + + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7) }); + public static List ToList(this IDatabase db, FormattableString query) + => ToList(db, query, new[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8) }); + + public static T Single(this IDatabase db, FormattableString query) + => db.Query(new Query(query)).Single(); + + public static T SingleOrDefault(this IDatabase db, FormattableString query) + => db.Query(new Query(query)).SingleOrDefault(); + + public static T FirstOrDefault(this IDatabase db, FormattableString query) + => db.Query(new Query(query)).FirstOrDefault(); + + public static int NonQuery(this IDatabase db, FormattableString query) + => db.NonQuery(new Query(query)); + + public static List GetByIds(this IDatabase db, params object[] ids) + { + if (ids.Length == 0) + return new List(); + return db.Query(QueryHelper.GetById(ids)).ToList(); + } + + public static T GetById(this IDatabase db, object id) + { + return GetByIds(db, id).SingleOrDefault(); + } + + public static List Insert(this IDatabase db, params T[] entities) + { + if (entities.Length == 0) + return new List(); + var result = db.Query(QueryHelper.Insert(entities)).ToList(); + + var table = Table.Get(typeof(T)); + + if (table.AutoIncrementColumn != null) + foreach (var (inserted, i) in result.Select((x, i) => (x, i))) + table[entities[i], table.AutoIncrementColumn.LogicalColumn] = table[inserted, table.AutoIncrementColumn.LogicalColumn]; + + return entities.ToList(); + } + + public static T Insert(this IDatabase db, T entity) + { + return Insert(db, new [] { entity })[0]; + } + + public static int Update(this IDatabase db, params object[] entities) + { + if (entities.Length == 0) + return 0; + return db.NonQuery(QueryHelper.Update(entities)); + } + + public static int Delete(this IDatabase db, params object[] entities) + { + if (entities.Length == 0) + return 0; + return db.NonQuery(QueryHelper.Delete(entities)); + } + + public static int Delete(this IDatabase db, params object[] ids) + { + if (ids.Length == 0) + return 0; + return db.NonQuery(QueryHelper.Delete(ids)); + } + } +} diff --git a/Ptixed.Sql.Postgres/IDatabase.cs b/Ptixed.Sql.Postgres/IDatabase.cs new file mode 100644 index 0000000..3637cd0 --- /dev/null +++ b/Ptixed.Sql.Postgres/IDatabase.cs @@ -0,0 +1,8 @@ +using Npgsql; + +namespace Ptixed.Sql.Postgres +{ + public interface IDatabase : IDatabase + { + } +} diff --git a/Ptixed.Sql.Postgres/Ptixed.Sql.Postgres.csproj b/Ptixed.Sql.Postgres/Ptixed.Sql.Postgres.csproj new file mode 100644 index 0000000..3190186 --- /dev/null +++ b/Ptixed.Sql.Postgres/Ptixed.Sql.Postgres.csproj @@ -0,0 +1,22 @@ + + + netstandard2.0 + False + Ptixed.Sql.Postgres + 0.1.0 + Library for interacting with Microsoft SQL databases + ptixed + https://github.com/ptixed/Ptixed.Sql + ptixed sql tsql database db + 0.1.0 + 0.1.0 + + https://raw.githubusercontent.com/ptixed/Ptixed.Sql/master/logo-nuget.png + + + + + + + + diff --git a/Ptixed.Sql.Postgres/Query.cs b/Ptixed.Sql.Postgres/Query.cs new file mode 100644 index 0000000..24d0031 --- /dev/null +++ b/Ptixed.Sql.Postgres/Query.cs @@ -0,0 +1,33 @@ +using Npgsql; +using Ptixed.Sql.Meta; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ptixed.Sql.Postgres +{ + public class Query : Query + { + public static Query Unsafe(string query) => new Query(FormattableStringFactory.Create(query)); + + public Query() { } + public Query(FormattableString query) : base(query) { } + + protected override bool Format(object o, ref int index, MappingConfig mapping, List formants, List values) + { + switch (o) + { + case Table tm: + formants.Add($"\"{mapping.FormatTableName(tm)}\""); + return true; + case PhysicalColumn pc: + formants.Add($"\"{pc.Name}\""); + return true; + case ColumnValue cv: + formants.Add($"\"{cv.Name}\""); + return true; + } + return false; + } + } +} diff --git a/Ptixed.Sql.Postgres/QueryHelper.cs b/Ptixed.Sql.Postgres/QueryHelper.cs new file mode 100644 index 0000000..d0045a3 --- /dev/null +++ b/Ptixed.Sql.Postgres/QueryHelper.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Ptixed.Sql.Impl; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Postgres +{ + public static class QueryHelper + { + public static Query GetPrimaryKeyCondition(Table table, object key) + { + var query = new Query(); + query.Append($"("); + query.Append($" AND ", table.PrimaryKey.FromValueToQuery(key).Select(column => new Query($"{column} = {column.Value}"))); + query.Append($")"); + return query; + } + + public static Query GetById(params object[] ids) + { + if (ids == null || ids.Length == 0) + return null; + + var table = Table.Get(typeof(T)); + + var query = new Query(); + query.Append($"SELECT * FROM {table} WHERE "); + query.Append($" OR ", ids.Select(id => GetPrimaryKeyCondition(table, id))); + return query; + } + + public static Query Insert(params T[] entities) + { + if (entities == null || entities.Length == 0) + return null; + + var table = Table.Get(typeof(T)); + var columns = table.PhysicalColumns.Except(new[] { table.AutoIncrementColumn }).ToList(); + + var query = new Query(); + query.Append($"INSERT INTO {table} ("); + query.Append($", ", columns.Select(x => new Query($"{x}"))); + query.Append($") VALUES "); + query.Append($", ", entities.Select(entity => + { + var values = table.ToQuery(columns, entity) + .Select(column => new Query($"{column.Value}")) + .ToList(); + + var q = new Query(); + q.Append($"("); + q.Append($", ", values); + q.Append($")"); + return q; + })); + query.Append($" RETURNING "); + query.Append($", ", table.PhysicalColumns.Select(column => new Query($"{column}"))); + return query; + } + + public static Query Update(params object[] entities) + { + if (entities == null || entities.Length == 0) + return null; + + var query = new Query(); + for (var i = 0; i < entities.Length; ++i) + { + var entity = entities[i]; + var table = Table.Get(entity.GetType()); + var columns = table.PhysicalColumns.Where(x => !x.IsPrimaryKey).ToList(); + + var values = table.ToQuery(columns, entity) + .Select(column => new Query($"{column} = {column.Value}")) + .ToList(); + + query.Append($"UPDATE {table} SET "); + query.Append($", ", values); + query.Append($" WHERE "); + query.Append(GetPrimaryKeyCondition(table, table[entity, table.PrimaryKey])); + + if (i < entities.Length - 1) + query.Append($";"); + } + return query; + } + + public static Query Delete(params object[] entities) + { + if (entities == null || entities.Length == 0) + return null; + + var query = new Query(); + for (var i = 0; i < entities.Length; ++i) + { + var entity = entities[i]; + var table = Table.Get(entity.GetType()); + var id = table[entity, table.PrimaryKey]; + + query.Append($"DELETE FROM {table} WHERE "); + query.Append(GetPrimaryKeyCondition(table, id)); + + if (i < entities.Length - 1) + query.Append($";"); + } + return query; + } + + public static Query Delete(params object[] ids) + { + if (ids == null || ids.Length == 0) + return null; + + var table = Table.Get(typeof(T)); + + var query = new Query(); + query.Append($"DELETE FROM {table} WHERE "); + query.Append($" OR ", ids.Select(id => GetPrimaryKeyCondition(table, id))); + return query; + } + } +} diff --git a/Ptixed.Sql.SqlServer/Database.cs b/Ptixed.Sql.SqlServer/Database.cs index f986084..e3ab886 100644 --- a/Ptixed.Sql.SqlServer/Database.cs +++ b/Ptixed.Sql.SqlServer/Database.cs @@ -4,7 +4,7 @@ namespace Ptixed.Sql.SqlServer { - public class Database : Ptixed.Sql.Impl.Database, IDatabase + public class Database : Database, IDatabase { public Database(ConnectionConfig config) : base(config) { diff --git a/Ptixed.Sql.SqlServer/Query.cs b/Ptixed.Sql.SqlServer/Query.cs index 1118621..e34acce 100644 --- a/Ptixed.Sql.SqlServer/Query.cs +++ b/Ptixed.Sql.SqlServer/Query.cs @@ -1,4 +1,6 @@ -using System; +using Ptixed.Sql.Meta; +using System; +using System.Collections.Generic; using System.Data.SqlClient; using System.Runtime.CompilerServices; @@ -10,5 +12,22 @@ public class Query : Query public Query() { } public Query(FormattableString query) : base(query) { } + + protected override bool Format(object o, ref int index, MappingConfig mapping, List formants, List values) + { + switch (o) + { + case Table tm: + formants.Add(mapping.FormatTableName(tm)); + return true; + case PhysicalColumn pc: + formants.Add($"[{pc.Name}]"); + return true; + case ColumnValue cv: + formants.Add($"[{cv.Name}]"); + return true; + } + return false; + } } } diff --git a/Ptixed.Sql.SqlServer/QueryHelper.cs b/Ptixed.Sql.SqlServer/QueryHelper.cs index 22351cf..2b8613e 100644 --- a/Ptixed.Sql.SqlServer/QueryHelper.cs +++ b/Ptixed.Sql.SqlServer/QueryHelper.cs @@ -98,15 +98,9 @@ public static Query Upsert(Query searchCondition, T entity) public static Query Insert(string table, IDictionary values) { - if (table.Contains(']')) - throw PtixedException.InvalidIdenitfier(table); - foreach (var column in values.Keys) - if (column.Contains(']')) - throw PtixedException.InvalidIdenitfier(column); - var query = new Query(); query.Append(Query.Unsafe($"INSERT INTO [{table}] (")); - query.Append($", ", values.Select(x => Query.Unsafe($"[{x.Key}]"))); + query.Append($", ", values.Select(x => new Query($"{new PhysicalColumn(null, x.Key, false)}"))); query.Append($") VALUES ("); query.Append($", ", values.Select(x => new Query($"{x.Value}"))); query.Append($")\n\n"); @@ -134,7 +128,6 @@ public static Query Update(params object[] entities) query.Append($", ", values); query.Append($" WHERE "); query.Append(GetPrimaryKeyCondition(table, table[entity, table.PrimaryKey])); - return query; if (i < entities.Length - 1) query.Append($"\n\n"); diff --git a/Ptixed.Sql.Tests/AccessorTests.cs b/Ptixed.Sql.Tests/AccessorTests.cs new file mode 100644 index 0000000..1fe7931 --- /dev/null +++ b/Ptixed.Sql.Tests/AccessorTests.cs @@ -0,0 +1,99 @@ +using Ptixed.Sql.Impl; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Ptixed.Sql.Tests +{ + public class AccessorTests + { + class Foo + { + public static int StaticField; + public static int StaticProperty { get; set; } + + public int Field; + public static Foo Property { get; set; } + + private int PrivateFoo() + { + return 1; + } + + private static Foo StaticFoo() + { + return new Foo() { Field = 1 }; + } + + public void VoidMethod(int i, Foo f) + { + Field = i; + } + + public Foo Method() + { + return this; + } + + public Foo() + { + + } + + public Foo(int i, Foo f) + { + Field = i; + } + + public Foo(string s) + { + + } + } + + [Fact] + public void TestAccessor() + { + var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; + + var members = typeof(Foo).GetMethods(flags).ToDictionary(x => x.Name, x => (MemberInfo)x); + members.Add(".ctor", typeof(Foo).GetConstructor(Type.EmptyTypes)); + members.Add(".ctor`1", typeof(Foo).GetConstructor(new[] { typeof(int), typeof(Foo) })); + members.Add(".ctor`1b", typeof(Foo).GetConstructor(new[] { typeof(string) })); + foreach (var member in typeof(Foo).GetProperties(flags)) + members.Add(member.Name, member); + foreach (var member in typeof(Foo).GetFields(flags)) + members.Add(member.Name, member); + + var accessor = new Accessor(typeof(Foo), members); + + var foo = accessor.Invoke(null, ".ctor"); + Assert.NotNull(foo); + foo = accessor.Invoke(null, ".ctor`1", 1, new Foo()); + Assert.Equal(1, foo.Field); + accessor[null, "StaticField"] = 1; + Assert.Equal(1, accessor[null, "StaticField"]); + accessor[null, "StaticProperty"] = 1; + Assert.Equal(1, accessor[null, "StaticProperty"]); + accessor[foo, "Field"] = 1; + Assert.Equal(1, accessor[foo, "Field"]); + accessor[foo, "Property"] = foo; + Assert.Equal(foo, accessor[foo, "Property"]); + var v1 = accessor.Invoke(foo, "PrivateFoo"); + Assert.Equal(1, v1); + var v2 = accessor.Invoke(null, "StaticFoo"); + Assert.Equal(1, v2.Field); + var v3 = accessor.Invoke(foo, "Method"); + Assert.Equal(foo, v3); + var v4 = accessor.Invoke(foo, "VoidMethod", 1, new Foo()); + Assert.Equal(1, foo.Field); + Assert.Null(v4); + + var accessor2 = new Accessor(typeof(KeyValuePair), typeof(KeyValuePair).GetProperties().ToDictionary(x => x.Name, x => x as MemberInfo)); + var v5 = new KeyValuePair("foo", new Foo()); + Assert.Equal("foo", accessor2[v5, "Key"]); + } + } +} diff --git a/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs b/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs new file mode 100644 index 0000000..1489709 --- /dev/null +++ b/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs @@ -0,0 +1,55 @@ +using System; +using Ptixed.Sql.Impl; +using Ptixed.Sql.Postgres; + +namespace Ptixed.Sql.Tests.Postgres +{ + public class DatabaseFixture : IDisposable + { + private readonly ConnectionConfig _config = new ConnectionConfig("Host=cm-cloud.adrentech.com; Port=5444; Database=ptixed; Username=sa; Password=fy7B32dFgC4utAziA5zkLOitTWEAQlsHLi1ROgRMGII1;"); + + public DatabaseFixture() + { + using (var db = OpenConnection()) + { + var drop1 = new Query($@"drop table if exists ""Model"""); + var create1 = new Query($@"create table ""Model"" + ( ""client"" serial not null + , ""question"" uuid not null + , ""EnumAsInt"" int not null + , ""EnumAsString"" varchar(50) null + , ""sub"" varchar(1000) null + , ""SomeConstant"" varchar(50) null + , ""DROP"" varchar(50) null + , ""created"" timestamp + , primary key (""client"", ""question"") + )"); + + var drop2 = new Query($@"drop table if exists ""Model2"""); + var create2 = new Query($@"create table ""Model2"" + ( ""Id"" serial not null + , ""ModelId"" int null + , primary key (""Id"") + )"); + + var drop3 = new Query($@"drop table if exists ""ModelUpsert"""); + var create3 = new Query($@"create table ""ModelUpsert"" + ( ""Id"" serial not null + , ""Name"" varchar(256) null + , ""Age"" int null + , primary key (""Id"") + )"); + + db.NonQuery(drop1, create1, drop2, create2, drop3, create3); + } + } + + public void Dispose() + { + using (var db = OpenConnection()) + db.NonQuery(new Query($@"drop table ""Model""; drop table ""Model2""; drop table ""ModelUpsert"";")); + } + + public Database OpenConnection() => new Database(_config); + } +} diff --git a/Ptixed.Sql.Tests/Postgres/QueryTests.cs b/Ptixed.Sql.Tests/Postgres/QueryTests.cs new file mode 100644 index 0000000..cbd4ad9 --- /dev/null +++ b/Ptixed.Sql.Tests/Postgres/QueryTests.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using Npgsql; +using Ptixed.Sql.Postgres; +using Ptixed.Sql.Tests.Specimen; +using Xunit; + +namespace Ptixed.Sql.Tests.Postgres +{ + public class QueryTests : IClassFixture, IDisposable + { + private DatabaseFixture _db; + + public QueryTests(DatabaseFixture dbfixture) + { + _db = dbfixture; + } + + public void Dispose() + { + using (var db = _db.OpenConnection()) + db.NonQuery($@"DELETE FROM ""Model"""); + } + + [Fact] + public void TestQueries() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + }, + EnumAsString = SomeEnum.SomeEnumValue2, + SubModel = new SubModelClass + { + Id = 8 + } + }; + + var inserted = db.Insert(model); + Assert.Equal(model, inserted); + Assert.NotEqual(0, model.Id.ClientId); + + var selected = db.GetById(model.Id); + Assert.Equal(model.Id.ClientId, selected.Id.ClientId); + Assert.Equal(model.Id.QuestionId, selected.Id.QuestionId); + Assert.Equal(model.CreatedAt, selected.CreatedAt); + Assert.Equal(model.EnumAsInt, selected.EnumAsInt); + Assert.Equal(model.EnumAsString, selected.EnumAsString); + Assert.Equal(model.SubModel.Id, selected.SubModel.Id); + + model.CreatedAt = new DateTime(2019, 10, 19, 17, 21, 0); + db.Update(model); + selected = db.GetById(model.Id); + Assert.Equal(model.CreatedAt, selected.CreatedAt); + + db.Delete(model); + Assert.Null(db.GetById(model.Id)); + } + } + + [Fact] + public void TestRelations() + { + using (var db = _db.OpenConnection()) + { + var model = db.Insert(new Model + { + Id = new ModelKey + { + QuestionId = Guid.NewGuid() + } + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + + var q1 = new Query($@"SELECT m.*, m2.* FROM ""Model"" m JOIN ""Model2"" m2 ON m2.""ModelId"" = m.""client"" WHERE m.""client"" = {model.Id.ClientId}"); + var read1 = db.Query(q1, typeof(Model), typeof(Model2)).Single(); + Assert.Equal(2, read1.Related2.Count); + Assert.Equal(read1, read1.Related2[0].Model); + Assert.Equal(read1, read1.Related2[1].Model); + + var q2 = new Query($@"SELECT m.*, m2.* FROM ""Model"" m LEFT JOIN ""Model2"" m2 ON m2.""ModelId"" = -1 WHERE m.""client"" = {model.Id.ClientId}"); + var read2 = db.Query(q2, typeof(Model), typeof(Model2)).Single(); + Assert.Empty(read2.Related2); + } + } + + [Fact] + public void TestTransactions() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + } + }; + + using (var tran = db.OpenTransaction(IsolationLevel.Serializable)) + { + db.Insert(model); + } + Assert.Null(db.GetById(model.Id)); + + using (var tran = db.OpenTransaction(IsolationLevel.Serializable)) + { + db.Insert(model); + tran.Commit(); + } + Assert.NotNull(db.GetById(model.Id)); + } + } + + [Fact] + public void TestScalarQueries() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + } + }; + db.Insert(model); + + var count = db.Single($@"SELECT COUNT(*) FROM ""Model"""); + Assert.Equal(1, count); + + var countd = db.Single($@"SELECT COUNT(*) FROM ""Model"""); + Assert.Equal(1, countd); + + var now = db.Single($"SELECT CURRENT_TIMESTAMP"); + Assert.Equal(DateTime.Now.Date, now.Value.Date); + + var str = db.Single($@"SELECT ""SomeConstant"" FROM ""Model"""); + Assert.Equal(model.SomeConstant, str); + } + } + + [Fact] + public void TestModelWithNoPk() + { + using (var db = _db.OpenConnection()) + { + var model = new ModelWithNoPk + { + QuestionId = Guid.NewGuid(), + EnumAsString = SomeEnum.SomeEnumValue2, + SubModel = new SubModelClass + { + Id = 8 + } + }; + + var inserted = db.Insert(model); + + var selected = db.Single($@"SELECT * FROM ""Model"""); + Assert.Equal(model.QuestionId, selected.Id.QuestionId); + Assert.Equal(model.CreatedAt, selected.CreatedAt); + Assert.Equal(model.EnumAsInt, selected.EnumAsInt); + Assert.Equal(model.EnumAsString, selected.EnumAsString); + Assert.Equal(model.SubModel.Id, selected.SubModel.Id); + + var selected2 = db.Single($@"SELECT * FROM ""Model"""); + Assert.Equal(model.QuestionId, selected2.QuestionId); + Assert.Equal(model.CreatedAt, selected2.CreatedAt); + Assert.Equal(model.EnumAsInt, selected2.EnumAsInt); + Assert.Equal(model.EnumAsString, selected2.EnumAsString); + Assert.Equal(model.SubModel.Id, selected2.SubModel.Id); + } + } + + [Fact] + public void TestDictionaryResult() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + }, + SubModel = new SubModelClass + { + Id = 777 + } + }; + db.Insert(model); + + var read1 = db.Single>($@"SELECT * FROM ""Model"""); + Assert.Equal(model.Id.ClientId, read1["client"]); + } + } + + [Fact] + public void TestTuples() + { + using (var db = _db.OpenConnection()) + { + var model = db.Insert(new Model + { + Id = new ModelKey + { + QuestionId = Guid.NewGuid() + } + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + + var (selected, count) = db.Single<(Model, int)>($@"SELECT *, (SELECT COUNT(*) FROM ""Model2"" WHERE ""Model2"".""ModelId"" = ""Model"".""client"") c FROM ""Model"""); + + Assert.Equal(model.Id.ClientId, selected.Id.ClientId); + Assert.Equal(model.Id.QuestionId, selected.Id.QuestionId); + + Assert.Equal(2, count); + } + } + + [Fact] + public void TestAffectedRows() + { + using (var db = _db.OpenConnection()) + { + db.Insert(new Model2 + { + ModelId = 1 + }); + db.Insert(new Model2 + { + ModelId = 2 + }); + + var q1 = new Query($@"DELETE FROM ""Model2"" WHERE ""Id"" = 1"); + var q2 = new Query($@"DELETE FROM ""Model2"" WHERE ""Id"" = 2"); + + var affected = db.NonQuery(q1, q2); + + Assert.Equal(2, affected); + } + } + + + [Fact] + public void TestListInsert() + { + using (var db = _db.OpenConnection()) + Assert.Throws(() => + db.Insert(new List + { + new Model2 + { + ModelId = 1 + } + }) + ); + } + } +} diff --git a/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj b/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj index 597206f..e71ef40 100644 --- a/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj +++ b/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/Ptixed.Sql.Tests/Specimens/EnumToStringConverter.cs b/Ptixed.Sql.Tests/Specimen/EnumToStringConverter.cs similarity index 92% rename from Ptixed.Sql.Tests/Specimens/EnumToStringConverter.cs rename to Ptixed.Sql.Tests/Specimen/EnumToStringConverter.cs index 3eee73e..1b9dbad 100644 --- a/Ptixed.Sql.Tests/Specimens/EnumToStringConverter.cs +++ b/Ptixed.Sql.Tests/Specimen/EnumToStringConverter.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - internal class EnumToStringConverter : ISqlConverter - { - public List GetColumns(PropertyInfo member) - => null; - - public List ToQuery(object value, LogicalColumn meta) - => new List { new ColumnValue(meta.PhysicalColumns[0].Name, value.ToString()) }; - - public object FromQuery(ColumnValueSet columns, LogicalColumn meta, MappingConfig mapping) - => Enum.Parse(meta.Member.PropertyType, columns[meta.PhysicalColumns[0].Name].ToString()); - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + internal class EnumToStringConverter : ISqlConverter + { + public List GetColumns(PropertyInfo member) + => null; + + public List ToQuery(object value, LogicalColumn meta) + => new List { new ColumnValue(meta.PhysicalColumns[0].Name, value.ToString()) }; + + public object FromQuery(ColumnValueSet columns, LogicalColumn meta, MappingConfig mapping) + => Enum.Parse(meta.Member.PropertyType, columns[meta.PhysicalColumns[0].Name].ToString()); + } +} diff --git a/Ptixed.Sql.Tests/Specimens/JsonSqlConverter.cs b/Ptixed.Sql.Tests/Specimen/JsonSqlConverter.cs similarity index 93% rename from Ptixed.Sql.Tests/Specimens/JsonSqlConverter.cs rename to Ptixed.Sql.Tests/Specimen/JsonSqlConverter.cs index dc2e508..291b954 100644 --- a/Ptixed.Sql.Tests/Specimens/JsonSqlConverter.cs +++ b/Ptixed.Sql.Tests/Specimen/JsonSqlConverter.cs @@ -1,22 +1,22 @@ -using System.Collections.Generic; -using System.Reflection; -using Newtonsoft.Json; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - internal class JsonSqlConverter : ISqlConverter - { - private readonly string _name; - public JsonSqlConverter(string name) => _name = name; - - public List GetColumns(PropertyInfo member) - => new List { new ColumnAttribute(_name) }; - - public List ToQuery(object value, LogicalColumn meta) - => new List { new ColumnValue(_name, JsonConvert.SerializeObject(value)) }; - - public object FromQuery(ColumnValueSet columns, LogicalColumn meta, MappingConfig mapping) - => JsonConvert.DeserializeObject(columns[_name]?.ToString(), meta.Member.PropertyType); - } -} +using System.Collections.Generic; +using System.Reflection; +using Newtonsoft.Json; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + internal class JsonSqlConverter : ISqlConverter + { + private readonly string _name; + public JsonSqlConverter(string name) => _name = name; + + public List GetColumns(PropertyInfo member) + => new List { new ColumnAttribute(_name) }; + + public List ToQuery(object value, LogicalColumn meta) + => new List { new ColumnValue(_name, JsonConvert.SerializeObject(value)) }; + + public object FromQuery(ColumnValueSet columns, LogicalColumn meta, MappingConfig mapping) + => JsonConvert.DeserializeObject(columns[_name]?.ToString(), meta.Member.PropertyType); + } +} diff --git a/Ptixed.Sql.Tests/Specimens/Model.cs b/Ptixed.Sql.Tests/Specimen/Model.cs similarity index 92% rename from Ptixed.Sql.Tests/Specimens/Model.cs rename to Ptixed.Sql.Tests/Specimen/Model.cs index c3e6896..0324d16 100644 --- a/Ptixed.Sql.Tests/Specimens/Model.cs +++ b/Ptixed.Sql.Tests/Specimen/Model.cs @@ -1,35 +1,36 @@ -using System; -using System.Collections.Generic; -using Ptixed.Sql.Impl; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - [Table("Model", nameof(Id))] - internal class Model - { - [Column] - [SqlConverter(typeof(CompositeColumnConverter))] - public ModelKey Id { get; set; } - - [Column] - public SomeEnum EnumAsInt { get; set; } - - [SqlConverter(typeof(EnumToStringConverter))] - public SomeEnum EnumAsString { get; set; } - - [Ignore] - public string Temp { get; set; } - - [SqlConverter(typeof(JsonSqlConverter), "sub")] - public SubModelClass SubModel { get; set; } - - public string SomeConstant => "SomeConstantValue"; - - [Column("created")] - public DateTime CreatedAt { get; set; } = new DateTime(2019, 10, 19, 13, 9, 0); - - [Relation] - public List Related2 { get; set; } - } -} +using System; +using System.Collections.Generic; +using Ptixed.Sql.Impl; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + [Table("Model", nameof(Id))] + internal class Model + { + [Column] + [SqlConverter(typeof(CompositeColumnConverter))] + public ModelKey Id { get; set; } + + [Column] + public SomeEnum EnumAsInt { get; set; } + + [SqlConverter(typeof(EnumToStringConverter))] + public SomeEnum EnumAsString { get; set; } + + [Ignore] + public string Temp { get; set; } + + [SqlConverter(typeof(JsonSqlConverter), "sub")] + public SubModelClass SubModel { get; set; } + + public string SomeConstant => "SomeConstantValue"; + public string DROP => "DROP"; + + [Column("created")] + public DateTime CreatedAt { get; set; } = new DateTime(2019, 10, 19, 13, 9, 0); + + [Relation] + public List Related2 { get; set; } + } +} diff --git a/Ptixed.Sql.Tests/Specimens/Model2.cs b/Ptixed.Sql.Tests/Specimen/Model2.cs similarity index 84% rename from Ptixed.Sql.Tests/Specimens/Model2.cs rename to Ptixed.Sql.Tests/Specimen/Model2.cs index 6de1bf2..ff09c8a 100644 --- a/Ptixed.Sql.Tests/Specimens/Model2.cs +++ b/Ptixed.Sql.Tests/Specimen/Model2.cs @@ -1,17 +1,17 @@ -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - [Table("Model2", nameof(Id))] - internal class Model2 - { - [Column(IsAutoIncrement = true)] - public int Id { get; set; } - - [Column] - public int ModelId { get; set; } - - [Relation] - public Model Model { get; set; } - } -} +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + [Table("Model2", nameof(Id))] + internal class Model2 + { + [Column(IsAutoIncrement = true)] + public int Id { get; set; } + + [Column] + public int ModelId { get; set; } + + [Relation] + public Model Model { get; set; } + } +} diff --git a/Ptixed.Sql.Tests/Specimens/ModelUpsert.cs b/Ptixed.Sql.Tests/Specimen/ModelUpsert.cs similarity index 86% rename from Ptixed.Sql.Tests/Specimens/ModelUpsert.cs rename to Ptixed.Sql.Tests/Specimen/ModelUpsert.cs index 136c8cd..f1234ba 100644 --- a/Ptixed.Sql.Tests/Specimens/ModelUpsert.cs +++ b/Ptixed.Sql.Tests/Specimen/ModelUpsert.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using Ptixed.Sql.Impl; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - [Table("ModelUpsert", nameof(Id))] - internal class ModelUpsert - { - [Column(IsAutoIncrement = true)] - public int Id { get; set; } - - [Column] - public string Name { get; set; } - [Column] - public int Age { get; set; } - } -} +using System; +using System.Collections.Generic; +using Ptixed.Sql.Impl; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + [Table("ModelUpsert", nameof(Id))] + internal class ModelUpsert + { + [Column(IsAutoIncrement = true)] + public int Id { get; set; } + + [Column] + public string Name { get; set; } + [Column] + public int Age { get; set; } + } +} diff --git a/Ptixed.Sql.Tests/Specimens/ModelWithNoPk.cs b/Ptixed.Sql.Tests/Specimen/ModelWithNoPk.cs similarity index 91% rename from Ptixed.Sql.Tests/Specimens/ModelWithNoPk.cs rename to Ptixed.Sql.Tests/Specimen/ModelWithNoPk.cs index 8b72494..b06e587 100644 --- a/Ptixed.Sql.Tests/Specimens/ModelWithNoPk.cs +++ b/Ptixed.Sql.Tests/Specimen/ModelWithNoPk.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - [Table("Model")] - internal class ModelWithNoPk - { - [Column("question")] - public Guid QuestionId { get; set; } - - public SomeEnum EnumAsInt { get; set; } - - [SqlConverter(typeof(EnumToStringConverter))] - public SomeEnum EnumAsString { get; set; } - - [Ignore] - public string Temp { get; set; } - - [SqlConverter(typeof(JsonSqlConverter), "sub")] - public SubModelClass SubModel { get; set; } - - public string SomeConstant => "SomeConstantValue"; - - [Column("created")] - public DateTime CreatedAt { get; set; } = new DateTime(2019, 10, 19, 13, 9, 0); - - [Relation] - public List Related2 { get; set; } - } -} +using System; +using System.Collections.Generic; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + [Table("Model")] + internal class ModelWithNoPk + { + [Column("question")] + public Guid QuestionId { get; set; } + + public SomeEnum EnumAsInt { get; set; } + + [SqlConverter(typeof(EnumToStringConverter))] + public SomeEnum EnumAsString { get; set; } + + [Ignore] + public string Temp { get; set; } + + [SqlConverter(typeof(JsonSqlConverter), "sub")] + public SubModelClass SubModel { get; set; } + + public string SomeConstant => "SomeConstantValue"; + + [Column("created")] + public DateTime CreatedAt { get; set; } = new DateTime(2019, 10, 19, 13, 9, 0); + + [Relation] + public List Related2 { get; set; } + } +} diff --git a/Ptixed.Sql.Tests/Specimens/Others.cs b/Ptixed.Sql.Tests/Specimen/Others.cs similarity index 90% rename from Ptixed.Sql.Tests/Specimens/Others.cs rename to Ptixed.Sql.Tests/Specimen/Others.cs index 637207e..f125f6a 100644 --- a/Ptixed.Sql.Tests/Specimens/Others.cs +++ b/Ptixed.Sql.Tests/Specimen/Others.cs @@ -1,27 +1,27 @@ -using System; -using Ptixed.Sql.Meta; - -namespace Ptixed.Sql.Tests.Specimens -{ - public class SubModelClass - { - public int Id { get; set; } - } - - public enum SomeEnum - { - SomeEnumValue1, - SomeEnumValue2 - } - - public class ModelKey - { - [Column("client", IsAutoIncrement = true)] - public int ClientId { get; set; } - [Column("question")] - public Guid QuestionId { get; set; } - - public override bool Equals(object obj) => obj is ModelKey other && other.ClientId == ClientId && QuestionId == other.QuestionId; - public override int GetHashCode() => ClientId ^ QuestionId.GetHashCode(); - } -} +using System; +using Ptixed.Sql.Meta; + +namespace Ptixed.Sql.Tests.Specimen +{ + public class SubModelClass + { + public int Id { get; set; } + } + + public enum SomeEnum + { + SomeEnumValue1, + SomeEnumValue2 + } + + public class ModelKey + { + [Column("client", IsAutoIncrement = true)] + public int ClientId { get; set; } + [Column("question")] + public Guid QuestionId { get; set; } + + public override bool Equals(object obj) => obj is ModelKey other && other.ClientId == ClientId && QuestionId == other.QuestionId; + public override int GetHashCode() => ClientId ^ QuestionId.GetHashCode(); + } +} diff --git a/Ptixed.Sql.Tests/DatabaseFixture.cs b/Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs similarity index 96% rename from Ptixed.Sql.Tests/DatabaseFixture.cs rename to Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs index eaa7843..c32d4e0 100644 --- a/Ptixed.Sql.Tests/DatabaseFixture.cs +++ b/Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs @@ -1,54 +1,55 @@ -using System; -using Ptixed.Sql.Impl; -using Ptixed.Sql.SqlServer; - -namespace Ptixed.Sql.Tests -{ - public class DatabaseFixture : IDisposable - { - private readonly ConnectionConfig _config = new ConnectionConfig("Server=.,14444;initial catalog=Ptixed; User=sa; Password=123QWEasd;"); - - public DatabaseFixture() - { - using (var db = OpenConnection()) - { - var drop1 = new Query($@"if exists (select * from sys.tables where name = 'Model') drop table Model"); - var create1 = new Query($@"create table Model - ( client int not null identity(1,1) - , question uniqueidentifier not null - , EnumAsInt int not null - , EnumAsString varchar(50) null - , sub varchar(1000) null - , SomeConstant varchar(50) null - , created datetime - , primary key (client, question) - )"); - - var drop2 = new Query($@"if exists (select * from sys.tables where name = 'Model2') drop table Model2"); - var create2 = new Query($@"create table Model2 - ( Id int not null identity(1,1) - , ModelId int null - , primary key (Id) - )"); - - var drop3 = new Query($@"if exists (select * from sys.tables where name = 'ModelUpsert') drop table ModelUpsert"); - var create3 = new Query($@"create table ModelUpsert - ( Id int not null identity(1,1) - , Name varchar(256) null - , Age int null - , primary key (Id) - )"); - - db.NonQuery(drop1, create1, drop2, create2, drop3, create3); - } - } - - public void Dispose() - { - using (var db = OpenConnection()) - db.NonQuery(new Query($"drop table Model; drop table Model2; drop table ModelUpsert;")); - } - - public Database OpenConnection() => new Database(_config); - } -} +using System; +using Ptixed.Sql.Impl; +using Ptixed.Sql.SqlServer; + +namespace Ptixed.Sql.Tests.SqlServer +{ + public class DatabaseFixture : IDisposable + { + private readonly ConnectionConfig _config = new ConnectionConfig("Server=.,14444;initial catalog=Ptixed; User=sa; Password=123QWEasd;"); + + public DatabaseFixture() + { + using (var db = OpenConnection()) + { + var drop1 = new Query($@"if exists (select * from sys.tables where name = 'Model') drop table Model"); + var create1 = new Query($@"create table Model + ( client int not null identity(1,1) + , question uniqueidentifier not null + , EnumAsInt int not null + , EnumAsString varchar(50) null + , sub varchar(1000) null + , SomeConstant varchar(50) null + , [DROP] varchar(50) null + , created datetime + , primary key (client, question) + )"); + + var drop2 = new Query($@"if exists (select * from sys.tables where name = 'Model2') drop table Model2"); + var create2 = new Query($@"create table Model2 + ( Id int not null identity(1,1) + , ModelId int null + , primary key (Id) + )"); + + var drop3 = new Query($@"if exists (select * from sys.tables where name = 'ModelUpsert') drop table ModelUpsert"); + var create3 = new Query($@"create table ModelUpsert + ( Id int not null identity(1,1) + , Name varchar(256) null + , Age int null + , primary key (Id) + )"); + + db.NonQuery(drop1, create1, drop2, create2, drop3, create3); + } + } + + public void Dispose() + { + using (var db = OpenConnection()) + db.NonQuery(new Query($"drop table Model; drop table Model2; drop table ModelUpsert;")); + } + + public Database OpenConnection() => new Database(_config); + } +} diff --git a/Ptixed.Sql.Tests/QueryTests.cs b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs similarity index 75% rename from Ptixed.Sql.Tests/QueryTests.cs rename to Ptixed.Sql.Tests/SqlServer/QueryTests.cs index dd61064..9484afa 100644 --- a/Ptixed.Sql.Tests/QueryTests.cs +++ b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs @@ -1,430 +1,342 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Reflection; -using Ptixed.Sql.Impl; -using Ptixed.Sql.SqlServer; -using Ptixed.Sql.Tests.Specimens; -using Xunit; - -namespace Ptixed.Sql.Tests -{ - public class QueryTests : IClassFixture, IDisposable - { - private DatabaseFixture _db; - - public QueryTests(DatabaseFixture dbfixture) - { - _db = dbfixture; - } - - public void Dispose() - { - using (var db = _db.OpenConnection()) - db.NonQuery($"DELETE FROM Model"); - } - - [Fact] - public void TestQueries() - { - using (var db = _db.OpenConnection()) - { - var model = new Model - { - Id = new ModelKey() - { - QuestionId = Guid.NewGuid() - }, - EnumAsString = SomeEnum.SomeEnumValue2, - SubModel = new SubModelClass - { - Id = 8 - } - }; - - var inserted = db.Insert(model); - Assert.Equal(model, inserted); - Assert.NotEqual(0, model.Id.ClientId); - - var selected = db.GetById(model.Id); - Assert.Equal(model.Id.ClientId, selected.Id.ClientId); - Assert.Equal(model.Id.QuestionId, selected.Id.QuestionId); - Assert.Equal(model.CreatedAt, selected.CreatedAt); - Assert.Equal(model.EnumAsInt, selected.EnumAsInt); - Assert.Equal(model.EnumAsString, selected.EnumAsString); - Assert.Equal(model.SubModel.Id, selected.SubModel.Id); - - model.CreatedAt = new DateTime(2019, 10, 19, 17, 21, 0); - db.Update(model); - selected = db.GetById(model.Id); - Assert.Equal(model.CreatedAt, selected.CreatedAt); - - db.Delete(model); - Assert.Null(db.GetById(model.Id)); - } - } - - [Fact] - public void TestRelations() - { - using (var db = _db.OpenConnection()) - { - var model = db.Insert(new Model - { - Id = new ModelKey - { - QuestionId = Guid.NewGuid() - } - }); - db.Insert(new Model2 - { - ModelId = model.Id.ClientId - }); - db.Insert(new Model2 - { - ModelId = model.Id.ClientId - }); - - var q1 = new Query($@"SELECT m.*, m2.* FROM Model m JOIN Model2 m2 ON m2.ModelId = m.client WHERE m.client = {model.Id.ClientId}"); - var read1 = db.Query(q1, typeof(Model), typeof(Model2)).Single(); - Assert.Equal(2, read1.Related2.Count); - Assert.Equal(read1, read1.Related2[0].Model); - Assert.Equal(read1, read1.Related2[1].Model); - - var q2 = new Query($@"SELECT m.*, m2.* FROM Model m LEFT JOIN Model2 m2 ON m2.ModelId = -1 WHERE m.client = {model.Id.ClientId}"); - var read2 = db.Query(q2, typeof(Model), typeof(Model2)).Single(); - Assert.Empty(read2.Related2); - } - } - - [Fact] - public void TestTransactions() - { - using (var db = _db.OpenConnection()) - { - var model = new Model - { - Id = new ModelKey() - { - QuestionId = Guid.NewGuid() - } - }; - - using (var tran = db.OpenTransaction(IsolationLevel.Serializable)) - { - db.Insert(model); - } - Assert.Null(db.GetById(model.Id)); - - using (var tran = db.OpenTransaction(IsolationLevel.Serializable)) - { - db.Insert(model); - tran.Commit(); - } - Assert.NotNull(db.GetById(model.Id)); - } - } - - [Fact] - public void TestScalarQueries() - { - using (var db = _db.OpenConnection()) - { - var model = new Model - { - Id = new ModelKey() - { - QuestionId = Guid.NewGuid() - } - }; - db.Insert(model); - - var count = db.Single($"SELECT COUNT(*) FROM Model"); - Assert.Equal(1, count); - - var countd = db.Single($"SELECT COUNT(*) FROM Model"); - Assert.Equal(1, countd); - - var now = db.Single($"SELECT CURRENT_TIMESTAMP"); - Assert.Equal(DateTime.Now.Date, now.Value.Date); - - var str = db.Single($"SELECT SomeConstant FROM Model"); - Assert.Equal(model.SomeConstant, str); - } - } - - [Fact] - public void TestModelWithNoPk() - { - using (var db = _db.OpenConnection()) - { - var model = new ModelWithNoPk - { - QuestionId = Guid.NewGuid(), - EnumAsString = SomeEnum.SomeEnumValue2, - SubModel = new SubModelClass - { - Id = 8 - } - }; - - var inserted = db.Insert(model); - - var selected = db.Single($"SELECT * FROM Model"); - Assert.Equal(model.QuestionId, selected.Id.QuestionId); - Assert.Equal(model.CreatedAt, selected.CreatedAt); - Assert.Equal(model.EnumAsInt, selected.EnumAsInt); - Assert.Equal(model.EnumAsString, selected.EnumAsString); - Assert.Equal(model.SubModel.Id, selected.SubModel.Id); - - var selected2 = db.Single($"SELECT * FROM Model"); - Assert.Equal(model.QuestionId, selected2.QuestionId); - Assert.Equal(model.CreatedAt, selected2.CreatedAt); - Assert.Equal(model.EnumAsInt, selected2.EnumAsInt); - Assert.Equal(model.EnumAsString, selected2.EnumAsString); - Assert.Equal(model.SubModel.Id, selected2.SubModel.Id); - } - } - - [Fact] - public void TestDictionaryResult() - { - using (var db = _db.OpenConnection()) - { - var model = new Model - { - Id = new ModelKey() - { - QuestionId = Guid.NewGuid() - }, - SubModel = new SubModelClass - { - Id = 777 - } - }; - db.Insert(model); - - var read1 = db.Single>($"SELECT * FROM Model"); - Assert.Equal(model.Id.ClientId, read1["client"]); - } - } - - [Fact] - public void TestTuples() - { - using (var db = _db.OpenConnection()) - { - var model = db.Insert(new Model - { - Id = new ModelKey - { - QuestionId = Guid.NewGuid() - } - }); - db.Insert(new Model2 - { - ModelId = model.Id.ClientId - }); - db.Insert(new Model2 - { - ModelId = model.Id.ClientId - }); - - var (selected, count) = db.Single<(Model, int)>($"SELECT *, (SELECT COUNT(*) FROM Model2 WHERE Model2.ModelId = Model.client) c FROM Model"); - - Assert.Equal(model.Id.ClientId, selected.Id.ClientId); - Assert.Equal(model.Id.QuestionId, selected.Id.QuestionId); - - Assert.Equal(2, count); - } - } - - [Fact] - public void TestAffectedRows() - { - using (var db = _db.OpenConnection()) - { - db.Insert(new Model2 - { - ModelId = 1 - }); - db.Insert(new Model2 - { - ModelId = 2 - }); - - var q1 = new Query($"DELETE FROM Model2 WHERE Id = 1"); - var q2 = new Query($"DELETE FROM Model2 WHERE Id = 2"); - - var affected = db.NonQuery(q1, q2); - - Assert.Equal(2, affected); - } - } - - class Foo - { - public static int StaticField; - public static int StaticProperty { get; set; } - - public int Field; - public static Foo Property { get; set; } - - private int PrivateFoo() - { - return 1; - } - - private static Foo StaticFoo() - { - return new Foo() { Field = 1 }; - } - - public void VoidMethod(int i, Foo f) - { - Field = i; - } - - public Foo Method() - { - return this; - } - - public Foo() - { - - } - - public Foo(int i, Foo f) - { - Field = i; - } - - public Foo(string s) - { - - } - } - - [Fact] - public void TestAccessor() - { - var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; - - var members = typeof(Foo).GetMethods(flags).ToDictionary(x => x.Name, x => (MemberInfo)x); - members.Add(".ctor", typeof(Foo).GetConstructor(Type.EmptyTypes)); - members.Add(".ctor`1", typeof(Foo).GetConstructor(new[] { typeof(int), typeof(Foo) })); - members.Add(".ctor`1b", typeof(Foo).GetConstructor(new[] { typeof(string) })); - foreach (var member in typeof(Foo).GetProperties(flags)) - members.Add(member.Name, member); - foreach (var member in typeof(Foo).GetFields(flags)) - members.Add(member.Name, member); - - var accessor = new Accessor(typeof(Foo), members); - - var foo = accessor.Invoke(null, ".ctor"); - Assert.NotNull(foo); - foo = accessor.Invoke(null, ".ctor`1", 1, new Foo()); - Assert.Equal(1, foo.Field); - accessor[null, "StaticField"] = 1; - Assert.Equal(1, accessor[null, "StaticField"]); - accessor[null, "StaticProperty"] = 1; - Assert.Equal(1, accessor[null, "StaticProperty"]); - accessor[foo, "Field"] = 1; - Assert.Equal(1, accessor[foo, "Field"]); - accessor[foo, "Property"] = foo; - Assert.Equal(foo, accessor[foo, "Property"]); - var v1 = accessor.Invoke(foo, "PrivateFoo"); - Assert.Equal(1, v1); - var v2 = accessor.Invoke(null, "StaticFoo"); - Assert.Equal(1, v2.Field); - var v3 = accessor.Invoke(foo, "Method"); - Assert.Equal(foo, v3); - var v4 = accessor.Invoke(foo, "VoidMethod", 1, new Foo()); - Assert.Equal(1, foo.Field); - Assert.Null(v4); - - var accessor2 = new Accessor(typeof(KeyValuePair), typeof(KeyValuePair).GetProperties().ToDictionary(x => x.Name, x => x as MemberInfo)); - var v5 = new KeyValuePair("foo", new Foo()); - Assert.Equal("foo", accessor2[v5, "Key"]); - } - - [Fact] - public void TestListInsert() - { - using (var db = _db.OpenConnection()) - Assert.Throws(() => - db.Insert(new List - { - new Model2 - { - ModelId = 1 - } - }) - ); - } - - [Fact] - public void TestDictionaryInsert() - { - using (var db = _db.OpenConnection()) - { - var autoincr = db.Insert("Model", new Dictionary - { - { "question", Guid.NewGuid() }, - { "EnumAsInt", (int)SomeEnum.SomeEnumValue1 }, - { "EnumAsString", SomeEnum.SomeEnumValue2.ToString() }, - { "sub", "{ id: 1 }" }, - { "SomeConstant", "SomeConstantValue" }, - { "created", DateTime.Now } - }); - var model = db.Single($"SELECT * FROM Model"); - Assert.Equal(autoincr, model.Id.ClientId.ToString()); - } - } - - [Fact] - public void TestUpsertQueries() - { - using (var db = _db.OpenConnection()) - { - var model = new ModelUpsert - { - Age = 1, - Name = "John Doe" - }; - - var inserted = db.Insert(model); - Assert.True(inserted.Id > 0); - - var response = db.Upsert($"ON [Target].Id = [Source].Id", new ModelUpsert - { - Name = "Jane Doe", - Age = 12, - Id = inserted.Id - }); - Assert.NotNull(response); - Assert.True(response.Age == 12); - - var byId = db.GetById(inserted.Id); - Assert.NotNull(byId); - Assert.True(byId.Age == 12); - - response = db.Upsert($"ON [Target].Id = [Source].Id", new ModelUpsert - { - Name = "Janice Doe", - Age = 21 - }); - Assert.NotNull(response); - Assert.True(response.Age == 21); - Assert.True(response.Id != inserted.Id); - var byName = db.ToList($"SELECT * FROM ModelUpsert WHERE Name like 'Janice Doe'"); - Assert.NotNull(byName); - Assert.True(byName.First().Age == 21); - Assert.True(byName.First().Id != inserted.Id); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using Ptixed.Sql.SqlServer; +using Ptixed.Sql.Tests.Specimen; +using Xunit; + +namespace Ptixed.Sql.Tests.SqlServer +{ + public class QueryTests : IClassFixture, IDisposable + { + private DatabaseFixture _db; + + public QueryTests(DatabaseFixture dbfixture) + { + _db = dbfixture; + } + + public void Dispose() + { + using (var db = _db.OpenConnection()) + db.NonQuery($"DELETE FROM Model"); + } + + [Fact] + public void TestQueries() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + }, + EnumAsString = SomeEnum.SomeEnumValue2, + SubModel = new SubModelClass + { + Id = 8 + } + }; + + var inserted = db.Insert(model); + Assert.Equal(model, inserted); + Assert.NotEqual(0, model.Id.ClientId); + + var selected = db.GetById(model.Id); + Assert.Equal(model.Id.ClientId, selected.Id.ClientId); + Assert.Equal(model.Id.QuestionId, selected.Id.QuestionId); + Assert.Equal(model.CreatedAt, selected.CreatedAt); + Assert.Equal(model.EnumAsInt, selected.EnumAsInt); + Assert.Equal(model.EnumAsString, selected.EnumAsString); + Assert.Equal(model.SubModel.Id, selected.SubModel.Id); + + model.CreatedAt = new DateTime(2019, 10, 19, 17, 21, 0); + db.Update(model); + selected = db.GetById(model.Id); + Assert.Equal(model.CreatedAt, selected.CreatedAt); + + db.Delete(model); + Assert.Null(db.GetById(model.Id)); + } + } + + [Fact] + public void TestRelations() + { + using (var db = _db.OpenConnection()) + { + var model = db.Insert(new Model + { + Id = new ModelKey + { + QuestionId = Guid.NewGuid() + } + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + + var q1 = new Query($@"SELECT m.*, m2.* FROM Model m JOIN Model2 m2 ON m2.ModelId = m.client WHERE m.client = {model.Id.ClientId}"); + var read1 = db.Query(q1, typeof(Model), typeof(Model2)).Single(); + Assert.Equal(2, read1.Related2.Count); + Assert.Equal(read1, read1.Related2[0].Model); + Assert.Equal(read1, read1.Related2[1].Model); + + var q2 = new Query($@"SELECT m.*, m2.* FROM Model m LEFT JOIN Model2 m2 ON m2.ModelId = -1 WHERE m.client = {model.Id.ClientId}"); + var read2 = db.Query(q2, typeof(Model), typeof(Model2)).Single(); + Assert.Empty(read2.Related2); + } + } + + [Fact] + public void TestTransactions() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + } + }; + + using (var tran = db.OpenTransaction(IsolationLevel.Serializable)) + { + db.Insert(model); + } + Assert.Null(db.GetById(model.Id)); + + using (var tran = db.OpenTransaction(IsolationLevel.Serializable)) + { + db.Insert(model); + tran.Commit(); + } + Assert.NotNull(db.GetById(model.Id)); + } + } + + [Fact] + public void TestScalarQueries() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + } + }; + db.Insert(model); + + var count = db.Single($"SELECT COUNT(*) FROM Model"); + Assert.Equal(1, count); + + var countd = db.Single($"SELECT COUNT(*) FROM Model"); + Assert.Equal(1, countd); + + var now = db.Single($"SELECT CURRENT_TIMESTAMP"); + Assert.Equal(DateTime.Now.Date, now.Value.Date); + + var str = db.Single($"SELECT SomeConstant FROM Model"); + Assert.Equal(model.SomeConstant, str); + } + } + + [Fact] + public void TestModelWithNoPk() + { + using (var db = _db.OpenConnection()) + { + var model = new ModelWithNoPk + { + QuestionId = Guid.NewGuid(), + EnumAsString = SomeEnum.SomeEnumValue2, + SubModel = new SubModelClass + { + Id = 8 + } + }; + + var inserted = db.Insert(model); + + var selected = db.Single($"SELECT * FROM Model"); + Assert.Equal(model.QuestionId, selected.Id.QuestionId); + Assert.Equal(model.CreatedAt, selected.CreatedAt); + Assert.Equal(model.EnumAsInt, selected.EnumAsInt); + Assert.Equal(model.EnumAsString, selected.EnumAsString); + Assert.Equal(model.SubModel.Id, selected.SubModel.Id); + + var selected2 = db.Single($"SELECT * FROM Model"); + Assert.Equal(model.QuestionId, selected2.QuestionId); + Assert.Equal(model.CreatedAt, selected2.CreatedAt); + Assert.Equal(model.EnumAsInt, selected2.EnumAsInt); + Assert.Equal(model.EnumAsString, selected2.EnumAsString); + Assert.Equal(model.SubModel.Id, selected2.SubModel.Id); + } + } + + [Fact] + public void TestDictionaryResult() + { + using (var db = _db.OpenConnection()) + { + var model = new Model + { + Id = new ModelKey() + { + QuestionId = Guid.NewGuid() + }, + SubModel = new SubModelClass + { + Id = 777 + } + }; + db.Insert(model); + + var read1 = db.Single>($"SELECT * FROM Model"); + Assert.Equal(model.Id.ClientId, read1["client"]); + } + } + + [Fact] + public void TestTuples() + { + using (var db = _db.OpenConnection()) + { + var model = db.Insert(new Model + { + Id = new ModelKey + { + QuestionId = Guid.NewGuid() + } + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + db.Insert(new Model2 + { + ModelId = model.Id.ClientId + }); + + var (selected, count) = db.Single<(Model, int)>($"SELECT *, (SELECT COUNT(*) FROM Model2 WHERE Model2.ModelId = Model.client) c FROM Model"); + + Assert.Equal(model.Id.ClientId, selected.Id.ClientId); + Assert.Equal(model.Id.QuestionId, selected.Id.QuestionId); + + Assert.Equal(2, count); + } + } + + [Fact] + public void TestAffectedRows() + { + using (var db = _db.OpenConnection()) + { + db.Insert(new Model2 + { + ModelId = 1 + }); + db.Insert(new Model2 + { + ModelId = 2 + }); + + var q1 = new Query($"DELETE FROM Model2 WHERE Id = 1"); + var q2 = new Query($"DELETE FROM Model2 WHERE Id = 2"); + + var affected = db.NonQuery(q1, q2); + + Assert.Equal(2, affected); + } + } + + + [Fact] + public void TestListInsert() + { + using (var db = _db.OpenConnection()) + Assert.Throws(() => + db.Insert(new List + { + new Model2 + { + ModelId = 1 + } + }) + ); + } + + [Fact] + public void TestDictionaryInsert() + { + using (var db = _db.OpenConnection()) + { + var autoincr = db.Insert("Model", new Dictionary + { + { "question", Guid.NewGuid() }, + { "EnumAsInt", (int)SomeEnum.SomeEnumValue1 }, + { "EnumAsString", SomeEnum.SomeEnumValue2.ToString() }, + { "sub", "{ id: 1 }" }, + { "SomeConstant", "SomeConstantValue" }, + { "created", DateTime.Now } + }); + var model = db.Single($"SELECT * FROM Model"); + Assert.Equal(autoincr, model.Id.ClientId.ToString()); + } + } + + [Fact] + public void TestUpsertQueries() + { + using (var db = _db.OpenConnection()) + { + var model = new ModelUpsert + { + Age = 1, + Name = "John Doe" + }; + + var inserted = db.Insert(model); + Assert.True(inserted.Id > 0); + + var response = db.Upsert($"ON [Target].Id = [Source].Id", new ModelUpsert + { + Name = "Jane Doe", + Age = 12, + Id = inserted.Id + }); + Assert.NotNull(response); + Assert.True(response.Age == 12); + + var byId = db.GetById(inserted.Id); + Assert.NotNull(byId); + Assert.True(byId.Age == 12); + + response = db.Upsert($"ON [Target].Id = [Source].Id", new ModelUpsert + { + Name = "Janice Doe", + Age = 21 + }); + Assert.NotNull(response); + Assert.True(response.Age == 21); + Assert.True(response.Id != inserted.Id); + var byName = db.ToList($"SELECT * FROM ModelUpsert WHERE Name like 'Janice Doe'"); + Assert.NotNull(byName); + Assert.True(byName.First().Age == 21); + Assert.True(byName.First().Id != inserted.Id); + } + } + } +} diff --git a/Ptixed.Sql.sln b/Ptixed.Sql.sln index 4c40ab5..768373e 100644 --- a/Ptixed.Sql.sln +++ b/Ptixed.Sql.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ptixed.Sql.SqlServer", "Ptixed.Sql.SqlServer\Ptixed.Sql.SqlServer.csproj", "{606E057B-CA06-4A06-966C-60C2A3E3C645}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ptixed.Sql.Postgres", "Ptixed.Sql.Postgres\Ptixed.Sql.Postgres.csproj", "{B2BE56F5-5DC3-45A3-AAAD-F3FD9A4252BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {606E057B-CA06-4A06-966C-60C2A3E3C645}.Debug|Any CPU.Build.0 = Debug|Any CPU {606E057B-CA06-4A06-966C-60C2A3E3C645}.Release|Any CPU.ActiveCfg = Release|Any CPU {606E057B-CA06-4A06-966C-60C2A3E3C645}.Release|Any CPU.Build.0 = Release|Any CPU + {B2BE56F5-5DC3-45A3-AAAD-F3FD9A4252BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2BE56F5-5DC3-45A3-AAAD-F3FD9A4252BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2BE56F5-5DC3-45A3-AAAD-F3FD9A4252BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2BE56F5-5DC3-45A3-AAAD-F3FD9A4252BC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Ptixed.Sql/ColumnValue.cs b/Ptixed.Sql/ColumnValue.cs index c078b47..e1840b3 100644 --- a/Ptixed.Sql/ColumnValue.cs +++ b/Ptixed.Sql/ColumnValue.cs @@ -12,7 +12,5 @@ public ColumnValue(string name, object value) Name = name; Value = value == DBNull.Value ? null : value; } - - public override string ToString() => $"[{Name}]"; } } diff --git a/Ptixed.Sql/Impl/Database.cs b/Ptixed.Sql/Impl/Database.cs index 9fced7d..e5ce25f 100755 --- a/Ptixed.Sql/Impl/Database.cs +++ b/Ptixed.Sql/Impl/Database.cs @@ -81,7 +81,7 @@ public int NonQuery(params Query[] query) if (query.Length == 0) return 0; - var command = query.Aggregate((x, y) => x.Append($"\n\n").Append(y)).ToSql(NewCommand(), Config.Mappping); + var command = query.Aggregate((x, y) => x.Append($";\n\n").Append(y)).ToSql(NewCommand(), Config.Mappping); return command.ExecuteNonQuery(); } diff --git a/Ptixed.Sql/MappingConfig.cs b/Ptixed.Sql/MappingConfig.cs index bc2dd57..f33016c 100644 --- a/Ptixed.Sql/MappingConfig.cs +++ b/Ptixed.Sql/MappingConfig.cs @@ -37,6 +37,8 @@ public object ToDb(Type type, object obj) protected virtual Func ToDbImpl(Type type) { + if (type.IsEnum) + return x => (int)x; return x => x; } diff --git a/Ptixed.Sql/Meta/PhysicalColumn.cs b/Ptixed.Sql/Meta/PhysicalColumn.cs index 9c2f0c7..aae287c 100644 --- a/Ptixed.Sql/Meta/PhysicalColumn.cs +++ b/Ptixed.Sql/Meta/PhysicalColumn.cs @@ -18,7 +18,6 @@ public PhysicalColumn(LogicalColumn column, string name, bool isai) public static bool operator ==(PhysicalColumn l, PhysicalColumn r) => l?.Equals(r) ?? ReferenceEquals(r, null); public static bool operator !=(PhysicalColumn l, PhysicalColumn r) => !(l == r); - public override string ToString() => $"[{Name}]"; public override bool Equals(object obj) => obj is PhysicalColumn wl && wl.LogicalColumn == LogicalColumn && wl.Name == Name; public override int GetHashCode() => LogicalColumn.GetHashCode() ^ Name.GetHashCode(); diff --git a/Ptixed.Sql/Meta/Table.cs b/Ptixed.Sql/Meta/Table.cs index b2f9a87..6e57d23 100644 --- a/Ptixed.Sql/Meta/Table.cs +++ b/Ptixed.Sql/Meta/Table.cs @@ -72,7 +72,6 @@ private Table(Type type) public static bool operator ==(Table l, Table r) => l?.Equals(r) ?? ReferenceEquals(r, null); public static bool operator !=(Table l, Table r) => !(l == r); - public override string ToString() => $"[{Name}]"; public override bool Equals(object obj) => obj is Table t && t.Name == Name; public override int GetHashCode() => Name.GetHashCode(); diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index a8c2351..00f8c02 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -71,6 +71,8 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) return command; } + protected abstract bool Format(object o, ref int index, MappingConfig mapping, List formants, List values); + private (string, List) ToSql(ref int index, MappingConfig mapping) { var sb = new StringBuilder(); @@ -79,6 +81,9 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) { var formants = new List(); foreach (var argument in part.GetArguments()) + { + if (Format(argument, ref index, mapping, formants, values)) + continue; switch (argument) { case null: @@ -89,18 +94,9 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) values.AddRange(vs); formants.Add(text); break; - case Table tm: - formants.Add(mapping.FormatTableName(tm)); - break; case Type t: formants.Add(mapping.FormatTableName(Table.Get(t))); break; - case PhysicalColumn pc: - formants.Add(pc.ToString()); - break; - case ColumnValue cv: - formants.Add(cv.ToString()); - break; case int i: formants.Add(i.ToString()); break; @@ -134,6 +130,7 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) formants.Add("@" + index++.ToString()); break; } + } sb.Append(string.Format(part.Format, formants.ToArray())); } return (sb.ToString(), values); From ee6caf18c98a1b7631b87f77c22fc3437f2fd79e Mon Sep 17 00:00:00 2001 From: ptixed Date: Fri, 12 Apr 2024 15:49:46 +0200 Subject: [PATCH 03/11] fix an oppsie --- .gitignore | 6 +++--- Ptixed.Sql.Tests/AppSettings.cs | 11 +++++++++++ Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs | 2 +- Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj | 9 +++++++++ Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 Ptixed.Sql.Tests/AppSettings.cs diff --git a/.gitignore b/.gitignore index 5a63b26..7c01cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -/.vs/ +.vs/ +*.user -/packages/ bin/ obj/ -*.user +appsettings.json diff --git a/Ptixed.Sql.Tests/AppSettings.cs b/Ptixed.Sql.Tests/AppSettings.cs new file mode 100644 index 0000000..f7d871a --- /dev/null +++ b/Ptixed.Sql.Tests/AppSettings.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Configuration; + +namespace Ptixed.Sql.Tests +{ + internal static class AppSettings + { + public static readonly IConfigurationRoot Instance = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + } +} diff --git a/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs b/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs index 1489709..7751dcc 100644 --- a/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs +++ b/Ptixed.Sql.Tests/Postgres/DatabaseFixutre.cs @@ -6,7 +6,7 @@ namespace Ptixed.Sql.Tests.Postgres { public class DatabaseFixture : IDisposable { - private readonly ConnectionConfig _config = new ConnectionConfig("Host=cm-cloud.adrentech.com; Port=5444; Database=ptixed; Username=sa; Password=fy7B32dFgC4utAziA5zkLOitTWEAQlsHLi1ROgRMGII1;"); + private readonly ConnectionConfig _config = new ConnectionConfig(AppSettings.Instance["Postgres.ConnectionString"]); public DatabaseFixture() { diff --git a/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj b/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj index e71ef40..df8da39 100644 --- a/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj +++ b/Ptixed.Sql.Tests/Ptixed.Sql.Tests.csproj @@ -4,6 +4,15 @@ x64 + + + + + PreserveNewest + + + + diff --git a/Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs b/Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs index c32d4e0..163151e 100644 --- a/Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs +++ b/Ptixed.Sql.Tests/SqlServer/DatabaseFixture.cs @@ -6,7 +6,7 @@ namespace Ptixed.Sql.Tests.SqlServer { public class DatabaseFixture : IDisposable { - private readonly ConnectionConfig _config = new ConnectionConfig("Server=.,14444;initial catalog=Ptixed; User=sa; Password=123QWEasd;"); + private readonly ConnectionConfig _config = new ConnectionConfig(AppSettings.Instance["SqlServer.ConnectionString"]); public DatabaseFixture() { From 4883c9883be0a98cbd751d8421ff5ff538b68d99 Mon Sep 17 00:00:00 2001 From: ptixed Date: Fri, 17 May 2024 16:04:06 +0200 Subject: [PATCH 04/11] fix Type being incorrectly formatted into query --- Ptixed.Sql.Postgres/Query.cs | 5 ++++- Ptixed.Sql.SqlServer/Query.cs | 5 ++++- Ptixed.Sql/MappingConfig.cs | 5 ----- Ptixed.Sql/Query.cs | 3 --- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Ptixed.Sql.Postgres/Query.cs b/Ptixed.Sql.Postgres/Query.cs index 24d0031..3675d5c 100644 --- a/Ptixed.Sql.Postgres/Query.cs +++ b/Ptixed.Sql.Postgres/Query.cs @@ -17,8 +17,11 @@ protected override bool Format(object o, ref int index, MappingConfig mapping, L { switch (o) { + case Type t: + formants.Add($"\"{Table.Get(t).Name}\""); + return true; case Table tm: - formants.Add($"\"{mapping.FormatTableName(tm)}\""); + formants.Add($"\"{tm.Name}\""); return true; case PhysicalColumn pc: formants.Add($"\"{pc.Name}\""); diff --git a/Ptixed.Sql.SqlServer/Query.cs b/Ptixed.Sql.SqlServer/Query.cs index e34acce..fb35da0 100644 --- a/Ptixed.Sql.SqlServer/Query.cs +++ b/Ptixed.Sql.SqlServer/Query.cs @@ -17,8 +17,11 @@ protected override bool Format(object o, ref int index, MappingConfig mapping, L { switch (o) { + case Type t: + formants.Add(Table.Get(t).Name); + return true; case Table tm: - formants.Add(mapping.FormatTableName(tm)); + formants.Add(tm.Name); return true; case PhysicalColumn pc: formants.Add($"[{pc.Name}]"); diff --git a/Ptixed.Sql/MappingConfig.cs b/Ptixed.Sql/MappingConfig.cs index f33016c..ca16aa8 100644 --- a/Ptixed.Sql/MappingConfig.cs +++ b/Ptixed.Sql/MappingConfig.cs @@ -62,10 +62,5 @@ protected virtual Func FromDbImpl(Type type) return x => x; } - - public virtual string FormatTableName(Table table) - { - return table.Name; - } } } diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index 00f8c02..3f22583 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -94,9 +94,6 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) values.AddRange(vs); formants.Add(text); break; - case Type t: - formants.Add(mapping.FormatTableName(Table.Get(t))); - break; case int i: formants.Add(i.ToString()); break; From 9613d583b79be1976d4c0a6c1d5dac1d408e739a Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 10:14:01 +0200 Subject: [PATCH 05/11] BulkInsert --- Ptixed.Sql.Postgres/Database.cs | 5 +--- Ptixed.Sql.SqlServer/Database.cs | 36 ++++++++++++++++++++++-- Ptixed.Sql.SqlServer/IDatabase.cs | 4 ++- Ptixed.Sql.Tests/SqlServer/QueryTests.cs | 25 ++++++++++++++++ Ptixed.Sql/ColumnValue.cs | 6 ++++ Ptixed.Sql/Impl/Database.cs | 16 +++++------ 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/Ptixed.Sql.Postgres/Database.cs b/Ptixed.Sql.Postgres/Database.cs index 84a5477..6638bc5 100644 --- a/Ptixed.Sql.Postgres/Database.cs +++ b/Ptixed.Sql.Postgres/Database.cs @@ -1,15 +1,12 @@ using Npgsql; using Ptixed.Sql.Impl; -using System.Data.Common; namespace Ptixed.Sql.Postgres { - public class Database : Database, IDatabase + public class Database : Database, IDatabase { public Database(ConnectionConfig config) : base(config) { } - - protected override DbConnection CreateConnection(string connectionString) => new NpgsqlConnection(connectionString); } } diff --git a/Ptixed.Sql.SqlServer/Database.cs b/Ptixed.Sql.SqlServer/Database.cs index e3ab886..7d91960 100644 --- a/Ptixed.Sql.SqlServer/Database.cs +++ b/Ptixed.Sql.SqlServer/Database.cs @@ -1,15 +1,45 @@ using Ptixed.Sql.Impl; -using System.Data.Common; +using Ptixed.Sql.Meta; +using System.Collections.Generic; +using System.Data; using System.Data.SqlClient; +using System.Linq; namespace Ptixed.Sql.SqlServer { - public class Database : Database, IDatabase + public class Database : Database, IDatabase { public Database(ConnectionConfig config) : base(config) { } + + public void BulkInsert(IEnumerable entities) + { + var table = Table.Get(typeof(T)); + var columns = table.PhysicalColumns.Except(new[] { table.AutoIncrementColumn }).ToList(); + + var payload = new DataTable(); + + foreach (var column in columns) + payload.Columns.Add(column.Name); - protected override DbConnection CreateConnection(string connectionString) => new SqlConnection(connectionString); + foreach (var entity in entities) + { + var row = payload.NewRow(); + var values = table.ToQuery(columns, entity); + foreach (var (name, value) in values) + row[name] = value; + payload.Rows.Add(row); + } + + using (var bcp = new SqlBulkCopy(Connection.Value)) + { + foreach (var column in columns) + bcp.ColumnMappings.Add(column.Name, column.Name); + + bcp.DestinationTableName = table.Name; + bcp.WriteToServer(payload); + } + } } } diff --git a/Ptixed.Sql.SqlServer/IDatabase.cs b/Ptixed.Sql.SqlServer/IDatabase.cs index b5b789d..c64c4bb 100644 --- a/Ptixed.Sql.SqlServer/IDatabase.cs +++ b/Ptixed.Sql.SqlServer/IDatabase.cs @@ -1,8 +1,10 @@ -using System.Data.SqlClient; +using System.Collections.Generic; +using System.Data.SqlClient; namespace Ptixed.Sql.SqlServer { public interface IDatabase : IDatabase { + void BulkInsert(IEnumerable entries); } } diff --git a/Ptixed.Sql.Tests/SqlServer/QueryTests.cs b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs index 9484afa..2f0d537 100644 --- a/Ptixed.Sql.Tests/SqlServer/QueryTests.cs +++ b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs @@ -21,7 +21,10 @@ public QueryTests(DatabaseFixture dbfixture) public void Dispose() { using (var db = _db.OpenConnection()) + { db.NonQuery($"DELETE FROM Model"); + db.NonQuery($"DELETE FROM Model2"); + } } [Fact] @@ -338,5 +341,27 @@ public void TestUpsertQueries() Assert.True(byName.First().Id != inserted.Id); } } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(10)] + public void TestBulkInsert(int n) + { + using (var db = _db.OpenConnection()) + { + var data = new List(); + for (var i = 0; i < n; ++i) + data.Add(new Model2 + { + ModelId = i + }); + + db.BulkInsert(data); + + var inserted = db.Single($"SELECT COUNT(*) FROM Model2"); + Assert.Equal(n, inserted); + } + } } } diff --git a/Ptixed.Sql/ColumnValue.cs b/Ptixed.Sql/ColumnValue.cs index e1840b3..9d073f1 100644 --- a/Ptixed.Sql/ColumnValue.cs +++ b/Ptixed.Sql/ColumnValue.cs @@ -12,5 +12,11 @@ public ColumnValue(string name, object value) Name = name; Value = value == DBNull.Value ? null : value; } + + public void Deconstruct(out string name, out object value) + { + name = Name; + value = Value; + } } } diff --git a/Ptixed.Sql/Impl/Database.cs b/Ptixed.Sql/Impl/Database.cs index e5ce25f..7480c93 100755 --- a/Ptixed.Sql/Impl/Database.cs +++ b/Ptixed.Sql/Impl/Database.cs @@ -10,7 +10,8 @@ namespace Ptixed.Sql.Impl /// /// This class is not thread safe /// - public abstract class Database : IDatabase + public abstract class Database : IDatabase + where TConnection : DbConnection, new() where TParameter : DbParameter, new() where TCommand : DbCommand, new() { @@ -18,7 +19,7 @@ public abstract class Database : IDatabase private IDisposable _result; public readonly ConnectionConfig Config; - public Lazy Connection; + public Lazy Connection; public readonly DiagnosticsClass Diagnostics = new DiagnosticsClass(); @@ -66,16 +67,15 @@ public void Dispose() public void Reset() { Dispose(); - Connection = new Lazy(() => + Connection = new Lazy(() => { - var sql = CreateConnection(Config.ConnectionString); + var sql = new TConnection(); + sql.ConnectionString = Config.ConnectionString; sql.Open(); return sql; }, LazyThreadSafetyMode.None); } - protected abstract DbConnection CreateConnection(string connectionString); - public int NonQuery(params Query[] query) { if (query.Length == 0) @@ -103,11 +103,11 @@ public IDatabaseTransaction OpenTransaction(IsolationLevel isolation) private class DatabaseTransaction : IDatabaseTransaction { - private readonly Database _db; + private readonly Database _db; private bool _commited; private bool _rolledback; - public DatabaseTransaction(Database db, IsolationLevel isolation) + public DatabaseTransaction(Database db, IsolationLevel isolation) { _db = db; _db._transaction = db.Connection.Value.BeginTransaction(isolation); From 2146e3a4a0542262f7e7b76b1279f532459dd9fa Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 10:28:57 +0200 Subject: [PATCH 06/11] slight query refactor --- Ptixed.Sql.Postgres/Query.cs | 23 ++--------------------- Ptixed.Sql.SqlServer/Query.cs | 25 +++---------------------- Ptixed.Sql/Query.cs | 19 ++++++++++++++----- 3 files changed, 19 insertions(+), 48 deletions(-) diff --git a/Ptixed.Sql.Postgres/Query.cs b/Ptixed.Sql.Postgres/Query.cs index 3675d5c..aaca361 100644 --- a/Ptixed.Sql.Postgres/Query.cs +++ b/Ptixed.Sql.Postgres/Query.cs @@ -1,7 +1,5 @@ using Npgsql; -using Ptixed.Sql.Meta; using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Ptixed.Sql.Postgres @@ -13,24 +11,7 @@ public class Query : Query public Query() { } public Query(FormattableString query) : base(query) { } - protected override bool Format(object o, ref int index, MappingConfig mapping, List formants, List values) - { - switch (o) - { - case Type t: - formants.Add($"\"{Table.Get(t).Name}\""); - return true; - case Table tm: - formants.Add($"\"{tm.Name}\""); - return true; - case PhysicalColumn pc: - formants.Add($"\"{pc.Name}\""); - return true; - case ColumnValue cv: - formants.Add($"\"{cv.Name}\""); - return true; - } - return false; - } + protected override string FormatTableName(string name) => $"\"{name}\""; + protected override string FormatColumnName(string name) => $"\"{name}\""; } } diff --git a/Ptixed.Sql.SqlServer/Query.cs b/Ptixed.Sql.SqlServer/Query.cs index fb35da0..7a03543 100644 --- a/Ptixed.Sql.SqlServer/Query.cs +++ b/Ptixed.Sql.SqlServer/Query.cs @@ -1,6 +1,4 @@ -using Ptixed.Sql.Meta; -using System; -using System.Collections.Generic; +using System; using System.Data.SqlClient; using System.Runtime.CompilerServices; @@ -13,24 +11,7 @@ public class Query : Query public Query() { } public Query(FormattableString query) : base(query) { } - protected override bool Format(object o, ref int index, MappingConfig mapping, List formants, List values) - { - switch (o) - { - case Type t: - formants.Add(Table.Get(t).Name); - return true; - case Table tm: - formants.Add(tm.Name); - return true; - case PhysicalColumn pc: - formants.Add($"[{pc.Name}]"); - return true; - case ColumnValue cv: - formants.Add($"[{cv.Name}]"); - return true; - } - return false; - } + protected override string FormatTableName(string name) => name; + protected override string FormatColumnName(string name) => $"[{name}]"; } } diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index 3f22583..20cd550 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -71,7 +71,8 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) return command; } - protected abstract bool Format(object o, ref int index, MappingConfig mapping, List formants, List values); + protected abstract string FormatTableName(string name); + protected abstract string FormatColumnName(string name); private (string, List) ToSql(ref int index, MappingConfig mapping) { @@ -81,9 +82,6 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) { var formants = new List(); foreach (var argument in part.GetArguments()) - { - if (Format(argument, ref index, mapping, formants, values)) - continue; switch (argument) { case null: @@ -101,6 +99,18 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) values.Add(s); formants.Add("@" + index++.ToString()); break; + case Type type: + formants.Add(FormatTableName(Table.Get(type).Name)); + break; + case Table table: + formants.Add(FormatTableName(table.Name)); + break; + case PhysicalColumn pc: + formants.Add(FormatColumnName(pc.Name)); + break; + case ColumnValue cv: + formants.Add(FormatColumnName(cv.Name)); + break; case IEnumerable ie: var sb1 = new StringBuilder("("); using (var enumerator = ie.Cast().GetEnumerator()) @@ -127,7 +137,6 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) formants.Add("@" + index++.ToString()); break; } - } sb.Append(string.Format(part.Format, formants.ToArray())); } return (sb.ToString(), values); From e638dba4626ed9b092f63937e28a53343c6b909d Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 10:46:15 +0200 Subject: [PATCH 07/11] allow FormattableStrings within FormattableStrings for Query --- Ptixed.Sql.Tests/SqlServer/QueryTests.cs | 24 +++++ Ptixed.Sql/Query.cs | 116 +++++++++++++---------- 2 files changed, 91 insertions(+), 49 deletions(-) diff --git a/Ptixed.Sql.Tests/SqlServer/QueryTests.cs b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs index 2f0d537..11666de 100644 --- a/Ptixed.Sql.Tests/SqlServer/QueryTests.cs +++ b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs @@ -342,6 +342,30 @@ public void TestUpsertQueries() } } + [Fact] + public void FormattableStringWithinFormattableString() + { + using (var db = _db.OpenConnection()) + { + var model = new ModelUpsert + { + Age = 1, + Name = "John Doe" + }; + + var inserted = db.Insert(model); + Assert.True(inserted.Id > 0); + + FormattableString q1 = $"{model.Id}"; + FormattableString q2 = $"Id = {q1}"; + Query q3 = new Query($"WHERE {q2} or Id = {q1}"); + Query q4 = new Query($"SELECT * FROM ModelUpsert {q3}"); + + var result = db.Query(q4).Single(); + Assert.Equal(result.Id, model.Id); + } + } + [Theory] [InlineData(0)] [InlineData(1)] diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index 20cd550..c6a8013 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; +using System.Reflection; using System.Text; namespace Ptixed.Sql @@ -80,66 +81,83 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) var values = new List(); foreach (var part in _parts) { - var formants = new List(); - foreach (var argument in part.GetArguments()) - switch (argument) - { - case null: - formants.Add("NULL"); + var (text, vs) = ToSqlPart(ref index, part, mapping); + sb.Append(text); + values.AddRange(vs); + } + return (sb.ToString(), values); + } + + private (string, List) ToSqlPart(ref int index, FormattableString part, MappingConfig mapping) + { + var values = new List(); + var formants = new List(); + foreach (var argument in part.GetArguments()) + switch (argument) + { + case null: + formants.Add("NULL"); + break; + case FormattableString fs: + { + var (text, vs) = ToSqlPart(ref index, fs, mapping); + values.AddRange(vs); + formants.Add(text); break; - case Query q: + } + case Query q: + { var (text, vs) = q.ToSql(ref index, mapping); values.AddRange(vs); formants.Add(text); break; - case int i: - formants.Add(i.ToString()); - break; - case string s: - values.Add(s); - formants.Add("@" + index++.ToString()); - break; - case Type type: - formants.Add(FormatTableName(Table.Get(type).Name)); - break; - case Table table: - formants.Add(FormatTableName(table.Name)); - break; - case PhysicalColumn pc: - formants.Add(FormatColumnName(pc.Name)); - break; - case ColumnValue cv: - formants.Add(FormatColumnName(cv.Name)); - break; - case IEnumerable ie: - var sb1 = new StringBuilder("("); - using (var enumerator = ie.Cast().GetEnumerator()) + } + case int i: + formants.Add(i.ToString()); + break; + case string s: + values.Add(s); + formants.Add("@" + index++.ToString()); + break; + case Type type: + formants.Add(FormatTableName(Table.Get(type).Name)); + break; + case Table table: + formants.Add(FormatTableName(table.Name)); + break; + case PhysicalColumn pc: + formants.Add(FormatColumnName(pc.Name)); + break; + case ColumnValue cv: + formants.Add(FormatColumnName(cv.Name)); + break; + case IEnumerable ie: + var sb1 = new StringBuilder("("); + using (var enumerator = ie.Cast().GetEnumerator()) + { + if (!enumerator.MoveNext()) + sb1.Append("SELECT TOP 0 0"); + else { - if (!enumerator.MoveNext()) - sb1.Append("SELECT TOP 0 0"); - else + values.Add(enumerator.Current); + sb1.Append("@" + index++.ToString()); + while (enumerator.MoveNext()) { + sb1.Append(", "); values.Add(enumerator.Current); sb1.Append("@" + index++.ToString()); - while (enumerator.MoveNext()) - { - sb1.Append(", "); - values.Add(enumerator.Current); - sb1.Append("@" + index++.ToString()); - } } } - sb1.Append(")"); - formants.Add(sb1.ToString()); - break; - default: - values.Add(argument); - formants.Add("@" + index++.ToString()); - break; - } - sb.Append(string.Format(part.Format, formants.ToArray())); - } - return (sb.ToString(), values); + } + sb1.Append(")"); + formants.Add(sb1.ToString()); + break; + default: + values.Add(argument); + formants.Add("@" + index++.ToString()); + break; + } + return (string.Format(part.Format, formants.ToArray()), values); } } } From 8f6749e305cb8633d5cc7235abd8508beebd4fef Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 10:53:03 +0200 Subject: [PATCH 08/11] simplify query some more --- Ptixed.Sql/Query.cs | 53 ++++++++++++++------------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index c6a8013..56eb91c 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -44,18 +44,12 @@ public Query Append(FormattableString separator, IEnumerable ToString(new MappingConfig()); - - public string ToString(MappingConfig mc) - { - var index = 0; - return ToSql(ref index, mc).Item1; - } + public override string ToString() => ToSql(new List(), new MappingConfig()); public DbCommand ToSql(DbCommand command, MappingConfig mapping) { - var index = 0; - var (text, values) = ToSql(ref index, mapping); + var values = new List(); + var text = ToSql(values, mapping); command.CommandText = text; foreach (var (value, i) in values.Select((x, i) => (x, i))) @@ -75,22 +69,16 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) protected abstract string FormatTableName(string name); protected abstract string FormatColumnName(string name); - private (string, List) ToSql(ref int index, MappingConfig mapping) + private string ToSql(List values, MappingConfig mapping) { var sb = new StringBuilder(); - var values = new List(); foreach (var part in _parts) - { - var (text, vs) = ToSqlPart(ref index, part, mapping); - sb.Append(text); - values.AddRange(vs); - } - return (sb.ToString(), values); + sb.Append(ToSqlPart(values, part, mapping)); + return sb.ToString(); } - private (string, List) ToSqlPart(ref int index, FormattableString part, MappingConfig mapping) + private string ToSqlPart(List values, FormattableString part, MappingConfig mapping) { - var values = new List(); var formants = new List(); foreach (var argument in part.GetArguments()) switch (argument) @@ -99,25 +87,17 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) formants.Add("NULL"); break; case FormattableString fs: - { - var (text, vs) = ToSqlPart(ref index, fs, mapping); - values.AddRange(vs); - formants.Add(text); - break; - } + formants.Add(ToSqlPart(values, fs, mapping)); + break; case Query q: - { - var (text, vs) = q.ToSql(ref index, mapping); - values.AddRange(vs); - formants.Add(text); - break; - } + formants.Add(q.ToSql(values, mapping)); + break; case int i: formants.Add(i.ToString()); break; case string s: + formants.Add($"@{values.Count}"); values.Add(s); - formants.Add("@" + index++.ToString()); break; case Type type: formants.Add(FormatTableName(Table.Get(type).Name)); @@ -139,13 +119,12 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) sb1.Append("SELECT TOP 0 0"); else { + formants.Add($"@{values.Count}"); values.Add(enumerator.Current); - sb1.Append("@" + index++.ToString()); while (enumerator.MoveNext()) { - sb1.Append(", "); + sb1.Append($", @{values.Count}"); values.Add(enumerator.Current); - sb1.Append("@" + index++.ToString()); } } } @@ -153,11 +132,11 @@ public DbCommand ToSql(DbCommand command, MappingConfig mapping) formants.Add(sb1.ToString()); break; default: + formants.Add($"@{values.Count}"); values.Add(argument); - formants.Add("@" + index++.ToString()); break; } - return (string.Format(part.Format, formants.ToArray()), values); + return string.Format(part.Format, formants.ToArray()); } } } From 7ad80c7ac290455e3dba9bcf7495b3b9e195d637 Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 11:00:28 +0200 Subject: [PATCH 09/11] keep old package name --- Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj b/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj index b7d1de9..6e91bb6 100644 --- a/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj +++ b/Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj @@ -2,7 +2,7 @@ netstandard2.0 False - Ptixed.Sql.SqlServer + Ptixed.Sql 0.9.0 Library for interacting with Microsoft SQL databases ptixed From 71717b6b417b3b193615ca59159ae35f938428bb Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 11:41:02 +0200 Subject: [PATCH 10/11] publish fixes --- .github/workflows/publish.yml | 5 ++--- Ptixed.Sql/Ptixed.Sql.csproj | 14 -------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4fbe859..ebd66c2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,12 +13,11 @@ jobs: id: publish_nuget uses: alirezanet/publish-nuget@v3.0.4 with: - PROJECT_FILE_PATH: Ptixed.Sql/Ptixed.Sql.csproj + PROJECT_FILE_PATH: Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj PACKAGE_NAME: Ptixed.Sql - VERSION_FILE_PATH: Ptixed.Sql/Ptixed.Sql.csproj + VERSION_FILE_PATH: Ptixed.Sql.SqlServer/Ptixed.Sql.SqlServer.csproj VERSION_REGEX: ^\s*(.*)<\/Version>\s*$ - # VERSION_STATIC: 1.0.0 TAG_COMMIT: true TAG_FORMAT: v* diff --git a/Ptixed.Sql/Ptixed.Sql.csproj b/Ptixed.Sql/Ptixed.Sql.csproj index b95bbc9..b5aeca6 100644 --- a/Ptixed.Sql/Ptixed.Sql.csproj +++ b/Ptixed.Sql/Ptixed.Sql.csproj @@ -1,21 +1,7 @@  netstandard2.0 - False - Ptixed.Sql - 0.8.0 - Library for interacting with Microsoft SQL databases - ptixed - https://github.com/ptixed/Ptixed.Sql - ptixed sql tsql database db - 0.8.0 - 0.8.0 - - https://raw.githubusercontent.com/ptixed/Ptixed.Sql/master/logo-nuget.png - - - From 3ba96157d4b26c7ef6cb9ef89d5bd0d4e4f2da02 Mon Sep 17 00:00:00 2001 From: ptixed Date: Thu, 4 Jul 2024 12:02:59 +0200 Subject: [PATCH 11/11] fix IEnumerable not applying transformations to elements --- Ptixed.Sql.Tests/SqlServer/QueryTests.cs | 16 +++- Ptixed.Sql/Query.cs | 114 +++++++++++------------ 2 files changed, 70 insertions(+), 60 deletions(-) diff --git a/Ptixed.Sql.Tests/SqlServer/QueryTests.cs b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs index 11666de..e7f69ef 100644 --- a/Ptixed.Sql.Tests/SqlServer/QueryTests.cs +++ b/Ptixed.Sql.Tests/SqlServer/QueryTests.cs @@ -265,7 +265,6 @@ public void TestAffectedRows() } } - [Fact] public void TestListInsert() { @@ -343,7 +342,7 @@ public void TestUpsertQueries() } [Fact] - public void FormattableStringWithinFormattableString() + public void TestFormattableStringWithinFormattableString() { using (var db = _db.OpenConnection()) { @@ -366,6 +365,19 @@ public void FormattableStringWithinFormattableString() } } + [Fact] + public void TestIEnumerable() + { + using (var db = _db.OpenConnection()) + { + var s = "2"; + var q1 = new Query($"1"); + var q2 = new Query($"{s}"); + var qs = new[] { q1, q2 }; + var result = db.ToList($"SELECT * FROM ModelUpsert WHERE Id IN {qs}"); + } + } + [Theory] [InlineData(0)] [InlineData(1)] diff --git a/Ptixed.Sql/Query.cs b/Ptixed.Sql/Query.cs index 56eb91c..934e384 100644 --- a/Ptixed.Sql/Query.cs +++ b/Ptixed.Sql/Query.cs @@ -73,70 +73,68 @@ private string ToSql(List values, MappingConfig mapping) { var sb = new StringBuilder(); foreach (var part in _parts) - sb.Append(ToSqlPart(values, part, mapping)); + sb.Append(ToSqlPart(values, mapping, part)); return sb.ToString(); } - private string ToSqlPart(List values, FormattableString part, MappingConfig mapping) + private string ToSqlPart(List values, MappingConfig mapping, FormattableString part) { - var formants = new List(); + var formants = new List(); foreach (var argument in part.GetArguments()) - switch (argument) - { - case null: - formants.Add("NULL"); - break; - case FormattableString fs: - formants.Add(ToSqlPart(values, fs, mapping)); - break; - case Query q: - formants.Add(q.ToSql(values, mapping)); - break; - case int i: - formants.Add(i.ToString()); - break; - case string s: - formants.Add($"@{values.Count}"); - values.Add(s); - break; - case Type type: - formants.Add(FormatTableName(Table.Get(type).Name)); - break; - case Table table: - formants.Add(FormatTableName(table.Name)); - break; - case PhysicalColumn pc: - formants.Add(FormatColumnName(pc.Name)); - break; - case ColumnValue cv: - formants.Add(FormatColumnName(cv.Name)); - break; - case IEnumerable ie: - var sb1 = new StringBuilder("("); - using (var enumerator = ie.Cast().GetEnumerator()) - { - if (!enumerator.MoveNext()) - sb1.Append("SELECT TOP 0 0"); - else - { - formants.Add($"@{values.Count}"); - values.Add(enumerator.Current); - while (enumerator.MoveNext()) - { - sb1.Append($", @{values.Count}"); - values.Add(enumerator.Current); - } - } - } - sb1.Append(")"); - formants.Add(sb1.ToString()); - break; - default: - formants.Add($"@{values.Count}"); - values.Add(argument); - break; - } + ToSqlArgument(values, mapping, formants, argument); return string.Format(part.Format, formants.ToArray()); } + + private void ToSqlArgument(List values, MappingConfig mapping, List formants, object argument) + { + switch (argument) + { + case null: + formants.Add("NULL"); + break; + case FormattableString fs: + formants.Add(ToSqlPart(values, mapping, fs)); + break; + case Query q: + formants.Add(q.ToSql(values, mapping)); + break; + case int i: + formants.Add(i.ToString()); + break; + case string s: + formants.Add($"@{values.Count}"); + values.Add(s); + break; + case Type type: + formants.Add(FormatTableName(Table.Get(type).Name)); + break; + case Table table: + formants.Add(FormatTableName(table.Name)); + break; + case PhysicalColumn pc: + formants.Add(FormatColumnName(pc.Name)); + break; + case ColumnValue cv: + formants.Add(FormatColumnName(cv.Name)); + break; + case IEnumerable ie: + var formants1 = new List(); + var enumerator = ie.GetEnumerator(); + if (!enumerator.MoveNext()) + formants1.Add("SELECT TOP 0 0"); + else + { + ToSqlArgument(values, mapping, formants1, enumerator.Current); + while (enumerator.MoveNext()) + ToSqlArgument(values, mapping, formants1, enumerator.Current); + } + formants.Add($"({string.Join(", ", formants1)})"); + break; + default: + formants.Add($"@{values.Count}"); + values.Add(argument); + break; + } + } } }