From 42b15ef29261ed447bb1a186dbe6327df311fa82 Mon Sep 17 00:00:00 2001 From: Yurii Mazur Date: Fri, 22 Apr 2022 12:36:11 +0300 Subject: [PATCH 1/2] Oracle support --- YesSql.sln | 27 +- pack.ps1 | 10 + src/YesSql.Abstractions/Document.cs | 23 +- src/YesSql.Abstractions/ISession.cs | 2 +- src/YesSql.Abstractions/ISqlDialect.cs | 17 + .../Indexes/DescribeContext.cs | 4 +- .../Indexes/DescribeFor.cs | 13 +- .../Storage/IIdentityEntity.cs | 6 +- .../Commands/CreateDocumentCommand.cs | 18 +- .../Commands/CreateIndexCommand.cs | 13 +- .../Commands/DeleteDocumentCommand.cs | 7 +- .../Commands/DeleteMapIndexCommand.cs | 4 +- .../Commands/DeleteReduceIndexCommand.cs | 8 +- src/YesSql.Core/Commands/DocumentCommand.cs | 6 +- src/YesSql.Core/Commands/IndexCommand.cs | 25 +- .../Commands/UpdateDocumentCommand.cs | 29 +- .../Commands/UpdateIndexCommand.cs | 23 +- src/YesSql.Core/Data/WorkerQueryKey.cs | 26 +- src/YesSql.Core/Provider/BaseDialect.cs | 51 +- .../Serialization/JsonContentSerializer.cs | 2 +- .../Services/DbBlockIdGenerator.cs | 37 +- .../Services/DefaultIdGenerator.cs | 2 +- src/YesSql.Core/Services/DefaultQuery.cs | 66 +- src/YesSql.Core/Session.cs | 9 +- src/YesSql.Core/Sql/BaseComandInterpreter.cs | 2 +- .../Sql/Schema/AddColumnCommand.cs | 2 +- .../Sql/Schema/SqlStatementCommand.cs | 4 +- src/YesSql.Core/Sql/Schema/TableCommand.cs | 2 +- src/YesSql.Core/Sql/SchemaBuilder.cs | 2 +- src/YesSql.Core/Sql/SqlBuilder.cs | 47 +- src/YesSql.Core/StoreFactory.cs | 8 +- .../OracleCommandInterpreter.cs | 38 + .../OracleDbProviderOptionsExtensions.cs | 40 + src/YesSql.Provider.Oracle/OracleDialect.cs | 449 ++++++++++ src/YesSql.Provider.Oracle/TableColumnInfo.cs | 38 + .../YesSql.Provider.Oracle.csproj | 15 + .../SqlServerDialect.cs | 6 +- test/YesSql.Tests/CoreTests.cs | 787 ++++++++++-------- .../Indexes/CustomerByCreationDate.cs | 31 + test/YesSql.Tests/Indexes/CustomerById.cs | 28 + test/YesSql.Tests/Indexes/FileInfo.cs | 30 + test/YesSql.Tests/Models/Customer.cs | 14 + test/YesSql.Tests/OracleTests.cs | 549 ++++++++++++ test/YesSql.Tests/SqlServer2019Tests.cs | 2 +- test/YesSql.Tests/SqliteTests.cs | 13 - test/YesSql.Tests/YesSql.Tests.csproj | 3 +- 46 files changed, 2005 insertions(+), 533 deletions(-) create mode 100644 pack.ps1 create mode 100644 src/YesSql.Provider.Oracle/OracleCommandInterpreter.cs create mode 100644 src/YesSql.Provider.Oracle/OracleDbProviderOptionsExtensions.cs create mode 100644 src/YesSql.Provider.Oracle/OracleDialect.cs create mode 100644 src/YesSql.Provider.Oracle/TableColumnInfo.cs create mode 100644 src/YesSql.Provider.Oracle/YesSql.Provider.Oracle.csproj create mode 100644 test/YesSql.Tests/Indexes/CustomerByCreationDate.cs create mode 100644 test/YesSql.Tests/Indexes/CustomerById.cs create mode 100644 test/YesSql.Tests/Indexes/FileInfo.cs create mode 100644 test/YesSql.Tests/Models/Customer.cs create mode 100644 test/YesSql.Tests/OracleTests.cs diff --git a/YesSql.sln b/YesSql.sln index d39f435b..a6ab00ee 100644 --- a/YesSql.sln +++ b/YesSql.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29123.88 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32414.318 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0882456B-4A70-4895-A3AE-39B9D30A1B31}" EndProject @@ -45,9 +45,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesSql.Provider.PostgreSql" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesSql", "src\YesSql\YesSql.csproj", "{6BAC7887-02FB-4B6F-BB5C-D6DEF9646CC6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YesSql.Filters.Abstractions", "src\YesSql.Filters.Abstractions\YesSql.Filters.Abstractions.csproj", "{348B307C-66F0-4DBE-BA96-5D9DFAB5588E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesSql.Filters.Abstractions", "src\YesSql.Filters.Abstractions\YesSql.Filters.Abstractions.csproj", "{348B307C-66F0-4DBE-BA96-5D9DFAB5588E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YesSql.Filters.Query", "src\YesSql.Filters.Query\YesSql.Filters.Query.csproj", "{86C3967F-B817-4119-B354-B6EB1AC1F237}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesSql.Filters.Query", "src\YesSql.Filters.Query\YesSql.Filters.Query.csproj", "{86C3967F-B817-4119-B354-B6EB1AC1F237}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesSql.Provider.Oracle", "src\YesSql.Provider.Oracle\YesSql.Provider.Oracle.csproj", "{97F56727-60FC-406D-9EF8-3456A78CFB9D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -301,6 +303,22 @@ Global {86C3967F-B817-4119-B354-B6EB1AC1F237}.Release|x64.Build.0 = Release|Any CPU {86C3967F-B817-4119-B354-B6EB1AC1F237}.Release|x86.ActiveCfg = Release|Any CPU {86C3967F-B817-4119-B354-B6EB1AC1F237}.Release|x86.Build.0 = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|x64.ActiveCfg = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|x64.Build.0 = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|x86.ActiveCfg = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Debug|x86.Build.0 = Debug|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|Any CPU.Build.0 = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|x64.ActiveCfg = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|x64.Build.0 = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|x86.ActiveCfg = Release|Any CPU + {97F56727-60FC-406D-9EF8-3456A78CFB9D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -321,6 +339,7 @@ Global {6BAC7887-02FB-4B6F-BB5C-D6DEF9646CC6} = {0C294EC4-E6EF-4839-AD34-335C1A5112F9} {348B307C-66F0-4DBE-BA96-5D9DFAB5588E} = {0C294EC4-E6EF-4839-AD34-335C1A5112F9} {86C3967F-B817-4119-B354-B6EB1AC1F237} = {0C294EC4-E6EF-4839-AD34-335C1A5112F9} + {97F56727-60FC-406D-9EF8-3456A78CFB9D} = {0C294EC4-E6EF-4839-AD34-335C1A5112F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A6AB2945-2138-42B9-8F82-FCCF4DFC8F55} diff --git a/pack.ps1 b/pack.ps1 new file mode 100644 index 00000000..1243ade7 --- /dev/null +++ b/pack.ps1 @@ -0,0 +1,10 @@ +param ( + [string]$solutionPath = ".\YesSql.sln", + [string]$destinationLocalNuget = "..\localNugets" +) + +Remove-Item .\nupkgs -Recurse -ErrorAction Ignore +dotnet pack $solutionPath -c release -o $pwd\nupkgs --nologo --version-suffix "tpc-portal-ymaz1" -v q +Remove-Item $destinationLocalNuget -Recurse -ErrorAction Ignore +nuget init .\nupkgs $destinationLocalNuget + diff --git a/src/YesSql.Abstractions/Document.cs b/src/YesSql.Abstractions/Document.cs index 37244650..1f4aa018 100644 --- a/src/YesSql.Abstractions/Document.cs +++ b/src/YesSql.Abstractions/Document.cs @@ -1,9 +1,11 @@ +using System; + namespace YesSql { /// /// The class stored in the Document table of a collection. /// - public class Document + public class Document : IEquatable { /// /// The unique identifier of the document in the database. @@ -27,5 +29,24 @@ public class Document /// This property is used to track updates, and optionally detect concurrency violations. /// public long Version { get; set; } + public bool Equals(Document other) + { + if (other == null) + { + return false; + } + return Id == other.Id && Type == other.Type && Content == other.Content && Version == other.Version; + } + + public override int GetHashCode() + { + var hashCode = 13; + hashCode = (hashCode * 397) ^ Id; + hashCode = (hashCode * 397) ^ (!string.IsNullOrEmpty(Type) ? Type.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (!string.IsNullOrEmpty(Content) ? Content.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (int)Version; + + return hashCode; + } } } diff --git a/src/YesSql.Abstractions/ISession.cs b/src/YesSql.Abstractions/ISession.cs index 73f13132..4c19f4f8 100644 --- a/src/YesSql.Abstractions/ISession.cs +++ b/src/YesSql.Abstractions/ISession.cs @@ -135,7 +135,7 @@ public static class SessionExtensions /// Loads an object by its id. /// /// The object or null. - public async static Task GetAsync(this ISession session, int id, string collection = null) where T : class + public static async Task GetAsync(this ISession session, int id, string collection = null) where T : class { return (await session.GetAsync(new[] { id }, collection)).FirstOrDefault(); } diff --git a/src/YesSql.Abstractions/ISqlDialect.cs b/src/YesSql.Abstractions/ISqlDialect.cs index 5737bb53..8349dcc4 100644 --- a/src/YesSql.Abstractions/ISqlDialect.cs +++ b/src/YesSql.Abstractions/ISqlDialect.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Text; +using System.Threading.Tasks; +using YesSql.Indexes; namespace YesSql { @@ -209,5 +212,19 @@ public interface ISqlDialect /// Create a instance. /// ISqlBuilder CreateBuilder(string tablePrefix); + + string QuoteForParameter(string parameterName); + string ParameterNamePrefix { get; } + string StatementEnd { get; } + string BatchStatementEnd { get; } + string NullString { get; } + bool IsSpecialDistinctRequired { get; } + string GetParameterName(string parameterName); + IDbCommand ConfigureCommand(IDbCommand command); + Task InsertReturningReduceIndexAsync(DbConnection connection, IIndex index, string insertSql, DbTransaction transaction); + void PrepareReturningMapIndexCommand(DbCommand command); + object GetDynamicParameters(DbConnection connection, object parameters, string tableName); + object GetSafeIndexParameters(IIndex index); + string AliasKeyword { get; } } } diff --git a/src/YesSql.Abstractions/Indexes/DescribeContext.cs b/src/YesSql.Abstractions/Indexes/DescribeContext.cs index 22a3bab9..58c0f979 100644 --- a/src/YesSql.Abstractions/Indexes/DescribeContext.cs +++ b/src/YesSql.Abstractions/Indexes/DescribeContext.cs @@ -32,9 +32,7 @@ public IMapFor For() where TIndex : IIndex public IMapFor For() where TIndex : IIndex { - List descriptors; - - if (!_describes.TryGetValue(typeof(T), out descriptors)) + if (!_describes.TryGetValue(typeof(T), out var descriptors)) { descriptors = _describes[typeof(T)] = new List(); } diff --git a/src/YesSql.Abstractions/Indexes/DescribeFor.cs b/src/YesSql.Abstractions/Indexes/DescribeFor.cs index 60f556b6..58806922 100644 --- a/src/YesSql.Abstractions/Indexes/DescribeFor.cs +++ b/src/YesSql.Abstractions/Indexes/DescribeFor.cs @@ -50,7 +50,7 @@ public class IndexDescriptor : IDescribeFor, IMapFor private Func _filter; public PropertyInfo GroupProperty { get; set; } - public Type IndexType { get { return typeof(TIndex); } } + public Type IndexType => typeof(TIndex); public Func Filter => _filter; @@ -62,13 +62,13 @@ public IGroupFor Map(Func> map) public IMapFor When(Func predicate) { - _filter = x => predicate((T) x); + _filter = x => predicate((T)x); return this; } public IGroupFor Map(Func map) { - _map = x => Task.FromResult((IEnumerable) new[] { map(x) }); + _map = x => Task.FromResult((IEnumerable)new[] { map(x) }); return this; } @@ -166,10 +166,7 @@ public GroupedEnumerable(object key, IEnumerable enumerable) _enumerable = enumerable; } - public TKey Key - { - get { return (TKey)_key; } - } + public TKey Key => (TKey)_key; public IEnumerator GetEnumerator() { @@ -181,4 +178,4 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() return GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/src/YesSql.Abstractions/Storage/IIdentityEntity.cs b/src/YesSql.Abstractions/Storage/IIdentityEntity.cs index dc15b8c3..c4465eac 100644 --- a/src/YesSql.Abstractions/Storage/IIdentityEntity.cs +++ b/src/YesSql.Abstractions/Storage/IIdentityEntity.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; namespace YesSql.Storage { diff --git a/src/YesSql.Core/Commands/CreateDocumentCommand.cs b/src/YesSql.Core/Commands/CreateDocumentCommand.cs index 0113e278..234924c1 100644 --- a/src/YesSql.Core/Commands/CreateDocumentCommand.cs +++ b/src/YesSql.Core/Commands/CreateDocumentCommand.cs @@ -1,9 +1,9 @@ -using Dapper; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data.Common; using System.Threading.Tasks; +using Dapper; +using Microsoft.Extensions.Logging; namespace YesSql.Commands { @@ -24,20 +24,26 @@ public override Task ExecuteAsync(DbConnection connection, DbTransaction transac { var documentTable = _tableNameConvention.GetDocumentTable(Collection); - var insertCmd = $"insert into {dialect.QuoteForTableName(_tablePrefix + documentTable)} ({dialect.QuoteForColumnName("Id")}, {dialect.QuoteForColumnName("Type")}, {dialect.QuoteForColumnName("Content")}, {dialect.QuoteForColumnName("Version")}) values (@Id, @Type, @Content, @Version);"; + var tableName = _tablePrefix + documentTable; + var insertCmd = "insert into " + dialect.QuoteForTableName(tableName) + " (" + dialect.QuoteForColumnName("Id") + ", " + dialect.QuoteForColumnName("Type") + ", " + dialect.QuoteForColumnName("Content") + ", " + dialect.QuoteForColumnName("Version") + ") " + + "values (" + dialect.QuoteForParameter("Id") + ", " + dialect.QuoteForParameter("Type") + ", " + dialect.QuoteForParameter("Content") + ", " + dialect.QuoteForParameter("Version") + ")" + dialect.StatementEnd; if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace(insertCmd); } - return connection.ExecuteAsync(insertCmd, Document, transaction); + var parameters = dialect.GetDynamicParameters(connection, Document, tableName); + return connection.ExecuteScalarAsync(insertCmd, parameters, transaction); } public override bool AddToBatch(ISqlDialect dialect, List queries, DbCommand batchCommand, List> actions, int index) { var documentTable = _tableNameConvention.GetDocumentTable(Collection); - var insertCmd = $"insert into {dialect.QuoteForTableName(_tablePrefix + documentTable)} ({dialect.QuoteForColumnName("Id")}, {dialect.QuoteForColumnName("Type")}, {dialect.QuoteForColumnName("Content")}, {dialect.QuoteForColumnName("Version")}) values (@Id_{index}, @Type_{index}, @Content_{index}, @Version_{index});"; + var tableName = _tablePrefix + documentTable; + var insertCmd = $"insert into {dialect.QuoteForTableName(tableName)} ({dialect.QuoteForColumnName("Id")}, " + + $"{dialect.QuoteForColumnName("Type")}, {dialect.QuoteForColumnName("Content")}, {dialect.QuoteForColumnName("Version")}) " + + $"values ({dialect.QuoteForParameter("Id")}_{index}, {dialect.QuoteForParameter("Type")}_{index}, {dialect.QuoteForParameter("Content")}_{index}, {dialect.QuoteForParameter("Version")}_{index})" + dialect.BatchStatementEnd; queries.Add(insertCmd); @@ -45,7 +51,7 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom .AddParameter("Id_" + index, Document.Id) .AddParameter("Type_" + index, Document.Type) .AddParameter("Content_" + index, Document.Content) - .AddParameter("Version_" + index, Document.Version); + .AddParameter(dialect.GetParameterName("Version") + "_" + index, Document.Version); return true; } diff --git a/src/YesSql.Core/Commands/CreateIndexCommand.cs b/src/YesSql.Core/Commands/CreateIndexCommand.cs index a704667d..f427a381 100644 --- a/src/YesSql.Core/Commands/CreateIndexCommand.cs +++ b/src/YesSql.Core/Commands/CreateIndexCommand.cs @@ -41,19 +41,18 @@ public override async Task ExecuteAsync(DbConnection connection, DbTransaction t { var command = connection.CreateCommand(); command.Transaction = transaction; - command.CommandText = sql; + command.CommandText = sql; //"update " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + type.Name) + " set " + dialect.QuoteForColumnName("DocumentId") + " =" + dialect.QuoteForParameter("mapid") + " where " + dialect.QuoteForColumnName("Id") + " = " + dialect.QuoteForParameter("Id"); GetProperties(command, Index, "", dialect); command.AddParameter($"DocumentId", Index.GetAddedDocuments().Single().Id); + dialect.PrepareReturningMapIndexCommand(command); Index.Id = Convert.ToInt32(await command.ExecuteScalarAsync()); } else { - Index.Id = await connection.ExecuteScalarAsync(sql, Index, transaction); - - var reduceIndex = Index as ReduceIndex; + Index.Id = await dialect.InsertReturningReduceIndexAsync(connection, Index, sql, transaction); var bridgeTableName = _store.Configuration.TableNameConvention.GetIndexTable(type, Collection) + "_" + documentTable; var columnList = dialect.QuoteForColumnName(type.Name + "Id") + ", " + dialect.QuoteForColumnName("DocumentId"); - var bridgeSql = "insert into " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) + " (" + columnList + ") values (@Id, @DocumentId);"; + var bridgeSql = "insert into " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) + " (" + columnList + ") values (" + dialect.QuoteForParameter("Id") + ", " + dialect.QuoteForParameter("DocumentId") + ")" + dialect.StatementEnd; if (logger.IsEnabled(LogLevel.Trace)) { @@ -107,11 +106,9 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom } else { - var reduceIndex = Index as ReduceIndex; - var bridgeTableName = _store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection) + "_" + documentTable; var columnList = dialect.QuoteForColumnName(type.Name + "Id") + ", " + dialect.QuoteForColumnName("DocumentId"); - queries.Add($"insert into {dialect.QuoteForTableName(bridgeTableName)} ({columnList}) values ({dialect.IdentityLastId}, @DocumentId_{index});"); + queries.Add($"insert into {dialect.QuoteForTableName(bridgeTableName)} ({columnList}) values ({dialect.IdentityLastId}, {dialect.QuoteForParameter("DocumentId")}_{index})"+ dialect.BatchStatementEnd); batchCommand.AddParameter($"DocumentId_{index}", _addedDocumentIds[0]); } diff --git a/src/YesSql.Core/Commands/DeleteDocumentCommand.cs b/src/YesSql.Core/Commands/DeleteDocumentCommand.cs index 31164f78..0d730505 100644 --- a/src/YesSql.Core/Commands/DeleteDocumentCommand.cs +++ b/src/YesSql.Core/Commands/DeleteDocumentCommand.cs @@ -21,7 +21,9 @@ public DeleteDocumentCommand(Document document, IStore store, string collection) public override Task ExecuteAsync(DbConnection connection, DbTransaction transaction, ISqlDialect dialect, ILogger logger) { var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); - var deleteCmd = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + documentTable)} where {dialect.QuoteForColumnName("Id")} = @Id;"; + var deleteCmd = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + documentTable) + " where " + dialect.QuoteForColumnName("Id") + + " = " + dialect.QuoteForParameter("Id") + dialect.StatementEnd; + if (logger.IsEnabled(LogLevel.Trace)) { @@ -35,7 +37,8 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom { var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); - var deleteCmd = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + documentTable)} where {dialect.QuoteForColumnName("Id")} = @Id_{index};"; + var deleteCmd = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + documentTable)} where {dialect.QuoteForColumnName("Id")} " + + $"= {dialect.QuoteForParameter("Id")}_{index}"+ dialect.BatchStatementEnd; queries.Add(deleteCmd); command.AddParameter($"Id_{index}", Document.Id, DbType.Int32); diff --git a/src/YesSql.Core/Commands/DeleteMapIndexCommand.cs b/src/YesSql.Core/Commands/DeleteMapIndexCommand.cs index a8ef5762..c2eabd5a 100644 --- a/src/YesSql.Core/Commands/DeleteMapIndexCommand.cs +++ b/src/YesSql.Core/Commands/DeleteMapIndexCommand.cs @@ -27,7 +27,7 @@ public DeleteMapIndexCommand(Type indexType, int documentId, IStore store, strin public Task ExecuteAsync(DbConnection connection, DbTransaction transaction, ISqlDialect dialect, ILogger logger ) { - var command = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(IndexType, Collection))} where {dialect.QuoteForColumnName("DocumentId")} = @Id;"; + var command = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(IndexType, Collection))} where {dialect.QuoteForColumnName("DocumentId")} = " + dialect.QuoteForParameter("Id") + dialect.StatementEnd; if (logger.IsEnabled(LogLevel.Trace)) { @@ -39,7 +39,7 @@ public Task ExecuteAsync(DbConnection connection, DbTransaction transaction, ISq public bool AddToBatch(ISqlDialect dialect, List queries, DbCommand command, List> actions, int index) { - var sql = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(IndexType, Collection))} where {dialect.QuoteForColumnName("DocumentId")} = @Id_{index};"; + var sql = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(IndexType, Collection))} where {dialect.QuoteForColumnName("DocumentId")} = {dialect.QuoteForParameter("Id")}_{index}"+ dialect.BatchStatementEnd; queries.Add(sql); diff --git a/src/YesSql.Core/Commands/DeleteReduceIndexCommand.cs b/src/YesSql.Core/Commands/DeleteReduceIndexCommand.cs index 54b1c513..f8a4d3e7 100644 --- a/src/YesSql.Core/Commands/DeleteReduceIndexCommand.cs +++ b/src/YesSql.Core/Commands/DeleteReduceIndexCommand.cs @@ -23,8 +23,8 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); var bridgeTableName = _store.Configuration.TableNameConvention.GetIndexTable(type, Collection) + "_" + documentTable; - var bridgeSql = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName)} where {dialect.QuoteForColumnName(name + "Id")} = @Id_{index};"; - var command = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection))} where { dialect.QuoteForColumnName("Id")} = @Id_{index};"; + var bridgeSql = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName)} where {dialect.QuoteForColumnName(name + "Id")} = {dialect.QuoteForParameter("Id")}_{index}"+ dialect.BatchStatementEnd; + var command = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection))} where { dialect.QuoteForColumnName("Id")} = {dialect.QuoteForParameter("Id")}_{index}"+ dialect.BatchStatementEnd; queries.Add(bridgeSql); queries.Add(command); batchCommand.AddParameter("Id_" + index, Index.Id); @@ -39,13 +39,13 @@ public override async Task ExecuteAsync(DbConnection connection, DbTransaction t var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); var bridgeTableName = _store.Configuration.TableNameConvention.GetIndexTable(type, Collection) + "_" + documentTable; - var bridgeSql = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) +" where " + dialect.QuoteForColumnName(name + "Id") + " = @Id;"; + var bridgeSql = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) +" where " + dialect.QuoteForColumnName(name + "Id") + " = " + dialect.QuoteForParameter("Id") + dialect.StatementEnd;; if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace(bridgeSql); } await connection.ExecuteAsync(bridgeSql, new { Id = Index.Id }, transaction); - var command = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection)) + " where " + dialect.QuoteForColumnName("Id") + " = @Id;"; + var command = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection)) + " where " + dialect.QuoteForColumnName("Id") + " = " + dialect.QuoteForParameter("Id") + dialect.StatementEnd;; if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace(command); diff --git a/src/YesSql.Core/Commands/DocumentCommand.cs b/src/YesSql.Core/Commands/DocumentCommand.cs index 86f927dd..3ce330e9 100644 --- a/src/YesSql.Core/Commands/DocumentCommand.cs +++ b/src/YesSql.Core/Commands/DocumentCommand.cs @@ -1,20 +1,20 @@ -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data.Common; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace YesSql.Commands { public abstract class DocumentCommand : IIndexCommand, ICollectionName { - protected static readonly PropertyInfo[] AllProperties = new PropertyInfo[] + protected static readonly PropertyInfo[] AllProperties = { typeof(Document).GetProperty("Type") }; - protected static readonly PropertyInfo[] AllKeys = new PropertyInfo[] + protected static readonly PropertyInfo[] AllKeys = { typeof(Document).GetProperty("Id") }; diff --git a/src/YesSql.Core/Commands/IndexCommand.cs b/src/YesSql.Core/Commands/IndexCommand.cs index 009a6bc9..6e3ad21c 100644 --- a/src/YesSql.Core/Commands/IndexCommand.cs +++ b/src/YesSql.Core/Commands/IndexCommand.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,6 +7,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using YesSql.Indexes; using YesSql.Serialization; @@ -28,7 +28,7 @@ public abstract class IndexCommand : IIndexCommand public abstract int ExecutionOrder { get; } - public IndexCommand(IIndex index, IStore store, string collection) + protected IndexCommand(IIndex index, IStore store, string collection) { Index = index; _store = store; @@ -58,9 +58,9 @@ protected static void GetProperties(DbCommand command, object item, string suffi var value = accessor.Get(item); var parameter = command.CreateParameter(); - parameter.ParameterName = property.Name + suffix; - parameter.Value = dialect.TryConvert(value) ?? DBNull.Value; + parameter.ParameterName = dialect.GetParameterName(property.Name) + suffix; parameter.DbType = dialect.ToDbType(property.PropertyType); + parameter.Value = dialect.TryConvert(value) ?? DBNull.Value; command.Parameters.Add(parameter); } } @@ -105,7 +105,7 @@ protected string Inserts(Type type, ISqlDialect dialect) for (var i = 0; i < allProperties.Count(); i++) { var property = allProperties.ElementAt(i); - sbParameterList.Append("@").Append(property.Name).Append(ParameterSuffix); + sbParameterList.Append(dialect.QuoteForParameter(property.Name)).Append(ParameterSuffix); if (i < allProperties.Count() - 1) { sbParameterList.Append(", "); @@ -116,7 +116,7 @@ protected string Inserts(Type type, ISqlDialect dialect) { // We can set the document id sbColumnList.Append(", ").Append(dialect.QuoteForColumnName("DocumentId")); - sbParameterList.Append(", @DocumentId").Append(ParameterSuffix); + sbParameterList.Append(", ").Append(dialect.QuoteForParameter("DocumentId")).Append(ParameterSuffix); } values = $"({sbColumnList}) values ({sbParameterList})"; @@ -125,7 +125,7 @@ protected string Inserts(Type type, ISqlDialect dialect) { if (typeof(MapIndex).IsAssignableFrom(type)) { - values = $"({dialect.QuoteForColumnName("DocumentId")}) values (@DocumentId{ParameterSuffix})"; + values = $"({dialect.QuoteForColumnName("DocumentId")}) values ({dialect.QuoteForParameter("DocumentId")}{ParameterSuffix})"; } else { @@ -133,10 +133,10 @@ protected string Inserts(Type type, ISqlDialect dialect) } } - InsertsList[key] = result = $"insert into {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection))} {values} {dialect.IdentitySelectString} {dialect.QuoteForColumnName("Id")};"; + InsertsList[key] = result = $"insert into {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection))} {values} {dialect.IdentitySelectString} {dialect.QuoteForColumnName("Id")}{dialect.StatementEnd}"; } - return result; + return result; } protected string Updates(Type type, ISqlDialect dialect) @@ -151,14 +151,14 @@ protected string Updates(Type type, ISqlDialect dialect) for (var i = 0; i < allProperties.Length; i++) { var property = allProperties[i]; - values.Append(dialect.QuoteForColumnName(property.Name) + " = @" + property.Name + ParameterSuffix); + values.Append(dialect.QuoteForColumnName(property.Name) + " = " + dialect.QuoteForParameter(property.Name) + ParameterSuffix); if (i < allProperties.Length - 1) { values.Append(", "); } } - UpdatesList[key] = result = $"update {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection))} set {values} where {dialect.QuoteForColumnName("Id")} = @Id{ParameterSuffix};"; + UpdatesList[key] = result = $"update {dialect.QuoteForTableName(_store.Configuration.TablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(type, Collection))} set {values} where {dialect.QuoteForColumnName("Id")} = {dialect.QuoteForParameter("Id")}{ParameterSuffix}{dialect.StatementEnd}"; } return result; @@ -170,8 +170,7 @@ private static bool IsWriteable(PropertyInfo pi) pi.Name != nameof(IIndex.Id) && // don't read DocumentId when on a MapIndex as it might be used to // read the DocumentId directly from an Index query - pi.Name != "DocumentId" - ; + pi.Name != "DocumentId"; } public abstract bool AddToBatch(ISqlDialect dialect, List queries, DbCommand batchCommand, List> actions, int index); diff --git a/src/YesSql.Core/Commands/UpdateDocumentCommand.cs b/src/YesSql.Core/Commands/UpdateDocumentCommand.cs index aa106bcb..090f78c4 100644 --- a/src/YesSql.Core/Commands/UpdateDocumentCommand.cs +++ b/src/YesSql.Core/Commands/UpdateDocumentCommand.cs @@ -1,10 +1,10 @@ -using Dapper; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Threading.Tasks; +using Dapper; +using Microsoft.Extensions.Logging; namespace YesSql.Commands { @@ -26,28 +26,27 @@ public override async Task ExecuteAsync(DbConnection connection, DbTransaction t var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); var updateCmd = $"update {dialect.QuoteForTableName(_store.Configuration.TablePrefix + documentTable)} " - + $"set {dialect.QuoteForColumnName("Content")} = @Content, {dialect.QuoteForColumnName("Version")} = @Version where " - + $"{dialect.QuoteForColumnName("Id")} = @Id " + + $"set {dialect.QuoteForColumnName("Content")} = {dialect.QuoteForParameter("Content")}, " + + $"{dialect.QuoteForColumnName("Version")} = {dialect.QuoteForParameter("Version")} where " + + $"{dialect.QuoteForColumnName("Id")} = {dialect.QuoteForParameter("Id")} " + (_checkVersion > -1 ? Document.Version == 1 // When the Document.Version is 0 + 1 the Version column maybe null. - ? $" and ({dialect.QuoteForColumnName("Version")} IS NULL OR {dialect.QuoteForColumnName("Version")} = {dialect.GetSqlValue(_checkVersion)}) ;" - : $" and {dialect.QuoteForColumnName("Version")} = {dialect.GetSqlValue(_checkVersion)} ;" - : ";") - ; + ? $" and ({dialect.QuoteForColumnName("Version")} IS NULL OR {dialect.QuoteForColumnName("Version")} = {dialect.GetSqlValue(_checkVersion)}){dialect.StatementEnd}" + : $" and {dialect.QuoteForColumnName("Version")} = {dialect.GetSqlValue(_checkVersion)}{dialect.StatementEnd}" + : dialect.StatementEnd); if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace(updateCmd); } - var updatedCount = await connection.ExecuteAsync(updateCmd, Document, transaction); + var parameters = dialect.GetDynamicParameters(connection, Document, _store.Configuration.TablePrefix + documentTable); + var updatedCount = await connection.ExecuteAsync(updateCmd, parameters, transaction); if (_checkVersion > -1 && updatedCount != 1) { throw new ConcurrencyException(); } - - return; } public override bool AddToBatch(ISqlDialect dialect, List queries, DbCommand batchCommand, List> actions, int index) @@ -65,16 +64,16 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); var updateCmd = $"update {dialect.QuoteForTableName(_store.Configuration.TablePrefix + documentTable)} " - + $"set {dialect.QuoteForColumnName("Content")} = @Content_{index}, {dialect.QuoteForColumnName("Version")} = @Version_{index} where " - + $"{dialect.QuoteForColumnName("Id")} = @Id_{index};" - ; + + $"set {dialect.QuoteForColumnName("Content")} = {dialect.QuoteForParameter("Content")}_{index}," + + $" {dialect.QuoteForColumnName("Version")} = {dialect.QuoteForParameter("Version")}_{index} where " + + $"{dialect.QuoteForColumnName("Id")} = {dialect.QuoteForParameter("Id")}_{index}{dialect.BatchStatementEnd}"; queries.Add(updateCmd); batchCommand .AddParameter("Id_" + index, Document.Id, DbType.Int32) .AddParameter("Content_" + index, Document.Content, DbType.String) - .AddParameter("Version_" + index, Document.Version, DbType.Int64); + .AddParameter(dialect.GetParameterName("Version") + "_" + index, Document.Version, DbType.Int64); return true; } diff --git a/src/YesSql.Core/Commands/UpdateIndexCommand.cs b/src/YesSql.Core/Commands/UpdateIndexCommand.cs index 2073b6a6..1207883a 100644 --- a/src/YesSql.Core/Commands/UpdateIndexCommand.cs +++ b/src/YesSql.Core/Commands/UpdateIndexCommand.cs @@ -1,12 +1,11 @@ -using Dapper; -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Threading.Tasks; +using Dapper; +using Microsoft.Extensions.Logging; using YesSql.Indexes; -using YesSql.Sql.Schema; namespace YesSql.Commands { @@ -32,13 +31,14 @@ public override async Task ExecuteAsync(DbConnection connection, DbTransaction t { var type = Index.GetType(); + var parameter = dialect.GetSafeIndexParameters(Index); var sql = Updates(type, dialect); sql = sql.Replace(ParameterSuffix, ""); if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace(sql); } - await connection.ExecuteAsync(sql, Index, transaction); + await connection.ExecuteAsync(sql, parameter, transaction); // Update the documents list if (Index is ReduceIndex) @@ -46,8 +46,10 @@ public override async Task ExecuteAsync(DbConnection connection, DbTransaction t var documentTable = _store.Configuration.TableNameConvention.GetDocumentTable(Collection); var bridgeTableName = _store.Configuration.TableNameConvention.GetIndexTable(type, Collection) + "_" + documentTable; var columnList = dialect.QuoteForTableName(type.Name + "Id") + ", " + dialect.QuoteForColumnName("DocumentId"); - var bridgeSqlAdd = "insert into " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) + " (" + columnList + ") values (@Id, @DocumentId);"; - var bridgeSqlRemove = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) + " where " + dialect.QuoteForColumnName("DocumentId") + " = @DocumentId and " + dialect.QuoteForColumnName(type.Name + "Id") + " = @Id;"; + var bridgeSqlAdd = "insert into " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) + " (" + columnList + ") " + + "values (" + dialect.QuoteForParameter("Id") + ", " + dialect.QuoteForParameter("DocumentId") + ")" + dialect.StatementEnd; + var bridgeSqlRemove = "delete from " + dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName) + " where " + dialect.QuoteForColumnName("DocumentId") + " =" + + dialect.QuoteForParameter("DocumentId") + " and " + dialect.QuoteForColumnName(type.Name + "Id") + " = " + dialect.QuoteForParameter("Id") + dialect.StatementEnd; if (_addedDocumentIds.Any()) { @@ -89,7 +91,7 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom queries.Add(sql); GetProperties(command, Index, index.ToString(), dialect); - + var parameter = command.CreateParameter(); parameter.ParameterName = $"Id{index}"; parameter.Value = Index.Id; @@ -111,7 +113,8 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom for (var i = 0; i < _addedDocumentIds.Length; i++) { - var bridgeSqlAdd = $"insert into {dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName)} ({columnList}) values (@Id_{index}, @AddedId_{index}_{i});"; + var bridgeSqlAdd = $"insert into {dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName)} ({columnList}) " + + $"values ({dialect.QuoteForParameter("Id")}_{index}, {dialect.QuoteForParameter("AddedId")}_{index}_{i}){dialect.BatchStatementEnd}"; queries.Add(bridgeSqlAdd); parameter = command.CreateParameter(); @@ -123,7 +126,9 @@ public override bool AddToBatch(ISqlDialect dialect, List queries, DbCom for (var i = 0; i < _deletedDocumentIds.Length; i++) { - var bridgeSqlRemove = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName)} where {dialect.QuoteForColumnName("DocumentId")} = @RemovedId_{index}_{i} and {dialect.QuoteForColumnName(type.Name + "Id")} = @Id_{index};"; + var bridgeSqlRemove = $"delete from {dialect.QuoteForTableName(_store.Configuration.TablePrefix + bridgeTableName)} " + + $"where {dialect.QuoteForColumnName("DocumentId")} = {dialect.QuoteForParameter("RemovedId")}_{index}_{i} " + + $"and {dialect.QuoteForColumnName(type.Name + "Id")} = {dialect.QuoteForParameter("Id")}_{index}{dialect.BatchStatementEnd}"; queries.Add(bridgeSqlRemove); parameter = command.CreateParameter(); diff --git a/src/YesSql.Core/Data/WorkerQueryKey.cs b/src/YesSql.Core/Data/WorkerQueryKey.cs index 470ae2cd..3f8b3c35 100644 --- a/src/YesSql.Core/Data/WorkerQueryKey.cs +++ b/src/YesSql.Core/Data/WorkerQueryKey.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace YesSql.Data { @@ -15,19 +16,9 @@ namespace YesSql.Data public WorkerQueryKey(string prefix, int[] ids) { - if (prefix == null) - { - throw new ArgumentNullException(nameof(prefix)); - } - - if (ids == null) - { - throw new ArgumentNullException(nameof(ids)); - } - - _prefix = prefix; + _prefix = prefix ?? throw new ArgumentNullException(nameof(prefix)); _parameters = null; - _ids = ids; + _ids = ids ?? throw new ArgumentNullException(nameof(ids)); _hashcode = 0; _hashcode = BuildHashCode(); } @@ -59,12 +50,12 @@ public bool Equals(WorkerQueryKey other) { return false; } - + if (_parameters != null || other._parameters != null) { return SameParameters(_parameters, other._parameters); } - + if (_ids != null || other._ids != null) { return SameIds(_ids, other._ids); @@ -99,12 +90,7 @@ private int BuildHashCode() if (_ids != null) { - foreach (var id in _ids) - { - combinedHash = ((combinedHash << 5) + combinedHash) ^ id; - } - - return combinedHash; + return _ids.Aggregate(combinedHash, (current, id) => ((current << 5) + current) ^ id); } return default; diff --git a/src/YesSql.Core/Provider/BaseDialect.cs b/src/YesSql.Core/Provider/BaseDialect.cs index 505a3f12..b972f414 100644 --- a/src/YesSql.Core/Provider/BaseDialect.cs +++ b/src/YesSql.Core/Provider/BaseDialect.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Globalization; using System.Text; +using System.Threading.Tasks; +using Dapper; +using YesSql.Indexes; using YesSql.Sql; namespace YesSql.Provider @@ -75,7 +79,7 @@ public virtual object TryConvert(object source) public abstract string Name { get; } public virtual string InOperator(string values) { - if (values.StartsWith("@") && !values.Contains(",")) + if (values.StartsWith(ParameterNamePrefix) && !values.Contains(",")) { return " IN " + values; } @@ -106,7 +110,7 @@ public virtual string NotInSelectOperator(string values) public abstract string IdentitySelectString { get; } public abstract string IdentityLastId { get; } - + public virtual string IdentityColumnString => "[int] IDENTITY(1,1) primary key"; public virtual string NullColumnString => String.Empty; @@ -309,5 +313,48 @@ public void AddTypeHandler(Func handler) handlers.Add(i => handler((T)i)); } + + public virtual string QuoteForParameter(string parameterName) + { + return ParameterNamePrefix + parameterName; + } + + public virtual string GetParameterName(string parameterName) + { + return parameterName; + } + public virtual string ParameterNamePrefix => "@"; + public virtual string StatementEnd => ";"; + public virtual string BatchStatementEnd => ";"; + + public virtual string NullString => String.Empty; + + public virtual bool IsSpecialDistinctRequired => false; + + public virtual IDbCommand ConfigureCommand(IDbCommand command) + { + return command; + } + + public virtual Task InsertReturningReduceIndexAsync(DbConnection connection, IIndex index, string sql, DbTransaction transaction) + { + return connection.ExecuteScalarAsync(sql, index, transaction); + } + + public virtual void PrepareReturningMapIndexCommand(DbCommand command) + { + //not used, maybe should be removed + } + public virtual object GetDynamicParameters(DbConnection connection, object parameters, string tableName) + { + return new DynamicParameters(parameters); + } + + public virtual object GetSafeIndexParameters(IIndex index) + { + return new DynamicParameters(index); + } + + public virtual string AliasKeyword => "AS"; } } diff --git a/src/YesSql.Core/Serialization/JsonContentSerializer.cs b/src/YesSql.Core/Serialization/JsonContentSerializer.cs index 648093e5..ccb33080 100644 --- a/src/YesSql.Core/Serialization/JsonContentSerializer.cs +++ b/src/YesSql.Core/Serialization/JsonContentSerializer.cs @@ -5,7 +5,7 @@ namespace YesSql.Serialization { public class JsonContentSerializer : IContentSerializer { - private readonly static JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { + private static readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, DateTimeZoneHandling = DateTimeZoneHandling.Utc, NullValueHandling = NullValueHandling.Ignore diff --git a/src/YesSql.Core/Services/DbBlockIdGenerator.cs b/src/YesSql.Core/Services/DbBlockIdGenerator.cs index 347f899c..7422ad87 100644 --- a/src/YesSql.Core/Services/DbBlockIdGenerator.cs +++ b/src/YesSql.Core/Services/DbBlockIdGenerator.cs @@ -1,7 +1,8 @@ -using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Data.Common; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using YesSql.Sql; namespace YesSql.Services @@ -43,9 +44,13 @@ public async Task InitializeAsync(IStore store, ISchemaBuilder builder) _tablePrefix = store.Configuration.TablePrefix; _store = store; - SelectCommand = "SELECT " + _dialect.QuoteForColumnName("nextval") + " FROM " + _dialect.QuoteForTableName(_tablePrefix + TableName) + " WHERE " + _dialect.QuoteForTableName("dimension") + " = @dimension;"; - UpdateCommand = "UPDATE " + _dialect.QuoteForTableName(_tablePrefix + TableName) + " SET " + _dialect.QuoteForColumnName("nextval") + "=@new WHERE " + _dialect.QuoteForColumnName("nextval") + " = @previous AND " + _dialect.QuoteForColumnName("dimension") + " = @dimension;"; - InsertCommand = "INSERT INTO " + _dialect.QuoteForTableName(_tablePrefix + TableName) + " (" + _dialect.QuoteForColumnName("dimension") + ", " + _dialect.QuoteForColumnName("nextval") + ") VALUES(@dimension, @nextval);"; + SelectCommand = "SELECT " + _dialect.QuoteForColumnName("nextval") + " FROM " + _dialect.QuoteForTableName(_tablePrefix + TableName) + + " WHERE " + _dialect.QuoteForTableName("dimension") + " = " + _dialect.QuoteForParameter("dimension") + _dialect.StatementEnd; + UpdateCommand = "UPDATE " + _dialect.QuoteForTableName(_tablePrefix + TableName) + " SET " + _dialect.QuoteForColumnName("nextval") + " = " + _dialect.QuoteForParameter("new") + + " WHERE " + _dialect.QuoteForColumnName("nextval") + " = " + _dialect.QuoteForParameter("previous") + + " AND " + _dialect.QuoteForColumnName("dimension") + " = " + _dialect.QuoteForParameter("dimension") + _dialect.StatementEnd; + InsertCommand = "INSERT INTO " + _dialect.QuoteForTableName(_tablePrefix + TableName) + " (" + _dialect.QuoteForColumnName("dimension") + ", " + + _dialect.QuoteForColumnName("nextval") + ") VALUES(" + _dialect.QuoteForParameter("dimension") + ", " + _dialect.QuoteForParameter("nextval") + ")" + _dialect.StatementEnd; #if SUPPORTS_ASYNC_TRANSACTIONS await using (var connection = store.Configuration.ConnectionFactory.CreateConnection()) @@ -86,7 +91,7 @@ public async Task InitializeAsync(IStore store, ISchemaBuilder builder) public long GetNextId(string collection) { - collection ??= ""; + collection = string.IsNullOrEmpty(collection) ? _dialect.NullString : collection; lock (_synLock) { @@ -126,11 +131,12 @@ private void LeaseRange(Range range) try { var selectCommand = connection.CreateCommand(); + selectCommand = (DbCommand)_dialect.ConfigureCommand(selectCommand); selectCommand.CommandText = SelectCommand; var selectDimension = selectCommand.CreateParameter(); selectDimension.Value = range.Collection; - selectDimension.ParameterName = "@dimension"; + selectDimension.ParameterName = _dialect.QuoteForParameter("dimension"); selectCommand.Parameters.Add(selectDimension); selectCommand.Transaction = transaction; @@ -142,21 +148,22 @@ private void LeaseRange(Range range) nextval = Convert.ToInt64(selectCommand.ExecuteScalar()); var updateCommand = connection.CreateCommand(); + updateCommand = (DbCommand)_dialect.ConfigureCommand(updateCommand); updateCommand.CommandText = UpdateCommand; var updateDimension = updateCommand.CreateParameter(); updateDimension.Value = range.Collection; - updateDimension.ParameterName = "@dimension"; + updateDimension.ParameterName = _dialect.QuoteForParameter("dimension"); updateCommand.Parameters.Add(updateDimension); var newValue = updateCommand.CreateParameter(); newValue.Value = nextval + _blockSize; - newValue.ParameterName = "@new"; + newValue.ParameterName = _dialect.QuoteForParameter("new"); updateCommand.Parameters.Add(newValue); var previousValue = updateCommand.CreateParameter(); previousValue.Value = nextval; - previousValue.ParameterName = "@previous"; + previousValue.ParameterName = _dialect.QuoteForParameter("previous"); updateCommand.Parameters.Add(previousValue); updateCommand.Transaction = transaction; @@ -188,6 +195,10 @@ private void LeaseRange(Range range) public async Task InitializeCollectionAsync(IConfiguration configuration, string collection) { + if (String.IsNullOrEmpty(collection)) + { + collection = _dialect.NullString; + } if (_ranges.ContainsKey(collection)) { return; @@ -215,7 +226,7 @@ public async Task InitializeCollectionAsync(IConfiguration configuration, string var selectDimension = selectCommand.CreateParameter(); selectDimension.Value = collection; - selectDimension.ParameterName = "@dimension"; + selectDimension.ParameterName = _dialect.QuoteForParameter("dimension"); selectCommand.Parameters.Add(selectDimension); selectCommand.Transaction = transaction; @@ -255,12 +266,12 @@ public async Task InitializeCollectionAsync(IConfiguration configuration, string var dimensionParameter = command.CreateParameter(); dimensionParameter.Value = collection; - dimensionParameter.ParameterName = "@dimension"; + dimensionParameter.ParameterName = _dialect.QuoteForParameter("dimension"); command.Parameters.Add(dimensionParameter); var nextValParameter = command.CreateParameter(); nextValParameter.Value = 1; - nextValParameter.ParameterName = "@nextval"; + nextValParameter.ParameterName = _dialect.QuoteForParameter("nextval"); command.Parameters.Add(nextValParameter); if (_store.Configuration.Logger.IsEnabled(LogLevel.Trace)) @@ -283,7 +294,7 @@ public async Task InitializeCollectionAsync(IConfiguration configuration, string } _ranges[collection] = new Range(collection); - } + } } private class Range diff --git a/src/YesSql.Core/Services/DefaultIdGenerator.cs b/src/YesSql.Core/Services/DefaultIdGenerator.cs index ed5462b6..7fc5c59f 100644 --- a/src/YesSql.Core/Services/DefaultIdGenerator.cs +++ b/src/YesSql.Core/Services/DefaultIdGenerator.cs @@ -21,7 +21,7 @@ public long GetNextId(string collection) { lock (_synLock) { - collection = collection ?? ""; + collection = string.IsNullOrEmpty(collection) ? _dialect.NullString : collection; if (!_seeds.TryGetValue(collection, out var seed)) { diff --git a/src/YesSql.Core/Services/DefaultQuery.cs b/src/YesSql.Core/Services/DefaultQuery.cs index 9224001b..58768d70 100644 --- a/src/YesSql.Core/Services/DefaultQuery.cs +++ b/src/YesSql.Core/Services/DefaultQuery.cs @@ -1,5 +1,3 @@ -using Dapper; -using Microsoft.Extensions.Logging; using System; using System.Collections; using System.Collections.Generic; @@ -8,6 +6,8 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using Dapper; +using Microsoft.Extensions.Logging; using YesSql.Data; using YesSql.Indexes; using YesSql.Utils; @@ -100,18 +100,19 @@ public List GetBindings() public QueryState Clone() { - var clone = new QueryState(_sqlBuilder.Clone(), _store, _collection); - - clone._bindingName = _bindingName; - clone._bindings = new Dictionary>(); + var clone = new QueryState(_sqlBuilder.Clone(), _store, _collection) + { + _bindingName = _bindingName, + _bindings = new Dictionary>() + }; foreach (var binding in _bindings) { clone._bindings.Add(binding.Key, new List(binding.Value)); } - clone._currentPredicate = (CompositeNode) _predicate.Clone(); + clone._currentPredicate = (CompositeNode)_predicate.Clone(); clone._predicate = clone._currentPredicate; - + clone._lastParameterName = _lastParameterName; clone._parameterBindings = _parameterBindings == null ? null : new List>(_parameterBindings); @@ -124,8 +125,8 @@ public class DefaultQuery : IQuery internal QueryState _queryState; private readonly Session _session; private readonly ISqlDialect _dialect; - private object _compiledQuery = null; - private string _collection; + private readonly object _compiledQuery = null; + private readonly string _collection; public static Dictionary> MethodMappings = new Dictionary>(); @@ -222,7 +223,7 @@ static DefaultQuery() var objects = Expression.Lambda(expression.Arguments[1]).Compile().DynamicInvoke() as IEnumerable; var values = new List(); - foreach(var o in objects) + foreach (var o in objects) { values.Add(o); } @@ -234,7 +235,7 @@ static DefaultQuery() else if (values.Count == 1) { query.ConvertFragment(builder, expression.Arguments[0]); - builder.Append(" = " ); + builder.Append(" = "); query.ConvertFragment(builder, Expression.Constant(values[0])); } else @@ -402,7 +403,7 @@ private void Bind(Type tIndex) if (bindingIndex != -1) { // When a binding is reused it should be last to be correctly applied to a filter predicate. - if (bindingIndex != bindings.Count -1) + if (bindingIndex != bindings.Count - 1) { var binding = bindings[bindingIndex]; bindings.RemoveAt(bindingIndex); @@ -528,7 +529,7 @@ private ConstantExpression Evaluate(Expression expression) // Create a delegate that will be invoked every time a compiled query is reused, // which will re-evaluate the current node, for the current parameter. - var _parameterName = "@p" + _queryState._sqlBuilder.Parameters.Count.ToString(); + var _parameterName = _dialect.QuoteForParameter("p" + _queryState._sqlBuilder.Parameters.Count); _queryState._parameterBindings.Add((o, sqlBuilder) => { @@ -562,7 +563,7 @@ private ConstantExpression Evaluate(Expression expression) // Create a delegate that will be invoked every time a compiled query is reused, // which will re-evaluate the current node, for the current parameter. - var _parameterName = "@p" + _queryState._sqlBuilder.Parameters.Count.ToString(); + var _parameterName = _dialect.QuoteForParameter("p" + _queryState._sqlBuilder.Parameters.Count); _queryState._parameterBindings.Add((o, sqlBuilder) => { @@ -644,16 +645,16 @@ public void ConvertFragment(IStringBuilder builder, Expression expression) var binaryExpression = (BinaryExpression)expression; if (binaryExpression.Left is ConstantExpression left && binaryExpression.Right is ConstantExpression right) { - _queryState._lastParameterName = "@p" + _queryState._sqlBuilder.Parameters.Count.ToString(); + _queryState._lastParameterName = _dialect.QuoteForParameter("p" + _queryState._sqlBuilder.Parameters.Count); _queryState._sqlBuilder.Parameters.Add(_queryState._lastParameterName, _dialect.TryConvert(left.Value)); builder.Append(_queryState._lastParameterName); - + builder.Append(GetBinaryOperator(expression)); - _queryState._lastParameterName = "@p" + _queryState._sqlBuilder.Parameters.Count.ToString(); + _queryState._lastParameterName = _dialect.QuoteForParameter("p" + _queryState._sqlBuilder.Parameters.Count); _queryState._sqlBuilder.Parameters.Add(_queryState._lastParameterName, _dialect.TryConvert(right.Value)); builder.Append(_queryState._lastParameterName); - + return; } @@ -722,10 +723,10 @@ public void ConvertFragment(IStringBuilder builder, Expression expression) var boundTable = _queryState.GetTypeAlias(bound); builder.Append(_queryState._sqlBuilder.FormatColumn(boundTable, memberExpression.Member.Name, true)); } - + break; case ExpressionType.Constant: - _queryState._lastParameterName = "@p" + _queryState._sqlBuilder.Parameters.Count.ToString(); + _queryState._lastParameterName = _dialect.QuoteForParameter("p" + _queryState._sqlBuilder.Parameters.Count); var value = ((ConstantExpression)expression).Value; _queryState._sqlBuilder.Parameters.Add(_queryState._lastParameterName, _dialect.TryConvert(value)); builder.Append(_queryState._lastParameterName); @@ -734,7 +735,7 @@ public void ConvertFragment(IStringBuilder builder, Expression expression) var methodCallExpression = (MethodCallExpression)expression; var methodInfo = methodCallExpression.Method; Action action; - if (MethodMappings.TryGetValue(methodInfo, out action) + if (MethodMappings.TryGetValue(methodInfo, out action) || MethodMappings.TryGetValue(methodInfo.GetGenericMethodDefinition(), out action)) { action(this, builder, _dialect, methodCallExpression); @@ -1049,8 +1050,8 @@ IQuery IQuery.For(bool filterType) if (filterType) { - _queryState._sqlBuilder.WhereAnd(_queryState._sqlBuilder.FormatColumn(_queryState._documentTable, "Type") + " = @Type"); // TODO: investigate, this makes the query 3 times slower on sqlite - _queryState._sqlBuilder.Parameters["@Type"] = _session.Store.TypeNames[typeof(T)]; + _queryState._sqlBuilder.WhereAnd(_queryState._sqlBuilder.FormatColumn(_queryState._documentTable, "Type") + " = " + _dialect.QuoteForParameter("Type")); // TODO: investigate, this makes the query 3 times slower on sqlite + _queryState._sqlBuilder.Parameters[_dialect.QuoteForParameter("Type")] = _session.Store.TypeNames[typeof(T)]; } return new Query(this); @@ -1176,7 +1177,7 @@ Task> IQuery.ListAsync() async IAsyncEnumerable IQuery.ToAsyncEnumerable() { // TODO: [IAsyncEnumerable] Once Dapper supports IAsyncEnumerable we can replace this call by a non-buffered one - foreach(var item in await ListImpl()) + foreach (var item in await ListImpl()) { yield return item; } @@ -1237,7 +1238,10 @@ internal async Task> ListImpl() } _query._queryState._sqlBuilder.Selector(_query._queryState._sqlBuilder.FormatColumn(_query._queryState._documentTable, "*")); - _query._queryState._sqlBuilder.Distinct(); + if (!_query._dialect.IsSpecialDistinctRequired)//We cannot specify a LOB column in a SELECT... DISTINCT statement or in a join. + { + _query._queryState._sqlBuilder.Distinct(); + } var sql = _query._queryState._sqlBuilder.ToSqlString(); var key = new WorkerQueryKey(sql, _query._queryState._sqlBuilder.Parameters); var documents = await _query._session._store.ProduceAsync(key, static (state) => @@ -1252,6 +1256,10 @@ internal async Task> ListImpl() return state.Connection.QueryAsync(state.Sql, state.Query._queryState._sqlBuilder.Parameters, state.Transaction); }, new { Query = _query, Sql = sql, Connection = connection, Transaction = transaction }); + if (_query._dialect.IsSpecialDistinctRequired)//We cannot specify a LOB column in a SELECT... DISTINCT statement or in a join. + { + return _query._session.Get(documents.Distinct().ToArray(), _query._collection); + } return _query._session.Get(documents.ToArray(), _query._collection); } } @@ -1368,7 +1376,7 @@ private async ValueTask> ComposeQueryAsync(Func, ValueTask(_query); - } + } IQuery IQuery.With() { @@ -1400,7 +1408,7 @@ IQuery IQuery.With(Expression> predicat } } - class QueryIndex : Query, IQueryIndex where T : class, IIndex + private class QueryIndex : Query, IQueryIndex where T : class, IIndex { public QueryIndex(DefaultQuery query) : base(query) { } @@ -1521,7 +1529,7 @@ IQueryIndex IQueryIndex.WithParameter(string name, object value) } } - class Query : Query, IQuery + private class Query : Query, IQuery where T : class where TIndex : IIndex { diff --git a/src/YesSql.Core/Session.cs b/src/YesSql.Core/Session.cs index 3ab26c02..84e7aa36 100644 --- a/src/YesSql.Core/Session.cs +++ b/src/YesSql.Core/Session.cs @@ -393,7 +393,7 @@ private async Task GetDocumentByIdAsync(int id, string collection) var documentTable = Store.Configuration.TableNameConvention.GetDocumentTable(collection); - var command = "select * from " + _dialect.QuoteForTableName(_tablePrefix + documentTable) + " where " + _dialect.QuoteForColumnName("Id") + " = @Id"; + var command = "select * from " + _dialect.QuoteForTableName(_tablePrefix + documentTable) + " where " + _dialect.QuoteForColumnName("Id") + " = "+_dialect.QuoteForParameter("Id"); var key = new WorkerQueryKey(nameof(GetDocumentByIdAsync), new[] { id }); try @@ -489,7 +489,7 @@ public async Task> GetAsync(int[] ids, string collection = nul var documentTable = Store.Configuration.TableNameConvention.GetDocumentTable(collection); - var command = "select * from " + _dialect.QuoteForTableName(_tablePrefix + documentTable) + " where " + _dialect.QuoteForColumnName("Id") + " " + _dialect.InOperator("@Ids"); + var command = "select * from " + _dialect.QuoteForTableName(_tablePrefix + documentTable) + " where " + _dialect.QuoteForColumnName("Id") + " " + _dialect.InOperator(_dialect.QuoteForParameter("Ids")); var key = new WorkerQueryKey(nameof(GetAsync), ids); try @@ -609,7 +609,7 @@ public IQuery ExecuteQuery(ICompiledQuery compiledQuery, string collect _store.CompiledQueries[discriminator] = queryState; } - } + } } queryState = queryState.Clone(); @@ -778,6 +778,7 @@ private void BatchCommands() if (!_dialect.SupportsBatching || _store.Configuration.CommandsPageSize == 0) { + _commands = _commands.OrderBy(x => x.ExecutionOrder).ToList(); return; } @@ -1204,7 +1205,7 @@ private async Task ReduceForAsync(IndexDescriptor descriptor, objec await CreateConnectionAsync(); var name = _tablePrefix + _store.Configuration.TableNameConvention.GetIndexTable(descriptor.IndexType, collection); - var sql = "select * from " + _dialect.QuoteForTableName(name) + " where " + _dialect.QuoteForColumnName(descriptor.GroupKey.Name) + " = @currentKey"; + var sql = "select * from " + _dialect.QuoteForTableName(name) + " where " + _dialect.QuoteForColumnName(descriptor.GroupKey.Name) + " = "+_dialect.QuoteForParameter("currentKey"); var index = await _connection.QueryAsync(descriptor.IndexType, sql, new { currentKey }, _transaction); return index.FirstOrDefault() as ReduceIndex; diff --git a/src/YesSql.Core/Sql/BaseComandInterpreter.cs b/src/YesSql.Core/Sql/BaseComandInterpreter.cs index 4d3f2d57..b95f5e59 100644 --- a/src/YesSql.Core/Sql/BaseComandInterpreter.cs +++ b/src/YesSql.Core/Sql/BaseComandInterpreter.cs @@ -12,7 +12,7 @@ public abstract class BaseCommandInterpreter : ICommandInterpreter protected readonly ISqlDialect _dialect; private const char Space = ' '; - public BaseCommandInterpreter(ISqlDialect dialect) + protected BaseCommandInterpreter(ISqlDialect dialect) { _dialect = dialect; } diff --git a/src/YesSql.Core/Sql/Schema/AddColumnCommand.cs b/src/YesSql.Core/Sql/Schema/AddColumnCommand.cs index d6a0e48d..8ed35519 100644 --- a/src/YesSql.Core/Sql/Schema/AddColumnCommand.cs +++ b/src/YesSql.Core/Sql/Schema/AddColumnCommand.cs @@ -1,4 +1,4 @@ -namespace YesSql.Sql.Schema +namespace YesSql.Sql.Schema { public class AddColumnCommand : CreateColumnCommand, IAddColumnCommand { diff --git a/src/YesSql.Core/Sql/Schema/SqlStatementCommand.cs b/src/YesSql.Core/Sql/Schema/SqlStatementCommand.cs index 3ee99f24..91c4c0dd 100644 --- a/src/YesSql.Core/Sql/Schema/SqlStatementCommand.cs +++ b/src/YesSql.Core/Sql/Schema/SqlStatementCommand.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace YesSql.Sql.Schema { @@ -13,7 +13,7 @@ public SqlStatementCommand(string sql) } public string Sql { get; private set; } - public List Providers { get { return _providers; } } + public List Providers => _providers; public ISqlStatementCommand ForProvider(string dataProvider) { diff --git a/src/YesSql.Core/Sql/Schema/TableCommand.cs b/src/YesSql.Core/Sql/Schema/TableCommand.cs index 83046e41..f2ed85ef 100644 --- a/src/YesSql.Core/Sql/Schema/TableCommand.cs +++ b/src/YesSql.Core/Sql/Schema/TableCommand.cs @@ -9,7 +9,7 @@ public abstract class TableCommand : ITableCommand public List TableCommands { get; private set; } - public TableCommand(string tableName) + protected TableCommand(string tableName) { Name = tableName; } diff --git a/src/YesSql.Core/Sql/SchemaBuilder.cs b/src/YesSql.Core/Sql/SchemaBuilder.cs index 52444125..8c2d6344 100644 --- a/src/YesSql.Core/Sql/SchemaBuilder.cs +++ b/src/YesSql.Core/Sql/SchemaBuilder.cs @@ -160,7 +160,7 @@ public ISchemaBuilder DropMapIndexTable(Type indexType, string collection = null var indexName = indexType.Name; var indexTable = TableNameConvention.GetIndexTable(indexType, collection); - if (String.IsNullOrEmpty(Dialect.CascadeConstraintsString)) + if (string.IsNullOrEmpty(Dialect.CascadeConstraintsString)) { DropForeignKey(indexTable, "FK_" + (collection ?? "") + indexName); } diff --git a/src/YesSql.Core/Sql/SqlBuilder.cs b/src/YesSql.Core/Sql/SqlBuilder.cs index 4fefb9fb..8a6dfa80 100644 --- a/src/YesSql.Core/Sql/SqlBuilder.cs +++ b/src/YesSql.Core/Sql/SqlBuilder.cs @@ -43,7 +43,7 @@ public SqlBuilder(string tablePrefix, ISqlDialect dialect) _dialect = dialect; } - public string Clause { get { return _clause; } } + public string Clause => _clause; public void Table(string table, string alias = null) { @@ -52,7 +52,7 @@ public void Table(string table, string alias = null) if (!String.IsNullOrEmpty(alias)) { - FromSegments.Add(" AS "); + FromSegments.Add($" {_dialect.AliasKeyword} "); FromSegments.Add(_dialect.QuoteForTableName(alias)); } } @@ -96,7 +96,7 @@ public virtual void InnerJoin(string table, string onTable, string onColumn, str JoinSegments.Add(_dialect.QuoteForTableName(_tablePrefix + table)); if (!String.IsNullOrEmpty(alias)) { - JoinSegments.AddRange(new[] { " AS ", _dialect.QuoteForTableName(alias) }); + JoinSegments.AddRange(new[] { $" {_dialect.AliasKeyword} ", _dialect.QuoteForTableName(alias) }); } JoinSegments.AddRange(new[] { " ON ", _dialect.QuoteForTableName(onTable), ".", _dialect.QuoteForColumnName(onColumn), @@ -137,10 +137,8 @@ public string GetSelector() { return SelectSegments[0]; } - else - { - return string.Join("", SelectSegments); - } + + return string.Join("", SelectSegments); } public void Distinct() @@ -381,23 +379,24 @@ public virtual string ToSqlString() public ISqlBuilder Clone() { - var clone = new SqlBuilder(_tablePrefix, _dialect); - - clone._clause = _clause; - clone._table = _table; - - clone._select = _select == null ? null : new List(_select); - clone._from = _from == null ? null : new List(_from); - clone._join = _join == null ? null : new List(_join); - clone._where = _where == null ? null : new List(_where); - clone._group = _group == null ? null : new List(_group); - clone._having = _having == null ? null : new List(_having); - clone._order = _order == null ? null : new List(_order); - clone._trail = _trail == null ? null : new List(_trail); - clone._skip = _skip; - clone._count = _count; - - clone.Parameters = new Dictionary(Parameters); + var clone = new SqlBuilder(_tablePrefix, _dialect) + { + _clause = _clause, + _table = _table, + + _select = _select == null ? null : new List(_select), + _from = _from == null ? null : new List(_from), + _join = _join == null ? null : new List(_join), + _where = _where == null ? null : new List(_where), + _group = _group == null ? null : new List(_group), + _having = _having == null ? null : new List(_having), + _order = _order == null ? null : new List(_order), + _trail = _trail == null ? null : new List(_trail), + _skip = _skip, + _count = _count, + + Parameters = new Dictionary(Parameters) + }; return clone; } } diff --git a/src/YesSql.Core/StoreFactory.cs b/src/YesSql.Core/StoreFactory.cs index 0cf55ea3..a2d6b0ad 100644 --- a/src/YesSql.Core/StoreFactory.cs +++ b/src/YesSql.Core/StoreFactory.cs @@ -11,8 +11,8 @@ public class StoreFactory /// /// Creates an instance and its new . /// - /// An action to execute on the of the new instance. - /// The instance will still need to be initialized. + /// An action to execute on the of the new instance. + /// The instance will still need to be initialized. public static IStore Create(Action configuration) { var store = new Store(configuration); @@ -23,7 +23,7 @@ public static IStore Create(Action configuration) /// Initializes an instance using a specific instance. /// /// The instance to use. - /// The instance will still need to be initialized. + /// The instance will still need to be initialized. public static IStore Create(IConfiguration configuration) { var store = new Store(configuration); @@ -33,7 +33,7 @@ public static IStore Create(IConfiguration configuration) /// /// Initializes an instance and its new . /// - /// An action to execute on the of the new instance. + /// An action to execute on the of the new instance. public static async Task CreateAndInitializeAsync(Action configuration) { var store = Create(configuration); diff --git a/src/YesSql.Provider.Oracle/OracleCommandInterpreter.cs b/src/YesSql.Provider.Oracle/OracleCommandInterpreter.cs new file mode 100644 index 00000000..5e5afdce --- /dev/null +++ b/src/YesSql.Provider.Oracle/OracleCommandInterpreter.cs @@ -0,0 +1,38 @@ +using System; +using System.Data; +using System.Text; +using YesSql.Sql; +using YesSql.Sql.Schema; + +namespace YesSql.Provider.Oracle +{ + public class OracleCommandInterpreter : BaseCommandInterpreter + { + public OracleCommandInterpreter(ISqlDialect dialect) : base(dialect) { } + + public override void Run(StringBuilder builder, IAlterColumnCommand command) + { + builder.AppendFormat("alter table {0} modify {1} ", + _dialect.QuoteForTableName(command.Name), + _dialect.QuoteForColumnName(command.ColumnName)); + + var dbType = _dialect.ToDbType(command.DbType); + if (dbType == DbType.Binary || dbType == DbType.Object) + { + if (command.Length > 0 || command.Precision > 0 || command.Scale > 0) + { + throw new Exception("Error while executing data migration: you need to specify the field's type in order to change its properties"); + } + } + else + { + builder.Append(_dialect.GetTypeName(dbType, command.Length, command.Precision, command.Scale)); + } + + if (command.Default != null) + { + builder.Append(" default ").Append(_dialect.GetSqlValue(command.Default)).Append(" "); + } + } + } +} diff --git a/src/YesSql.Provider.Oracle/OracleDbProviderOptionsExtensions.cs b/src/YesSql.Provider.Oracle/OracleDbProviderOptionsExtensions.cs new file mode 100644 index 00000000..682b689c --- /dev/null +++ b/src/YesSql.Provider.Oracle/OracleDbProviderOptionsExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Data; +using Oracle.ManagedDataAccess.Client; + + +namespace YesSql.Provider.Oracle +{ + public static class OracleDbProviderOptionsExtensions + { + public static IConfiguration UseOracle( + this IConfiguration configuration, + string connectionString) + { + return UseOracle(configuration, connectionString, IsolationLevel.ReadCommitted); + } + + public static IConfiguration UseOracle( + this IConfiguration configuration, + string connectionString, + IsolationLevel isolationLevel) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new ArgumentException(nameof(connectionString)); + } + + configuration.SqlDialect = new OracleDialect(); + configuration.CommandInterpreter = new OracleCommandInterpreter(configuration.SqlDialect); + configuration.ConnectionFactory = new DbConnectionFactory(connectionString); + configuration.IsolationLevel = isolationLevel; + + return configuration; + } + } +} diff --git a/src/YesSql.Provider.Oracle/OracleDialect.cs b/src/YesSql.Provider.Oracle/OracleDialect.cs new file mode 100644 index 00000000..f783eb3c --- /dev/null +++ b/src/YesSql.Provider.Oracle/OracleDialect.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dapper; +using Dapper.Oracle; +using Oracle.ManagedDataAccess.Client; +using YesSql.Indexes; +using YesSql.Sql; +using YesSql.Utils; + +namespace YesSql.Provider.Oracle +{ + public class OracleDialect : BaseDialect + { + private const string defaultValueInsertStringForReplace = "defaultValueInsertStringForReplace"; + private static readonly ConcurrentDictionary> tableColumnInfoCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary defaultCountCache = new ConcurrentDictionary(); + + private static readonly Dictionary ColumnTypes = new Dictionary + { + {DbType.Binary, "blob"},//typeof(object), typeof(byte[]) + {DbType.Date, "date"}, + {DbType.DateTime, "timestamp" },//typeof(DateTime),typeof(DateTimeOffset) + {DbType.DateTime2, "timestamp" },//typeof(DateTime),typeof(DateTimeOffset) + {DbType.Boolean, "number(1)"},//typeof(bool), + {DbType.Decimal, "number({0},{1})"},//typeof(decimal) + {DbType.Single, "binary_float"},//typeof(float) + {DbType.Double, "binary_double"},//typeof(double) + {DbType.Int16, "number(5,0)"},//typeof(short) + {DbType.Int32, "number(9,0)"},//typeof(int) + {DbType.Int64, "number(19,0)"},//typeof(long) + {DbType.UInt16, "number(5,0)"},//,typeof(ushort) + {DbType.UInt32, "number(9,0)"},// typeof(uint) + {DbType.UInt64, "number(19,0)"},//typeof(ulong) + {DbType.AnsiString, "varchar2(255)"}, + {DbType.String, "nvarchar2(255)"},//typeof(Guid), typeof(string),typeof(char) + {DbType.SByte, "integer"}//typeof(sbyte) + }; + + private static readonly string[] UnsafeParameters = + { + "Order", + "Date", + "Version", + "Comment", + "Start", + "End", + "Number" + }; + private static readonly string safeParameterSuffix = "Safe"; + + static OracleDialect() + { + _propertyTypes = new Dictionary() + { + { typeof(object), DbType.Binary }, + { typeof(byte[]), DbType.Binary }, + { typeof(string), DbType.String }, + { typeof(char), DbType.String }, + { typeof(bool), DbType.Int32 }, + { typeof(byte), DbType.Byte }, + { typeof(sbyte), DbType.SByte }, + { typeof(short), DbType.Int16 }, + { typeof(ushort), DbType.UInt16 }, + { typeof(int), DbType.Int32 }, + { typeof(uint), DbType.Int32 }, + { typeof(long), DbType.Int64 }, + { typeof(ulong), DbType.Int64 }, + { typeof(float), DbType.Single }, + { typeof(double), DbType.Double }, + { typeof(decimal), DbType.Decimal }, + { typeof(DateTime), DbType.DateTime }, + { typeof(DateTimeOffset), DbType.DateTime }, // stored as UTC datetime DbType.DateTimeOffset??? + { typeof(Guid), DbType.AnsiString }, + { typeof(TimeSpan), DbType.Int64 }, // stored as ticks + + // Nullable types to prevent extra reflection on common ones + { typeof(char?), DbType.String }, + { typeof(bool?), DbType.Int32 }, + { typeof(byte?), DbType.Byte }, + { typeof(sbyte?), DbType.Int16 }, + { typeof(short?), DbType.Int16 }, + { typeof(ushort?), DbType.Int16 }, + { typeof(int?), DbType.Int32 }, + { typeof(uint?), DbType.Int32 }, + { typeof(long?), DbType.Int64 }, + { typeof(ulong?), DbType.Int64 }, + { typeof(float?), DbType.Single }, + { typeof(double?), DbType.Double }, + { typeof(decimal?), DbType.Decimal }, + { typeof(DateTime?), DbType.DateTime }, + { typeof(DateTimeOffset?), DbType.DateTime }, + { typeof(Guid?), DbType.String }, + { typeof(TimeSpan?), DbType.Int64 } + }; + } + + public OracleDialect() + { + Methods.Add("second", new TemplateFunction("extract(second from {0})")); + Methods.Add("minute", new TemplateFunction("extract(minute from {0})")); + Methods.Add("hour", new TemplateFunction("extract(hour from {0})")); + Methods.Add("day", new TemplateFunction("extract(day from {0})")); + Methods.Add("month", new TemplateFunction("extract(month from {0})")); + Methods.Add("year", new TemplateFunction("extract(year from {0})")); + Methods.Add("now", new TemplateFunction("SYSDATE")); + + SqlMapper.AddTypeMap(typeof(bool), DbType.Int32); + SqlMapper.AddTypeMap(typeof(uint), DbType.Int32); + SqlMapper.AddTypeMap(typeof(Guid), DbType.String); + AddTypeHandler(x => x.ToString()); + AddTypeHandler(x => x.Ticks); + AddTypeHandler(x => x.UtcDateTime); + } + + public override string Name => "Oracle"; + public override bool IsSpecialDistinctRequired => true; + public override bool SupportsBatching => false; + public override string IdentityLastId => $"lastval()";//used only batch + public override string RandomOrderByClause => "dbms_random.value"; + public override bool SupportsIfExistsBeforeTableName => false; + public override bool PrefixIndex => true; + public override string CascadeConstraintsString => " cascade constraints"; + public override bool HasDataTypeInIdentityColumn => true; + public override string IdentitySelectString => "RETURNING"; + public override string IdentityColumnString => " GENERATED ALWAYS AS IDENTITY primary key"; //only available in Oracle 12c + public override byte DefaultDecimalPrecision => 19; + public override byte DefaultDecimalScale => 5; + + public override string FormatKeyName(string name) + { + if (name.Length >= 92) + { + return HashHelper.HashName("FK_", name); + } + + return name; + } + + public override string FormatIndexName(string name) + { + if (name.Length >= 92) + { + return HashHelper.HashName("IDX_FK_", name); + } + + return name; + } + + public override string GetTypeName(DbType dbType, int? length, byte? precision, byte? scale) + { + if (length.HasValue) + { + if (dbType == DbType.String) + { + return length.Value > 2000 ? "nclob" : $"nvarchar2({length})"; + } + + if (dbType == DbType.AnsiString) + { + return length.Value > 4000 ? "clob" : $"varchar2({length})"; + } + + if (dbType == DbType.Binary) + { + return length.Value > 4000 ? "blob" : $"raw({length})"; + } + } + + if (ColumnTypes.TryGetValue(dbType, out var value)) + { + if (dbType == DbType.Decimal) + { + value = String.Format(value, precision ?? DefaultDecimalPrecision, scale ?? DefaultDecimalScale); + } + return value; + } + + throw new Exception("DbType not found for: " + dbType); + } + + public override string DefaultValuesInsert => defaultValueInsertStringForReplace; + + public override void Page(ISqlBuilder sqlBuilder, string offset, string limit) + { + //only Oracle 12c + if (offset != null) + { + sqlBuilder.Trail(" OFFSET "); + sqlBuilder.Trail(offset); + sqlBuilder.Trail(" ROWS"); + } + + if (limit != null) + { + sqlBuilder.Trail(" FETCH NEXT "); + sqlBuilder.Trail(limit); + sqlBuilder.Trail(" ROWS ONLY"); + } + } + + public override string QuoteForColumnName(string columnName) + { + return QuoteString + columnName + QuoteString; + } + + public override string QuoteForTableName(string tableName) + { + return QuoteString + tableName + QuoteString; + } + + public override string QuoteForParameter(string parameterName) + { + return ParameterNamePrefix + GetParameterName(parameterName); + } + + public override string GetParameterName(string parameterName) + { + if (UnsafeParameters.Contains(parameterName, StringComparer.InvariantCultureIgnoreCase)) + { + return parameterName + safeParameterSuffix; + } + return parameterName; + } + + public override string GetSqlValue(object value) + { + if (value == null) + { + return NullString; + } + var type = value.GetType(); + + if (type == typeof(TimeSpan)) + { + return ((TimeSpan)value).Ticks.ToString(CultureInfo.InvariantCulture); + } + + if (type == typeof(DateTimeOffset)) + { + return base.GetSqlValue(((DateTimeOffset)value).UtcDateTime); + } + + switch (Convert.GetTypeCode(value)) + { + case TypeCode.Boolean: + return (bool)value ? "1" : "0"; + default: + return base.GetSqlValue(value); + } + } + public override string NullString => "null"; + public override string ParameterNamePrefix => ":"; + public override string StatementEnd => String.Empty; + public override string BatchStatementEnd => ";"; + public override IDbCommand ConfigureCommand(IDbCommand command) + { + var oracleCommand = (OracleCommand)command; + oracleCommand.BindByName = true; + return oracleCommand; + } + public override async Task InsertReturningReduceIndexAsync(DbConnection connection, IIndex index, string insertSql, DbTransaction transaction) + { + if (insertSql.Contains(defaultValueInsertStringForReplace)) + { + insertSql = await InsertDefaultValuesAsync(connection, insertSql, transaction); + } + var sql = insertSql + " INTO :id"; + var parameters = new DynamicParameters(index); + if (IsContainSafeParameters(sql)) + { + parameters = GetSafeParameters(index); + } + parameters.Add(":id", 0, DbType.Int32, ParameterDirection.Output); + + await connection.ExecuteAsync(sql, parameters, transaction); + var result = parameters.Get(":id"); + return result; + } + public override void PrepareReturningMapIndexCommand(DbCommand command) + { + if (command.CommandText.Contains(defaultValueInsertStringForReplace)) + { + AddDefaultValues(command); + } + command.CommandText += " INTO :id"; + if (IsContainSafeParameters(command.CommandText)) + { + foreach (DbParameter parameter in command.Parameters) + { + if (UnsafeParameters.Contains(parameter.ParameterName, StringComparer.InvariantCultureIgnoreCase)) + { + parameter.ParameterName += safeParameterSuffix; + } + } + } + + var idParameter = command.CreateParameter(); + idParameter.ParameterName = "id"; + idParameter.Direction = ParameterDirection.Output; + idParameter.DbType = DbType.Int32; + idParameter.Value = 0; + command.Parameters.Add(idParameter); + } + private static void AddDefaultValues(DbCommand command) + { + var firstIndex = "INSERT INTO \"".Count(); + var lenghtTableName = command.CommandText.IndexOf("\"", firstIndex + 1) - firstIndex; + var tableName = command.CommandText.Substring(firstIndex, lenghtTableName); + if (!defaultCountCache.TryGetValue(tableName, out var count)) + { + var sqlForDataTypes = $"SELECT COUNT(column_name) as \"ColumnName\"" + + $" FROM all_tab_columns where table_name = \'{tableName}\'"; + count = command.Connection.ExecuteScalar(sqlForDataTypes); + defaultCountCache.TryAdd(tableName, count); + } + var defaultValues = $"VALUES({String.Join(",", Enumerable.Repeat("DEFAULT", count))})"; + command.CommandText = command.CommandText.Replace(defaultValueInsertStringForReplace, defaultValues); + } + + + private static async Task InsertDefaultValuesAsync(DbConnection connection, string insertSql, DbTransaction transaction) + { + var firstIndex = "INSERT INTO \"".Count(); + var lenghtTableName = insertSql.IndexOf("\"", firstIndex + 1) - firstIndex; + var tableName = insertSql.Substring(firstIndex, lenghtTableName); + if (!defaultCountCache.TryGetValue(tableName, out var count)) + { + var sqlForDataTypes = $"SELECT COUNT(column_name) as \"ColumnName\"" + + $" FROM all_tab_columns where table_name = \'{tableName}\'"; + count = await connection.ExecuteScalarAsync(sqlForDataTypes, transaction: transaction); + defaultCountCache.TryAdd(tableName, count); + } + var defaultValues = $"VALUES({String.Join(",", Enumerable.Repeat("DEFAULT", count))})"; + insertSql = insertSql.Replace(defaultValueInsertStringForReplace, defaultValues); + return insertSql; + } + + public override string GetDropIndexString(string indexName, string tableName) + { + return $"drop index \"{indexName}\""; + } + public override object GetDynamicParameters(DbConnection connection, object parameters, string tableName) + { + return GetOracleDynamicParameters(connection, parameters, tableName); + } + + public override object GetSafeIndexParameters(IIndex index) + { + return GetSafeParameters(index); + } + + private OracleDynamicParameters GetOracleDynamicParameters(DbConnection connection, object parameters, string tableName) + { + if (!tableColumnInfoCache.TryGetValue(tableName, out var dataTypes)) + { + var sqlForDataTypes = $"SELECT column_name as \"ColumnName\", data_type as \"DataType\" " + + $" FROM all_tab_columns where table_name = \'{tableName}\'"; + dataTypes = connection.Query(sqlForDataTypes); + tableColumnInfoCache.TryAdd(tableName, dataTypes); + } + var type = parameters.GetType(); + var result = new OracleDynamicParameters(); + foreach (var property in type.GetProperties()) + { + var value = property.GetValue(parameters); + var tableColumnInfo = dataTypes.FirstOrDefault(dt => dt.ColumnName == property.Name); + if (tableColumnInfo != null) + { + var propertyName = property.Name; + if (UnsafeParameters.Contains(propertyName, StringComparer.InvariantCultureIgnoreCase)) + { + propertyName += safeParameterSuffix; + } + result.Add(propertyName, value, tableColumnInfo.OracleMappingType); + } + } + + return result; + } + + private bool IsContainSafeParameters(string sql) + { + return UnsafeParameters.Any(parameter => sql.Contains(ParameterNamePrefix + parameter + safeParameterSuffix)); + } + + private DynamicParameters GetSafeParameters(IIndex index) + { + var parameters = new DynamicParameters(); + var type = index.GetType(); + foreach (var property in type.GetProperties()) + { + var value = property.GetValue(index); + var propertyName = property.Name; + if (UnsafeParameters.Contains(propertyName, StringComparer.InvariantCultureIgnoreCase)) + { + propertyName += safeParameterSuffix; + } + var dbType = ToDbType(property.PropertyType); + if (property.PropertyType == typeof(bool)) + { + dbType = DbType.Int32; + } + + parameters.Add(propertyName, value, dbType); + } + return parameters; + } + + public override string AliasKeyword => string.Empty; + + public override string GetAddForeignKeyConstraintString(string name, string[] srcColumns, string destTable, string[] destColumns, bool primaryKey) + { + var res = new StringBuilder(200); + + if (SupportsForeignKeyConstraintInAlterTable) + { + res.Append(" add"); + } + + res.Append(" constraint \"") + .Append(name) + .Append("\" foreign key (") +#if NETSTANDARD2_1 + .AppendJoin(", ", srcColumns) +#else + .Append(string.Join(", ", srcColumns)) +#endif + .Append(") references ") + .Append(destTable); + + if (!primaryKey) + { + res.Append(" (") + .Append(string.Join(", ", destColumns)) + .Append(')'); + } + + return res.ToString(); + } + + } +} diff --git a/src/YesSql.Provider.Oracle/TableColumnInfo.cs b/src/YesSql.Provider.Oracle/TableColumnInfo.cs new file mode 100644 index 00000000..06a795a2 --- /dev/null +++ b/src/YesSql.Provider.Oracle/TableColumnInfo.cs @@ -0,0 +1,38 @@ +using Dapper.Oracle; + +namespace YesSql.Provider.Oracle +{ + public class TableColumnInfo + { + public string ColumnName { get; set; } + public string DataType { get; set; } + + public OracleMappingType OracleMappingType + { + get + { + switch (DataType) + { + case "NUMBER": + return OracleMappingType.Int32; + case "NCLOB": + return OracleMappingType.NClob; + case "BLOB": + return OracleMappingType.Blob; + case "TIMESTAMP(6)": + return OracleMappingType.TimeStamp; + case "BINARY_FLOAT": + return OracleMappingType.BinaryFloat; + case "BINARY_DOUBLE": + return OracleMappingType.BinaryDouble; + case "NVARCHAR2": + return OracleMappingType.NVarchar2; + case "INTEGER": + return OracleMappingType.Int32; + default: + return OracleMappingType.NVarchar2; + } + } + } + } +} \ No newline at end of file diff --git a/src/YesSql.Provider.Oracle/YesSql.Provider.Oracle.csproj b/src/YesSql.Provider.Oracle/YesSql.Provider.Oracle.csproj new file mode 100644 index 00000000..433354dd --- /dev/null +++ b/src/YesSql.Provider.Oracle/YesSql.Provider.Oracle.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/YesSql.Provider.SqlServer/SqlServerDialect.cs b/src/YesSql.Provider.SqlServer/SqlServerDialect.cs index a1b803f3..d9c96352 100644 --- a/src/YesSql.Provider.SqlServer/SqlServerDialect.cs +++ b/src/YesSql.Provider.SqlServer/SqlServerDialect.cs @@ -3,6 +3,7 @@ using System.Data; using System.Globalization; using System.Text; +using Dapper; using YesSql.Sql; using YesSql.Utils; @@ -52,7 +53,7 @@ static SqlServerDialect() { typeof(short), DbType.Int16 }, { typeof(ushort), DbType.UInt16 }, // not supported { typeof(int), DbType.Int32 }, - { typeof(uint), DbType.UInt32 }, + { typeof(uint), DbType.Int32 }, { typeof(long), DbType.Int64 }, { typeof(ulong), DbType.UInt64 }, { typeof(float), DbType.Single }, @@ -71,7 +72,7 @@ static SqlServerDialect() { typeof(short?), DbType.Int16 }, { typeof(ushort?), DbType.UInt16 }, { typeof(int?), DbType.Int32 }, - { typeof(uint?), DbType.UInt32 }, + { typeof(uint?), DbType.Int32 }, { typeof(long?), DbType.Int64 }, { typeof(ulong?), DbType.UInt64 }, { typeof(float?), DbType.Single }, @@ -92,6 +93,7 @@ public SqlServerDialect() Methods.Add("minute", new TemplateFunction("datepart(minute, {0})")); Methods.Add("hour", new TemplateFunction("datepart(hour, {0})")); Methods.Add("now", new TemplateFunction("getUtcDate()")); + SqlMapper.AddTypeMap(typeof(uint), DbType.Int32);// Fixed System.ArgumentException : No mapping exists from DbType UInt32 to a known SqlDbType. // These are not necessary since SQL Server 2008 //Methods.Add("day", new TemplateFunction("datepart(day, {0})")); diff --git a/test/YesSql.Tests/CoreTests.cs b/test/YesSql.Tests/CoreTests.cs index f61d05ca..cd2e83ae 100644 --- a/test/YesSql.Tests/CoreTests.cs +++ b/test/YesSql.Tests/CoreTests.cs @@ -34,7 +34,7 @@ public abstract class CoreTests : IAsyncLifetime protected abstract IConfiguration CreateConfiguration(); - public CoreTests(ITestOutputHelper output) + protected CoreTests(ITestOutputHelper output) { _output = output; } @@ -65,7 +65,7 @@ public async Task InitializeAsync() ClearTables(_configuration); } - public virtual Task DisposeAsync() + public Task DisposeAsync() { return Task.CompletedTask; } @@ -84,6 +84,9 @@ protected virtual void CleanDatabase(IConfiguration configuration, bool throwOnE builder.DropReduceIndexTable(); builder.DropMapIndexTable(); builder.DropMapIndexTable(); + builder.DropMapIndexTable(); + builder.DropMapIndexTable(); + builder.DropMapIndexTable(); builder.DropMapIndexTable(); builder.DropMapIndexTable(); builder.DropMapIndexTable(); @@ -212,61 +215,82 @@ public void CreateTables(IConfiguration configuration) var builder = new SchemaBuilder(configuration, transaction); builder.CreateReduceIndexTable(column => column - .Column(nameof(ArticlesByDay.Count)) - .Column(nameof(ArticlesByDay.DayOfYear)) - ); + .Column(nameof(ArticlesByDay.Count)) + .Column(nameof(ArticlesByDay.DayOfYear)) + ); builder.CreateReduceIndexTable(column => column - .Column(nameof(AttachmentByDay.Count)) - .Column(nameof(AttachmentByDay.Date)) - ); + .Column(nameof(AttachmentByDay.Count)) + .Column(nameof(AttachmentByDay.Date)) + ); builder.CreateReduceIndexTable(column => column - .Column(nameof(UserByRoleNameIndex.Count)) - .Column(nameof(UserByRoleNameIndex.RoleName)) - ); + .Column(nameof(UserByRoleNameIndex.Count)) + .Column(nameof(UserByRoleNameIndex.RoleName)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(ArticleByPublishedDate.PublishedDateTime)) - .Column(nameof(ArticleByPublishedDate.Title)) - ); + .Column(nameof(ArticleByPublishedDate.PublishedDateTime)) + .Column(nameof(ArticleByPublishedDate.Title)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(PersonByName.SomeName)) - ); + .Column(nameof(PersonByName.SomeName)) + ); + + builder.CreateMapIndexTable(column => column + .Column(nameof(FileInfoIndex.FileId)) + .Column(nameof(FileInfoIndex.Revision)) + ); + + builder.CreateMapIndexTable(column => column + .Column(nameof(CustomerById.CustomerId)) + .Column(nameof(CustomerById.OrderId)) + .Column(nameof(CustomerById.Active)) + .Column(nameof(CustomerById.Version)) + ); + + builder.CreateMapIndexTable(column => column + .Column(nameof(CustomerByCreationDate.CustomerId)) + .Column(nameof(CustomerByCreationDate.OrderId)) + .Column(nameof(CustomerByCreationDate.Active)) + .Column(nameof(CustomerByCreationDate.Version)) + ); + builder.AlterIndexTable(table => table + .AddColumn(nameof(CustomerByCreationDate.CreationDate))); builder.CreateMapIndexTable(column => column - .Column(nameof(CarIndex.Name)) - .Column(nameof(CarIndex.Category)) - ); + .Column(nameof(CarIndex.Name)) + .Column(nameof(CarIndex.Category)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(PersonByNameCol.Name)) - ); + .Column(nameof(PersonByNameCol.Name)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(PersonIdentity.Identity)) - ); + .Column(nameof(PersonIdentity.Identity)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(PersonByAge.Age)) - .Column(nameof(PersonByAge.Adult)) - .Column(nameof(PersonByAge.Name)) - ); + .Column(nameof(PersonByAge.Age)) + .Column(nameof(PersonByAge.Adult)) + .Column(nameof(PersonByAge.Name)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(ShapeIndex.Name)) - ); + .Column(nameof(ShapeIndex.Name)) + ); builder.CreateMapIndexTable(column => column - .Column(nameof(PersonByAge.Age), c => c.Nullable()) - ); + .Column(nameof(PersonByAge.Age), c => c.Nullable()) + ); builder.CreateMapIndexTable(column => { }); builder.CreateMapIndexTable(column => column - .Column(nameof(EmailByAttachment.Date)) - .Column(nameof(EmailByAttachment.AttachmentName)) - ); + .Column(nameof(EmailByAttachment.Date)) + .Column(nameof(EmailByAttachment.AttachmentName)) + ); builder.CreateMapIndexTable(column => column .Column(nameof(PropertyIndex.Name), col => col.WithLength(767)) @@ -276,11 +300,11 @@ public void CreateTables(IConfiguration configuration) ); builder.CreateMapIndexTable(column => column - .Column(nameof(Binary.Content1), c => c.WithLength(255)) - .Column(nameof(Binary.Content2), c => c.WithLength(8000)) - .Column(nameof(Binary.Content3), c => c.WithLength(65535)) - .Column(nameof(Binary.Content4), c => c.WithLength(1)) - ); + .Column(nameof(Binary.Content1), c => c.WithLength(255)) + .Column(nameof(Binary.Content2), c => c.WithLength(8000)) + .Column(nameof(Binary.Content3), c => c.WithLength(65535)) + .Column(nameof(Binary.Content4), c => c.WithLength(1)) + ); builder.CreateMapIndexTable(column => column .Column(nameof(TypesIndex.ValueBool)) @@ -315,29 +339,29 @@ public void CreateTables(IConfiguration configuration) //.Column(nameof(TypesIndex.NullableUInt), c => c.Nullable()) //.Column(nameof(TypesIndex.NullableULong), c => c.Nullable()) //.Column(nameof(TypesIndex.NullableUShort), c => c.Nullable()) - ); + ); builder.CreateMapIndexTable(column => column .Column(nameof(PersonByName.SomeName)), - "Col1" - ); + "Col1" + ); builder.CreateMapIndexTable(column => column .Column(nameof(PersonByNameCol.Name)), - "Col1" - ); + "Col1" + ); builder.CreateMapIndexTable(column => column .Column(nameof(PersonByBothNamesCol.Firstname)) .Column(nameof(PersonByBothNamesCol.Lastname)), - "Col1" - ); + "Col1" + ); builder.CreateReduceIndexTable(column => column .Column(nameof(PersonsByNameCol.Name)) .Column(nameof(PersonsByNameCol.Count)), - "Col1" - ); + "Col1" + ); transaction.Commit(); } @@ -345,11 +369,9 @@ public void CreateTables(IConfiguration configuration) [Fact] public void ShouldCreateDatabase() { - using (var session = _store.CreateSession()) - { - var doc = new Product(); - session.Save(doc); - } + using var session = _store.CreateSession(); + var doc = new Product(); + session.Save(doc); } [Fact] @@ -456,25 +478,23 @@ public async Task ShouldSaveSeveralObjects() [Fact] public async Task ShouldSaveAnonymousObject() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new { - var bill = new - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - var steve = new - { - Firstname = "Steve", - Lastname = "Balmer" - }; + var steve = new + { + Firstname = "Steve", + Lastname = "Balmer" + }; - session.Save(bill); - session.Save(steve); + session.Save(bill); + session.Save(steve); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); } [Fact] @@ -515,11 +535,9 @@ public async Task ShouldLoadAnonymousDocument() [Fact] public async Task ShouldQueryNonExistentResult() { - using (var session = _store.CreateSession()) - { - var person = await session.Query().FirstOrDefaultAsync(); - Assert.Null(person); - } + using var session = _store.CreateSession(); + var person = await session.Query().FirstOrDefaultAsync(); + Assert.Null(person); } [Fact] @@ -551,6 +569,7 @@ public async Task ShouldUpdateNewDocument() public async Task ShouldQueryIndexWithParameter() { _store.RegisterIndexes(); + var dialect = _store.Configuration.SqlDialect; using (var session = _store.CreateSession()) { @@ -561,10 +580,10 @@ public async Task ShouldQueryIndexWithParameter() using (var session = _store.CreateSession()) { - var person = await session.QueryIndex().Where(d => d.QuoteForColumnName(nameof(PersonByName.SomeName)) + " = @Name").WithParameter("Name", "Bill").FirstOrDefaultAsync(); + var person = await session.QueryIndex().Where(d => d.QuoteForColumnName(nameof(PersonByName.SomeName)) + $" = {dialect.QuoteForParameter("Name")}").WithParameter("Name", "Bill").FirstOrDefaultAsync(); Assert.NotNull(person); - Assert.Equal("Bill", (string)person.SomeName); + Assert.Equal("Bill", person.SomeName); } } @@ -754,6 +773,7 @@ public async Task ShouldCompareWithConstants() public async Task ShouldQueryDocumentWithParameter() { _store.RegisterIndexes(); + var dialect = _store.Configuration.SqlDialect; using (var session = _store.CreateSession()) { @@ -765,10 +785,10 @@ public async Task ShouldQueryDocumentWithParameter() using (var session = _store.CreateSession()) { - var person = await session.Query().Where(d => d.QuoteForColumnName(nameof(PersonByName.SomeName)) + " = @Name").WithParameter("Name", "Bill").FirstOrDefaultAsync(); + var person = await session.Query().Where(d => d.QuoteForColumnName(nameof(PersonByName.SomeName)) + $" = {dialect.QuoteForParameter("Name")}").WithParameter("Name", "Bill").FirstOrDefaultAsync(); Assert.NotNull(person); - Assert.Equal("Bill", (string)person.Firstname); + Assert.Equal("Bill", person.Firstname); } } @@ -844,60 +864,54 @@ public async Task ShouldSerializeComplexObject() [Fact] public async Task ShouldAssignIdWhenSaved() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - Assert.True(bill.Id == 0); - session.Save(bill); - Assert.True(bill.Id != 0); + Assert.True(bill.Id == 0); + session.Save(bill); + Assert.True(bill.Id != 0); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); } [Fact] public async Task ShouldAutoFlushOnGet() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - session.Save(bill); - var newBill = await session.GetAsync(bill.Id); + session.Save(bill); + var newBill = await session.GetAsync(bill.Id); - Assert.Same(newBill, bill); + Assert.Same(newBill, bill); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); } [Fact] public async Task ShouldKeepTrackedOnAutoFlush() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - session.Save(bill); - var newBill = await session.GetAsync(bill.Id); + session.Save(bill); + var newBill = await session.GetAsync(bill.Id); - Assert.Same(newBill, bill); + Assert.Same(newBill, bill); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); } [Fact] @@ -926,25 +940,23 @@ public async Task NoSavingChangesShouldRollbackAutoFlush() [Fact] public async Task ShouldKeepIdentityMapOnCommitAsync() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - session.Save(bill); - var newBill = await session.GetAsync(bill.Id); + session.Save(bill); + var newBill = await session.GetAsync(bill.Id); - Assert.Equal(bill, newBill); + Assert.Equal(bill, newBill); - await session.SaveChangesAsync(); + await session.SaveChangesAsync(); - newBill = await session.GetAsync(bill.Id); + newBill = await session.GetAsync(bill.Id); - Assert.Equal(bill, newBill); - } + Assert.Equal(bill, newBill); } [Fact] @@ -1207,10 +1219,8 @@ public virtual async Task ShouldRunCompiledQueriesConcurrently() { while (!stopping && Interlocked.Add(ref counter, 1) < MaxTransactions) { - using (var session = _store.CreateSession()) - { - Assert.Equal(2, await session.ExecuteQuery(new PersonByNameOrAgeQuery(0, "Bill")).CountAsync()); - } + using var session = _store.CreateSession(); + Assert.Equal(2, await session.ExecuteQuery(new PersonByNameOrAgeQuery(0, "Bill")).CountAsync()); } })).ToList(); @@ -1978,7 +1988,7 @@ public async Task ShouldClearOrders() Assert.Equal("Steve", (await query.FirstOrDefaultAsync()).Firstname); } } - + [Fact] public async Task ShouldJoinReduceIndex() { @@ -2674,17 +2684,15 @@ public async Task ChangesAreAutoFlushed() { _store.RegisterIndexes(); - using (var session = _store.CreateSession()) - { - var d1 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; - var d2 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; + using var session = _store.CreateSession(); + var d1 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; + var d2 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; - session.Save(d1); - session.Save(d2); + session.Save(d1); + session.Save(d2); - var articles = session.Query(x => x.DayOfYear == 305); - Assert.Equal(2, await articles.CountAsync()); - } + var articles = session.Query(x => x.DayOfYear == 305); + Assert.Equal(2, await articles.CountAsync()); } [Fact] @@ -2692,22 +2700,20 @@ public async Task AutoflushCanHappenMultipleTimes() { _store.RegisterIndexes(); - using (var session = _store.CreateSession()) - { - var d1 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; - var d2 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; + using var session = _store.CreateSession(); + var d1 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; + var d2 = new Article { PublishedUtc = new DateTime(2011, 11, 1) }; - session.Save(d1); - session.Save(d2); + session.Save(d1); + session.Save(d2); - var articles = await session.Query(x => x.DayOfYear == 305).ListAsync(); + var articles = await session.Query(x => x.DayOfYear == 305).ListAsync(); - d1.PublishedUtc = new DateTime(2011, 11, 2); + d1.PublishedUtc = new DateTime(2011, 11, 2); - articles = await session.Query(x => x.DayOfYear == 306).ListAsync(); + articles = await session.Query(x => x.DayOfYear == 306).ListAsync(); - Assert.Single(articles); - } + Assert.Single(articles); } [Fact] @@ -2844,7 +2850,7 @@ public async Task ShouldPageResults() for (var i = 1; i < ordered.Count; i++) { - Assert.Equal(1, String.Compare(ordered[i].SomeName, ordered[i - 1].SomeName)); + Assert.Equal(1, string.Compare(ordered[i].SomeName, ordered[i - 1].SomeName)); } } @@ -2860,7 +2866,7 @@ public async Task ShouldPageResults() for (var i = 1; i < ordered.Count; i++) { - Assert.Equal(-1, String.Compare(ordered[i].SomeName, ordered[i - 1].SomeName)); + Assert.Equal(-1, string.Compare(ordered[i].SomeName, ordered[i - 1].SomeName)); } } @@ -2876,7 +2882,7 @@ public async Task ShouldPageResults() for (var i = 1; i < ordered.Count; i++) { - Assert.Equal(1, String.Compare(ordered[i].SomeName, ordered[i - 1].SomeName)); + Assert.Equal(1, string.Compare(ordered[i].SomeName, ordered[i - 1].SomeName)); } } } @@ -3107,18 +3113,16 @@ public async Task ShouldQueryMultipleByReducedIndex() [Fact] public async Task ShouldSaveBigDocuments() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = new String('x', 10000), - }; + Firstname = new string('x', 10000), + }; - session.Save(bill); + session.Save(bill); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); } [Fact] @@ -4507,16 +4511,19 @@ public virtual async Task ShouldGateQuery() { while (!stopping && Interlocked.Add(ref counter, 1) < MaxTransactions) { - using (var session = _store.CreateSession()) - { - await session.Query().For().With().ListAsync(); - await session.Query().For().With(x => x.SomeName == "Steve").ListAsync(); - await session.Query().For().With().Where(x => x.SomeName == "Steve").ListAsync(); - } + using var session = _store.CreateSession(); + await session.Query().For().With().ListAsync(); + await session.Query().For().With(x => x.SomeName == "Steve").ListAsync(); + await session.Query().For().With().Where(x => x.SomeName == "Steve").ListAsync(); } })).ToList(); + await Task.Delay(TimeSpan.FromSeconds(3)); + + tasks.Add(Task.Delay(TimeSpan.FromSeconds(3))); + + await Task.WhenAny(tasks); // Flushing tasks stopping = true; @@ -4529,21 +4536,19 @@ public virtual async Task ShouldGateQuery() // Gated queries swGated.Restart(); - tasks = Enumerable.Range(1, concurrency).Select(i => Task.Run(async () => { + swGated.Start(); while (!stopping && Interlocked.Add(ref counter, 1) < MaxTransactions) { - using (var session = _store.CreateSession()) - { - await session.Query().For().With().ListAsync(); - await session.Query().For().With(x => x.SomeName == "Steve").ListAsync(); - await session.Query().For().With().Where(x => x.SomeName == "Steve").ListAsync(); - } + using var session = _store.CreateSession(); + await session.Query().For().With().ListAsync(); + await session.Query().For().With(x => x.SomeName == "Steve").ListAsync(); + await session.Query().For().With().Where(x => x.SomeName == "Steve").ListAsync(); } - gatedCounter = counter; swGated.Stop(); + })).ToList(); await Task.Delay(TimeSpan.FromSeconds(3)); @@ -4564,17 +4569,14 @@ public virtual async Task ShouldGateQuery() tasks = Enumerable.Range(1, concurrency).Select(i => Task.Run(async () => { + swNonGated.Start(); while (!stopping && Interlocked.Add(ref counter, 1) < MaxTransactions) { - using (var session = _store.CreateSession()) - { - await session.Query().For().With().ListAsync(); - await session.Query().For().With(x => x.SomeName == "Steve").ListAsync(); - await session.Query().For().With().Where(x => x.SomeName == "Steve").ListAsync(); - } + using var session = _store.CreateSession(); + await session.Query().For().With().ListAsync(); + await session.Query().For().With(x => x.SomeName == "Steve").ListAsync(); + await session.Query().For().With().Where(x => x.SomeName == "Steve").ListAsync(); } - - nonGatedCounter = counter; swNonGated.Stop(); })).ToList(); @@ -4638,7 +4640,7 @@ public virtual async Task ShouldConvertDateTimeToUtc() } [Fact] - public async Task ShouldOrderCaseInsensitively() + public virtual async Task ShouldOrderCaseInsensitively() { _store.RegisterIndexes(); @@ -4955,26 +4957,25 @@ public async Task ShouldGenerateIdsConcurrently(string collection) [Fact] public async Task ShouldNotDuplicateCommandsWhenCommitFails() { - - using (var session = (Session)_store.CreateSession()) + using var session = (Session)_store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - session.Save(bill); + session.Save(bill); - // Create a command that will throw an exception - // Use an order of 4 so it will be at the end of the list - session._commands = new List(); - session._commands.Add(new FailingCommand(new Document())); + // Create a command that will throw an exception + // Use an order of 4 so it will be at the end of the list + session._commands = new List + { + new FailingCommand(new Document()) + }; - await Assert.ThrowsAnyAsync(async () => await session.SaveChangesAsync()); + await Assert.ThrowsAnyAsync(async () => await session.SaveChangesAsync()); - Assert.Null(session._commands); - } + Assert.Null(session._commands); } [Fact] @@ -5082,86 +5083,80 @@ public virtual void ShouldRenameColumn() var column2 = "Column2"; var value = "Value"; - using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) - { - connection.Open(); - - try - { - using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) - { + using var connection = _store.Configuration.ConnectionFactory.CreateConnection(); + connection.Open(); - var builder = new SchemaBuilder(_store.Configuration, transaction); + try + { + using var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel); + var builder = new SchemaBuilder(_store.Configuration, transaction); - builder.DropTable(table); + builder.DropTable(table); - transaction.Commit(); - } - } - catch - { - // Do nothing if the table can't be dropped - } - - using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) - { + transaction.Commit(); + } + catch + { + // Do nothing if the table can't be dropped + } - var builder = new SchemaBuilder(_store.Configuration, transaction); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { - builder.CreateTable(table, column => column - .Column(column1) - ); + var builder = new SchemaBuilder(_store.Configuration, transaction); - var sqlInsert = String.Format("INSERT INTO {0} ({1}) VALUES({2})", - _store.Configuration.SqlDialect.QuoteForTableName(prefixedTable), - _store.Configuration.SqlDialect.QuoteForColumnName(column1), - _store.Configuration.SqlDialect.GetSqlValue(value) - ); + builder.CreateTable(table, column => column + .Column(column1) + ); - connection.Execute(sqlInsert, transaction: transaction); + var sqlInsert = string.Format("INSERT INTO {0} ({1}) VALUES({2})", + _store.Configuration.SqlDialect.QuoteForTableName(prefixedTable), + _store.Configuration.SqlDialect.QuoteForColumnName(column1), + _store.Configuration.SqlDialect.GetSqlValue(value) + ); - transaction.Commit(); - } + connection.Execute(sqlInsert, transaction: transaction); - using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) - { - var sqlSelect = String.Format("SELECT {0} FROM {1}", - _store.Configuration.SqlDialect.QuoteForColumnName(column1), - _store.Configuration.SqlDialect.QuoteForTableName(prefixedTable) - ); + transaction.Commit(); + } - var result = connection.Query(sqlSelect, transaction: transaction).FirstOrDefault(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var sqlSelect = string.Format("SELECT {0} FROM {1}", + _store.Configuration.SqlDialect.QuoteForColumnName(column1), + _store.Configuration.SqlDialect.QuoteForTableName(prefixedTable) + ); - Assert.Equal(value, result.Column1); + var result = connection.Query(sqlSelect, transaction: transaction).FirstOrDefault(); - transaction.Commit(); - } + Assert.Equal(value, result.Column1); - using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) - { - var builder = new SchemaBuilder(_store.Configuration, transaction); + transaction.Commit(); + } - builder.AlterTable(table, column => column - .RenameColumn(column1, column2) - ); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); - transaction.Commit(); - } + builder.AlterTable(table, column => column + .RenameColumn(column1, column2) + ); - using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) - { - var sqlSelect = String.Format("SELECT {0} FROM {1}", - _store.Configuration.SqlDialect.QuoteForColumnName(column2), - _store.Configuration.SqlDialect.QuoteForTableName(prefixedTable) - ); + transaction.Commit(); + } - var result = connection.Query(sqlSelect, transaction: transaction).FirstOrDefault(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var sqlSelect = string.Format("SELECT {0} FROM {1}", + _store.Configuration.SqlDialect.QuoteForColumnName(column2), + _store.Configuration.SqlDialect.QuoteForTableName(prefixedTable) + ); - Assert.Equal(value, result.Column2); + var result = connection.Query(sqlSelect, transaction: transaction).FirstOrDefault(); - transaction.Commit(); - } + Assert.Equal(value, result.Column2); + transaction.Commit(); } } @@ -5215,26 +5210,24 @@ public virtual async Task ShouldHandleConcurrency() await Assert.ThrowsAsync(async () => { - using (var session = _store.CreateSession()) - { - var person = await session.Query().FirstOrDefaultAsync(); - Assert.NotNull(person); + using var session = _store.CreateSession(); + var person = await session.Query().FirstOrDefaultAsync(); + Assert.NotNull(person); - task2Loaded.Set(); + task2Loaded.Set(); - // Wait for the other thread to save the person before updating it - if (!task1Saved.WaitOne(5000)) - { - Assert.True(false, "task1Saved timeout"); - await session.CancelAsync(); - } + // Wait for the other thread to save the person before updating it + if (!task1Saved.WaitOne(5000)) + { + Assert.True(false, "task1Saved timeout"); + await session.CancelAsync(); + } - person.Lastname = "Doors"; - session.Save(person, true); - Assert.NotNull(person); + person.Lastname = "Doors"; + session.Save(person, true); + Assert.NotNull(person); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); }); }); @@ -5339,28 +5332,26 @@ public async Task ShouldReloadDetachedObject() [Fact] public async Task ShouldDetachEntity() { - using (var session = _store.CreateSession()) + using var session = _store.CreateSession(); + var bill = new Person { - var bill = new Person - { - Firstname = "Bill", - Lastname = "Gates" - }; + Firstname = "Bill", + Lastname = "Gates" + }; - session.Save(bill); + session.Save(bill); - var newBill = await session.GetAsync(bill.Id); + var newBill = await session.GetAsync(bill.Id); - Assert.Equal(bill, newBill); + Assert.Equal(bill, newBill); - session.Detach(bill); + session.Detach(bill); - newBill = await session.GetAsync(bill.Id); + newBill = await session.GetAsync(bill.Id); - Assert.NotEqual(bill, newBill); + Assert.NotEqual(bill, newBill); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); } [Fact] @@ -5686,18 +5677,16 @@ public virtual async Task ShouldDetectVersionHaschanged() // User A submits the changes await Assert.ThrowsAsync(async () => { - using (var session = _store.CreateSession()) - { - var person = await session.Query().FirstOrDefaultAsync(); - Assert.NotNull(person); + using var session = _store.CreateSession(); + var person = await session.Query().FirstOrDefaultAsync(); + Assert.NotNull(person); - person.Version = viewModel.Version; - person.Firstname = viewModel.Firstname; + person.Version = viewModel.Version; + person.Firstname = viewModel.Firstname; - session.Save(person); + session.Save(person); - await session.SaveChangesAsync(); - } + await session.SaveChangesAsync(); }); // Changes should not have been persisted @@ -5714,9 +5703,9 @@ await Assert.ThrowsAsync(async () => [ClassData(typeof(DecimalPrecisionAndScaleDataGenerator))] public void SqlDecimalPrecisionAndScale(byte? precision, byte? scale) { - string expected = string.Format(DecimalColumnDefinitionFormatString, precision ?? _store.Configuration.SqlDialect.DefaultDecimalPrecision, scale ?? _store.Configuration.SqlDialect.DefaultDecimalScale); + var expected = string.Format(DecimalColumnDefinitionFormatString, precision ?? _store.Configuration.SqlDialect.DefaultDecimalPrecision, scale ?? _store.Configuration.SqlDialect.DefaultDecimalScale); - string result = _store.Configuration.SqlDialect.GetTypeName(DbType.Decimal, null, precision, scale); + var result = _store.Configuration.SqlDialect.GetTypeName(DbType.Decimal, null, precision, scale); Assert.Equal(expected, result); } @@ -5754,7 +5743,7 @@ public async Task ShouldUpdateVersions() } [Fact] - public async Task AllDataTypesShouldBeStored() + public virtual async Task AllDataTypesShouldBeStored() { var dummy = new Person(); @@ -5774,13 +5763,14 @@ public async Task AllDataTypesShouldBeStored() using (var session = _store.CreateSession()) { - var index = new TypesIndex(); - - index.ValueDateTime = valueDateTime; - index.ValueGuid = valueGuid; - index.ValueBool = valueBool; - index.ValueDateTimeOffset = valueDateTimeOffset; - index.ValueTimeSpan = valueTimeSpan; + var index = new TypesIndex + { + ValueDateTime = valueDateTime, + ValueGuid = valueGuid, + ValueBool = valueBool, + ValueDateTimeOffset = valueDateTimeOffset, + ValueTimeSpan = valueTimeSpan + }; ((IIndex)index).AddDocument(new Document { Id = dummy.Id }); @@ -5812,7 +5802,7 @@ public async Task AllDataTypesShouldBeStored() } [Fact] - public async Task AllDataTypesShouldBeQueryableWithProperties() + public virtual async Task AllDataTypesShouldBeQueryableWithProperties() { var dummy = new Person(); @@ -5832,13 +5822,14 @@ public async Task AllDataTypesShouldBeQueryableWithProperties() using (var session = _store.CreateSession()) { - var index = new TypesIndex(); - - index.ValueDateTime = valueDateTime; - index.ValueGuid = valueGuid; - index.ValueBool = valueBool; - index.ValueDateTimeOffset = valueDateTimeOffset; - index.ValueTimeSpan = valueTimeSpan; + var index = new TypesIndex + { + ValueDateTime = valueDateTime, + ValueGuid = valueGuid, + ValueBool = valueBool, + ValueDateTimeOffset = valueDateTimeOffset, + ValueTimeSpan = valueTimeSpan + }; ((IIndex)index).AddDocument(new Document { Id = dummy.Id }); @@ -5876,7 +5867,7 @@ public async Task AllDataTypesShouldBeQueryableWithProperties() } [Fact] - public async Task AllDataTypesShouldBeQueryableWithConstants() + public virtual async Task AllDataTypesShouldBeQueryableWithConstants() { var dummy = new Person(); @@ -5896,13 +5887,14 @@ public async Task AllDataTypesShouldBeQueryableWithConstants() using (var session = _store.CreateSession()) { - var index = new TypesIndex(); - - index.ValueDateTime = valueDateTime; - index.ValueGuid = valueGuid; - index.ValueBool = valueBool; - index.ValueDateTimeOffset = valueDateTimeOffset; - index.ValueTimeSpan = valueTimeSpan; + var index = new TypesIndex + { + ValueDateTime = valueDateTime, + ValueGuid = valueGuid, + ValueBool = valueBool, + ValueDateTimeOffset = valueDateTimeOffset, + ValueTimeSpan = valueTimeSpan + }; ((IIndex)index).AddDocument(new Document { Id = dummy.Id }); @@ -5918,12 +5910,11 @@ public async Task AllDataTypesShouldBeQueryableWithConstants() { // Ensure that query builing is also converting constants var index = await session.QueryIndex(x => - x.ValueBool == false - && x.ValueDateTime == new DateTime(2021, 1, 20) - && x.ValueDateTimeOffset == new DateTimeOffset(new DateTime(2021, 1, 20), new TimeSpan(1, 2, 0)) - && x.ValueTimeSpan == new TimeSpan(1, 2, 3, 4, 5) - && x.ValueGuid == Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e") - ).FirstOrDefaultAsync(); + x.ValueBool == false + && x.ValueDateTime == new DateTime(2021, 1, 20) + && x.ValueDateTimeOffset == new DateTimeOffset(valueDateTime, new TimeSpan(1, 2, 0)) + && x.ValueTimeSpan == new TimeSpan(1, 2, 3, 4, 5) + && x.ValueGuid == Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e")).FirstOrDefaultAsync(); Assert.Equal(valueDateTime, index.ValueDateTime); Assert.Equal(valueGuid, index.ValueGuid); @@ -6060,6 +6051,146 @@ public async Task SameQueryShouldBeReusable() Assert.NotNull(await queryIndex.FirstOrDefaultAsync()); } } + [Fact] + public virtual async Task ShouldUseSeveralIndexesAsync() + { + _store.RegisterIndexes(); + _store.RegisterIndexes(); + + using (var session = _store.CreateSession()) + { + var bill = new Person + { + Firstname = "Bill", + Lastname = "Gates", + Age = 50 + }; + + var elon = new Person + { + Firstname = "Elon", + Lastname = "Musk", + Age = 12 + }; + + var isaac = new Person + { + Firstname = "Isaac", + Lastname = "Newton", + Age = 376 + }; + + session.Save(bill); + session.Save(elon); + session.Save(isaac); + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + var query = session.Query().Where(x => x.Adult == true); + + Assert.Empty(await query.Where(i => i.Name.IsIn(select => select.SomeName, index => index.SomeName == "Elon")).ListAsync()); + + query = session.Query().Where(x => x.Adult == true); + Assert.Single(await query.Where(i => i.Name.IsIn(select => select.SomeName, index => index.SomeName == "Isaac")).ListAsync()); + } + + } + [Fact] + public async Task ShouldOrderThenOnValueType() + { + _store.RegisterIndexes(); + + using (var session = _store.CreateSession()) + { + for (var i = 0; i < 100; i++) + { + var person = new Person + { + Firstname = "Bill" + i, + Lastname = "Gates" + i, + Age = i + }; + + session.Save(person); + } + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + Assert.Equal(100, await session.QueryIndex().CountAsync()); + var orderBy = await session.QueryIndex().OrderBy(x => x.Age).ThenBy(x => x.Name).FirstOrDefaultAsync(); + Assert.Equal(0, orderBy.Age); + Assert.Equal("Bill0", orderBy.Name); + var orderByDesc = await session.QueryIndex().OrderByDescending(x => x.Age).ThenByDescending(x => x.Name).FirstOrDefaultAsync(); + Assert.Equal(99, orderByDesc.Age); + Assert.Equal("Bill99", orderByDesc.Name); + } + } + [Fact] + public virtual async Task ShouldUseUintIndexInfo() + { + _store.RegisterIndexes(); + + using var session = _store.CreateSession(); + var newRecord = new FileInfo + { + ContentType = "application/pdf", + FileId = "122222-987819-0199811", + Revision = 1,//uint + SerializedUserGroups = "[]" + }; + session.Save(newRecord); + await session.SaveChangesAsync(); + } + + [Fact] + public virtual async Task ShouldUseLongIndexInfoAsync() + { + _store.RegisterIndexes(); + + using (var session = _store.CreateSession()) + { + var newRecord = new Customer + { + Name = "Customer1", + CustomerId = 1126777363783373, + OrderId = 738939272898111, + Version = 0, + Active = false + }; + session.Save(newRecord); + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + var document = await session.Query(x => x.CustomerId == 1126777363783373).FirstOrDefaultAsync(); + Assert.Equal("Customer1", document.Name); + } + + using (var session = _store.CreateSession()) + { + var newRecord = new Customer + { + Name = "Customer2", + CustomerId = 879847847981724, + OrderId = null, + Version = 1, + Active = true + }; + session.Save(newRecord); + await session.SaveChangesAsync(); + } + using (var session = _store.CreateSession()) + { + var document = await session.Query(x => x.OrderId == null).FirstOrDefaultAsync(); + Assert.Equal("Customer2", document.Name); + } + } #region FilterTests diff --git a/test/YesSql.Tests/Indexes/CustomerByCreationDate.cs b/test/YesSql.Tests/Indexes/CustomerByCreationDate.cs new file mode 100644 index 00000000..677d0fc3 --- /dev/null +++ b/test/YesSql.Tests/Indexes/CustomerByCreationDate.cs @@ -0,0 +1,31 @@ +using System; +using YesSql.Indexes; +using YesSql.Tests.Models; + +namespace YesSql.Tests.Indexes +{ + public class CustomerByCreationDate : MapIndex + { + public long CustomerId { get; set; } + public long? OrderId { get; set; } + public bool Active { get; set; } + public int Version { get; set; } + public DateTime? CreationDate { get; set; } + + } + public class CustomerByCreationDateProvider : IndexProvider + { + public override void Describe(DescribeContext context) + { + context.For() + .Map(customer => new CustomerByCreationDate + { + CustomerId = customer.CustomerId, + OrderId = customer.OrderId, + Active=customer.Active, + Version=customer.Version, + CreationDate = customer.CreationDate + }); + } + } +} diff --git a/test/YesSql.Tests/Indexes/CustomerById.cs b/test/YesSql.Tests/Indexes/CustomerById.cs new file mode 100644 index 00000000..db8adf2a --- /dev/null +++ b/test/YesSql.Tests/Indexes/CustomerById.cs @@ -0,0 +1,28 @@ +using YesSql.Indexes; +using YesSql.Tests.Models; + +namespace YesSql.Tests.Indexes +{ + public class CustomerById : MapIndex + { + public long CustomerId { get; set; } + public long? OrderId { get; set; } + public bool Active { get; set; } + public int Version { get; set; } + + } + public class CustomerByIdProvider : IndexProvider + { + public override void Describe(DescribeContext context) + { + context.For() + .Map(customer => new CustomerById + { + CustomerId = customer.CustomerId, + OrderId = customer.OrderId, + Active=customer.Active, + Version=customer.Version + }); + } + } +} diff --git a/test/YesSql.Tests/Indexes/FileInfo.cs b/test/YesSql.Tests/Indexes/FileInfo.cs new file mode 100644 index 00000000..4199ec8e --- /dev/null +++ b/test/YesSql.Tests/Indexes/FileInfo.cs @@ -0,0 +1,30 @@ +using YesSql.Indexes; + +namespace YesSql.Tests.Indexes +{ + public class FileInfo + { + public string FileId { get; set; } + public uint Revision { get; set; } + public string ContentType { get; set; } + public string SerializedUserGroups { get; set; } + } + public class FileInfoIndex : MapIndex + { + public string FileId { get; set; } + public uint Revision { get; set; } + } + + public class FileIndexProvider : IndexProvider + { + public override void Describe(DescribeContext context) + { + context.For() + .Map(fileInfo => new FileInfoIndex + { + FileId = fileInfo.FileId, + Revision = fileInfo.Revision + }); + } + } +} diff --git a/test/YesSql.Tests/Models/Customer.cs b/test/YesSql.Tests/Models/Customer.cs new file mode 100644 index 00000000..fd06b2bd --- /dev/null +++ b/test/YesSql.Tests/Models/Customer.cs @@ -0,0 +1,14 @@ +using System; + +namespace YesSql.Tests.Models +{ + public class Customer + { + public string Name { get; set; } + public long CustomerId { get; set; } + public long? OrderId { get; set; } + public bool Active { get; set; } + public int Version { get; set; } + public DateTime? CreationDate { get; set; } + } +} diff --git a/test/YesSql.Tests/OracleTests.cs b/test/YesSql.Tests/OracleTests.cs new file mode 100644 index 00000000..ba8b47aa --- /dev/null +++ b/test/YesSql.Tests/OracleTests.cs @@ -0,0 +1,549 @@ +using System; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using YesSql.Commands; +using YesSql.Indexes; +using YesSql.Provider.Oracle; +using YesSql.Sql; +using YesSql.Tests.Indexes; +using YesSql.Tests.Models; +using Xunit; +using Xunit.Abstractions; + +namespace YesSql.Tests +{ + public class OracleTests : CoreTests + { + private const string OracleAllSupportedTypesTableName = "OracleAllSupportedTypes"; + private const string ModifyColumnTestTableName = "ModifyColumnTest"; + private const string OracleDifferentLengthTableName = "OracleDifferentLength"; + private const string DropIndexTestTableName = "DropIndexTest"; + + public static string ConnectionString => Environment.GetEnvironmentVariable("ORACLE_CONNECTION_STRING") ?? @"Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orcl.isd.dp.ua)));User Id=yessql_test;Password=admin123;"; + protected override IConfiguration CreateConfiguration() + { + return new Configuration() + .UseOracle(ConnectionString) + .SetTablePrefix(TablePrefix) + .UseBlockIdGenerator() + ; + } + + protected override string DecimalColumnDefinitionFormatString => "number({0},{1})"; + + public OracleTests(ITestOutputHelper output) : base(output) + { + } + + protected override void OnCleanDatabase(SchemaBuilder builder, DbTransaction transaction) + { + base.OnCleanDatabase(builder, transaction); + + try + { + builder.DropTable("Content"); + } + catch + { + // ignored + } + + try + { + builder.DropTable("Collection1_Content"); + } + catch + { + // ignored + } + + try + { + builder.DropTable(OracleAllSupportedTypesTableName); + } + catch + { + // ignored + } + + try + { + builder.DropTable(ModifyColumnTestTableName); + } + catch + { + // ignored + } + + try + { + builder.DropTable(OracleDifferentLengthTableName); + } + catch + { + // ignored + } + try + { + builder.DropTable(DropIndexTestTableName); + } + catch + { + // ignored + } + + } + + [Fact] + public void ShouldCreateTablesWithDifferentStringLenth() + { + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.CreateTable(OracleDifferentLengthTableName, column => column + .Column("default") + .Column("255", c => c.WithLength(255)) + .Column("2000", c => c.WithLength(2000)) + .Column("2001", c => c.WithLength(2001)) + .Column("4000", c => c.WithLength(4000)) + .Column("16777216", c => c.WithLength(16777216))); + + transaction.Commit(); + } + } + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var sqlForDataTypes = $"SELECT column_name as \"ColumnName\", data_type as \"DataType\" " + + $" FROM all_tab_columns where table_name = \'{TablePrefix}{OracleDifferentLengthTableName}\'"; + var dataTypes = connection.Query(sqlForDataTypes).ToList(); + + Assert.Equal(Dapper.Oracle.OracleMappingType.NVarchar2, dataTypes.FirstOrDefault(dt => dt.ColumnName == "default")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.NVarchar2, dataTypes.FirstOrDefault(dt => dt.ColumnName == "255")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.NVarchar2, dataTypes.FirstOrDefault(dt => dt.ColumnName == "2000")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.NClob, dataTypes.FirstOrDefault(dt => dt.ColumnName == "2001")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.NClob, dataTypes.FirstOrDefault(dt => dt.ColumnName == "4000")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.NClob, dataTypes.FirstOrDefault(dt => dt.ColumnName == "16777216")?.OracleMappingType); + + transaction.Commit(); + } + } + + + + } + [Fact] + public void ShouldCreateTablesAllSupportedTypes() + { + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.CreateTable(OracleAllSupportedTypesTableName, column => column + .Column("blob") + .Column("timestamp") + .Column("number(1)") + .Column("number") + .Column("binary_float") + .Column("binary_double") + .Column("number(5,0)") + .Column("number(9,0)") + .Column("number(19,0)") + .Column("unumber(5,0)") + .Column("unumber(9,0)") + .Column("unumber(19,0)") + .Column("nvarchar2(255)") + .Column("integer")); + transaction.Commit(); + } + } + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var sqlForDataTypes = $"SELECT column_name as \"ColumnName\", data_type as \"DataType\" " + + $" FROM all_tab_columns where table_name = \'{TablePrefix}{OracleAllSupportedTypesTableName}\'"; + var dataTypes = connection.Query(sqlForDataTypes); + + Assert.Equal(Dapper.Oracle.OracleMappingType.Blob, dataTypes.FirstOrDefault(dt => dt.ColumnName == "blob")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.TimeStamp, dataTypes.FirstOrDefault(dt => dt.ColumnName == "timestamp")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "number(1)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "number")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "number(5,0)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "number(9,0)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "number(19,0)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "unumber(5,0)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "unumber(9,0)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "unumber(19,0)")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.Int32, dataTypes.FirstOrDefault(dt => dt.ColumnName == "integer")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.BinaryFloat, dataTypes.FirstOrDefault(dt => dt.ColumnName == "binary_float")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.BinaryDouble, dataTypes.FirstOrDefault(dt => dt.ColumnName == "binary_double")?.OracleMappingType); + Assert.Equal(Dapper.Oracle.OracleMappingType.NVarchar2, dataTypes.FirstOrDefault(dt => dt.ColumnName == "nvarchar2(255)")?.OracleMappingType); + } + } + } + + [Fact] + public async Task ShouldDropIndex() + { + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.CreateTable(DropIndexTestTableName, column => column + .Column("Test") + .Column("integer")); + + transaction.Commit(); + } + } + + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.AlterTable(DropIndexTestTableName, table => table + .CreateIndex("IDX_Index", "Test")); + transaction.Commit(); + var sqlForDataTypes = $"SELECT count(*) FROM all_indexes WHERE table_name = '{TablePrefix}{DropIndexTestTableName}\'"; + Assert.Equal(1, await connection.QuerySingleAsync(sqlForDataTypes)); + } + } + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.AlterTable(DropIndexTestTableName, table => table + .DropIndex("IDX_Index")); + transaction.Commit(); + + var sqlForDataTypes = $"SELECT count(*) FROM all_indexes WHERE table_name = '{TablePrefix}{DropIndexTestTableName}\'"; + Assert.Equal(0, await connection.QuerySingleAsync(sqlForDataTypes)); + } + } + } + + [Fact] + public async Task ShouldModifyColumn() + { + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.CreateTable(ModifyColumnTestTableName, column => column + .Column("Test")); + + transaction.Commit(); + } + } + + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.AlterTable(ModifyColumnTestTableName, table => table + .AlterColumn("Test", column => column.WithType(typeof(int), 1))); + transaction.Commit(); + var sqlForDataTypes = $"SELECT column_name as \"ColumnName\", data_type as \"DataType\" " + + $" FROM all_tab_columns where table_name = \'{TablePrefix}{ModifyColumnTestTableName}\'"; + var dataTypes = await connection.QueryAsync(sqlForDataTypes); + + Assert.Equal("NUMBER", dataTypes.First().DataType); + } + } + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + var ex = Assert.Throws(() => + { + builder.AlterTable(ModifyColumnTestTableName, table => table + .AlterColumn("Test", column => column.WithType(typeof(object), 1, 0))); + transaction.Commit(); + }); + Assert.Equal("Error while executing data migration: you need to specify the field's type in order to change its properties", ex.Message); + } + } + using (var connection = _store.Configuration.ConnectionFactory.CreateConnection()) + { + connection.Open(); + using (var transaction = connection.BeginTransaction(_store.Configuration.IsolationLevel)) + { + var builder = new SchemaBuilder(_store.Configuration, transaction); + + builder.AlterTable(ModifyColumnTestTableName, table => table + .AlterColumn("Test", column => column.WithType(typeof(string), 2).WithDefault("ab"))); + + transaction.Commit(); + var sqlForDataTypes = $"SELECT column_name as \"ColumnName\", data_type as \"DataType\", data_default as \"DataDefault\" " + + $" FROM all_tab_columns where table_name = \'{TablePrefix}{ModifyColumnTestTableName}\'"; + var dataTypes = await connection.QuerySingleAsync(sqlForDataTypes); + + Assert.Equal("NVARCHAR2", dataTypes.DataType); + Assert.Equal("'ab' ", dataTypes.DataDefault); + } + } + } + + [Fact] + public override async Task AllDataTypesShouldBeStored() + { + var dummy = new Person(); + + var valueTimeSpan = new TimeSpan(1, 2, 3, 4, 5); + var valueDateTime = new DateTime(2021, 1, 20); + var valueGuid = Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e"); + var valueBool = false; + var valueDateTimeOffset = new DateTimeOffset(valueDateTime, new TimeSpan(1, 2, 0)); + + // Create fake document to associate to index + using (var session = _store.CreateSession()) + { + session.Save(dummy); + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + var index = new TypesIndex + { + ValueDateTime = valueDateTime, + ValueGuid = valueGuid, + ValueBool = valueBool, + ValueDateTimeOffset = valueDateTimeOffset, + ValueTimeSpan = valueTimeSpan + }; + + ((IIndex)index).AddDocument(new Document { Id = dummy.Id }); + + var connection = await session.CreateConnectionAsync(); + var transaction = await session.BeginTransactionAsync(); + + await new CreateIndexCommand(index, new[] { dummy.Id }, session.Store, "").ExecuteAsync(connection, transaction, session.Store.Configuration.SqlDialect, session.Store.Configuration.Logger); + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + var index = await session.QueryIndex().FirstOrDefaultAsync(); + + Assert.Equal(valueDateTime, index.ValueDateTime); + Assert.Equal(valueGuid, index.ValueGuid); + Assert.Equal(valueBool, index.ValueBool); + // stored as UTC datetime + Assert.Equal(valueDateTimeOffset.UtcDateTime, index.ValueDateTimeOffset.LocalDateTime); + //TODO Oracle Dapper not supported TimeSpan from Ticks converting + //Assert.Equal(valueTimeSpan, index.ValueTimeSpan); + + Assert.Equal(0, index.ValueDecimal); + Assert.Equal(0, index.ValueDouble); + Assert.Equal(0, index.ValueFloat); + Assert.Equal(0, index.ValueInt); + Assert.Equal(0, index.ValueLong); + Assert.Equal(0, index.ValueShort); + } + } + + [Fact] + public override async Task AllDataTypesShouldBeQueryableWithProperties() + { + var dummy = new Person(); + + var valueTimeSpan = new TimeSpan(1, 2, 3, 4, 5); + var valueDateTime = new DateTime(2021, 1, 20); + var valueGuid = Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e"); + var valueBool = false; + var valueDateTimeOffset = new DateTimeOffset(valueDateTime, new TimeSpan(1, 2, 0)); + + // Create fake document to associate to index + using (var session = _store.CreateSession()) + { + session.Save(dummy); + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + var index = new TypesIndex + { + ValueDateTime = valueDateTime, + ValueGuid = valueGuid, + ValueBool = valueBool, + ValueDateTimeOffset = valueDateTimeOffset, + ValueTimeSpan = valueTimeSpan + }; + + ((IIndex)index).AddDocument(new Document { Id = dummy.Id }); + + var connection = await session.CreateConnectionAsync(); + var transaction = await session.BeginTransactionAsync(); + + await new CreateIndexCommand(index, new[] { dummy.Id }, session.Store, "").ExecuteAsync(connection, transaction, session.Store.Configuration.SqlDialect, session.Store.Configuration.Logger); + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + // Ensure that query builing is also converting the values + var index = await session.QueryIndex(x => + x.ValueBool == valueBool + && x.ValueDateTime == valueDateTime + && x.ValueDateTimeOffset == valueDateTimeOffset + && x.ValueTimeSpan == valueTimeSpan + && x.ValueGuid == valueGuid).FirstOrDefaultAsync(); + + Assert.Equal(valueDateTime, index.ValueDateTime); + Assert.Equal(valueGuid, index.ValueGuid); + Assert.Equal(valueBool, index.ValueBool); + // stored as UTC datetime + Assert.Equal(valueDateTimeOffset.UtcDateTime, index.ValueDateTimeOffset.LocalDateTime); + //TODO Oracle Dapper not supported TimeSpan from Ticks converting + //Assert.Equal(valueTimeSpan, index.ValueTimeSpan); + + Assert.Equal(0, index.ValueDecimal); + Assert.Equal(0, index.ValueDouble); + Assert.Equal(0, index.ValueFloat); + Assert.Equal(0, index.ValueInt); + Assert.Equal(0, index.ValueLong); + Assert.Equal(0, index.ValueShort); + } + } + + [Fact] + public override async Task AllDataTypesShouldBeQueryableWithConstants() + { + var dummy = new Person(); + + var valueTimeSpan = new TimeSpan(1, 2, 3, 4, 5); + var valueDateTime = new DateTime(2021, 1, 20); + var valueGuid = Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e"); + var valueBool = false; + var valueDateTimeOffset = new DateTimeOffset(valueDateTime, new TimeSpan(1, 2, 0)); + + // Create fake document to associate to index + using (var session = _store.CreateSession()) + { + session.Save(dummy); + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + var index = new TypesIndex + { + ValueDateTime = valueDateTime, + ValueGuid = valueGuid, + ValueBool = valueBool, + ValueDateTimeOffset = valueDateTimeOffset, + ValueTimeSpan = valueTimeSpan + }; + + ((IIndex)index).AddDocument(new Document { Id = dummy.Id }); + + var connection = await session.CreateConnectionAsync(); + var transaction = await session.BeginTransactionAsync(); + + await new CreateIndexCommand(index, new[] { dummy.Id }, session.Store, "").ExecuteAsync(connection, transaction, session.Store.Configuration.SqlDialect, session.Store.Configuration.Logger); + + await session.SaveChangesAsync(); + } + + using (var session = _store.CreateSession()) + { + // Ensure that query builing is also converting constants + var index = await session.QueryIndex(x => + x.ValueBool == false + && x.ValueDateTime == new DateTime(2021, 1, 20) + && x.ValueDateTimeOffset == new DateTimeOffset(valueDateTime, new TimeSpan(1, 2, 0)) + && x.ValueTimeSpan == new TimeSpan(1, 2, 3, 4, 5) + && x.ValueGuid == Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e")).FirstOrDefaultAsync(); + + Assert.Equal(valueDateTime, index.ValueDateTime); + Assert.Equal(valueGuid, index.ValueGuid); + Assert.Equal(valueBool, index.ValueBool); + // stored as UTC datetime + Assert.Equal(valueDateTimeOffset.UtcDateTime, index.ValueDateTimeOffset.LocalDateTime); + //TODO Oracle Dapper not supported TimeSpan from Ticks converting + //Assert.Equal(valueTimeSpan, index.ValueTimeSpan); + + Assert.Equal(0, index.ValueDecimal); + Assert.Equal(0, index.ValueDouble); + Assert.Equal(0, index.ValueFloat); + Assert.Equal(0, index.ValueInt); + Assert.Equal(0, index.ValueLong); + Assert.Equal(0, index.ValueShort); + } + } + + [Fact(Skip = "Oracle ordered CaseInsensitively only if NLS_COMP = 'LINGUISTIC' and 'NLS_SORT=BINARY_CI' have been set")] + public override async Task ShouldOrderCaseInsensitively() + { + _store.RegisterIndexes(); + + using (var session = _store.CreateSession()) + { + session.Save(new Person { Firstname = "D" }); + session.Save(new Person { Firstname = "b" }); + session.Save(new Person { Firstname = "G" }); + session.Save(new Person { Firstname = "F" }); + session.Save(new Person { Firstname = "c" }); + session.Save(new Person { Firstname = "e" }); + session.Save(new Person { Firstname = "A" }); + } + + using (var session = _store.CreateSession()) + { + var results = await session.Query().OrderBy(x => x.SomeName).ListAsync(); + + Assert.Equal("A", results.ElementAt(0).Firstname); + Assert.Equal("b", results.ElementAt(1).Firstname); + Assert.Equal("c", results.ElementAt(2).Firstname); + Assert.Equal("D", results.ElementAt(3).Firstname); + Assert.Equal("e", results.ElementAt(4).Firstname); + Assert.Equal("F", results.ElementAt(5).Firstname); + Assert.Equal("G", results.ElementAt(6).Firstname); + } + } + + [Fact(Skip = "Oracle only supports read commited or serializable isolation levels. http://download.oracle.com/docs/cd/B19306_01/server.102/b14220/consist.htm#sthref1972")] + public override Task ShouldReadUncommittedRecords() + { + return base.ShouldReadUncommittedRecords(); + } + } +} diff --git a/test/YesSql.Tests/SqlServer2019Tests.cs b/test/YesSql.Tests/SqlServer2019Tests.cs index 0fb2ce4b..c9af56d7 100644 --- a/test/YesSql.Tests/SqlServer2019Tests.cs +++ b/test/YesSql.Tests/SqlServer2019Tests.cs @@ -9,7 +9,7 @@ public class SqlServer2019Tests : SqlServerTests public override string ConnectionString => Environment.GetEnvironmentVariable("SQLSERVER_2019_CONNECTION_STRING") - ?? @"Data Source=.;Initial Catalog=tempdb;Integrated Security=True" + ?? @"Data Source=.\SQLEXPRESS;Initial Catalog=orchard_core_test;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" ; public SqlServer2019Tests(ITestOutputHelper output) : base(output) diff --git a/test/YesSql.Tests/SqliteTests.cs b/test/YesSql.Tests/SqliteTests.cs index d18b8c07..047d667e 100644 --- a/test/YesSql.Tests/SqliteTests.cs +++ b/test/YesSql.Tests/SqliteTests.cs @@ -1,4 +1,3 @@ -using Microsoft.Data.Sqlite; using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -36,24 +35,12 @@ protected override IConfiguration CreateConfiguration() ; } - public override Task DisposeAsync() - { - //SqliteConnection.ClearAllPools(); - return Task.CompletedTask; - } - [Fact(Skip = "ReadCommitted is not supported by Sqlite")] public override Task ShouldReadCommittedRecords() { return base.ShouldReadCommittedRecords(); } - //[Fact(Skip = "Breaks other tests if can't finish during delay")] - //public override Task ShouldGateQuery() - //{ - // return base.ShouldGateQuery(); - //} - [Fact(Skip = "Sqlite doesn't support concurrent writers")] public override Task ShouldReadUncommittedRecords() { diff --git a/test/YesSql.Tests/YesSql.Tests.csproj b/test/YesSql.Tests/YesSql.Tests.csproj index 38508002..71dbe165 100644 --- a/test/YesSql.Tests/YesSql.Tests.csproj +++ b/test/YesSql.Tests/YesSql.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1;net5.0;net6.0 latest @@ -33,6 +33,7 @@ + From a19fb58a96f40753f25e5ecdc0f28cb73826326e Mon Sep 17 00:00:00 2001 From: Mazur Yuriy Date: Fri, 22 Apr 2022 14:37:00 +0300 Subject: [PATCH 2/2] Update OracleTests.cs --- test/YesSql.Tests/OracleTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/YesSql.Tests/OracleTests.cs b/test/YesSql.Tests/OracleTests.cs index ba8b47aa..88b0c540 100644 --- a/test/YesSql.Tests/OracleTests.cs +++ b/test/YesSql.Tests/OracleTests.cs @@ -21,7 +21,7 @@ public class OracleTests : CoreTests private const string OracleDifferentLengthTableName = "OracleDifferentLength"; private const string DropIndexTestTableName = "DropIndexTest"; - public static string ConnectionString => Environment.GetEnvironmentVariable("ORACLE_CONNECTION_STRING") ?? @"Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orcl.isd.dp.ua)));User Id=yessql_test;Password=admin123;"; + public static string ConnectionString => Environment.GetEnvironmentVariable("ORACLE_CONNECTION_STRING") ?? @"Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orcl)));User Id=yessql_test;Password=password;"; protected override IConfiguration CreateConfiguration() { return new Configuration()