diff --git a/Signum.Engine/Administrator.cs b/Signum.Engine/Administrator.cs index d4ee775c62..8eb2fc9d16 100644 --- a/Signum.Engine/Administrator.cs +++ b/Signum.Engine/Administrator.cs @@ -1,6 +1,8 @@ using Signum.Engine; +using Signum.Engine.Engine; using Signum.Engine.Linq; using Signum.Engine.Maps; +using Signum.Engine.PostgresCatalog; using Signum.Engine.SchemaInfoTables; using Signum.Entities; using Signum.Utilities; @@ -8,6 +10,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Data.Common; using System.Linq; using System.Linq.Expressions; @@ -31,7 +34,7 @@ public static void TotalGeneration() public static string GenerateViewCodes(params string[] tableNames) => tableNames.ToString(tn => GenerateViewCode(tn), "\r\n\r\n"); - public static string GenerateViewCode(string tableName) => GenerateViewCode(ObjectName.Parse(tableName)); + public static string GenerateViewCode(string tableName) => GenerateViewCode(ObjectName.Parse(tableName, Schema.Current.Settings.IsPostgres)); public static string GenerateViewCode(ObjectName tableName) { @@ -42,7 +45,7 @@ from c in t.Columns() select new DiffColumn { Name = c.name, - SqlDbType = SchemaSynchronizer.ToSqlDbType(c.Type()!.name), + DbType = new AbstractDbType(SysTablesSchema.ToSqlDbType(c.Type()!.name)), UserTypeName = null, PrimaryKey = t.Indices().Any(i => i.is_primary_key && i.IndexColumns().Any(ic => ic.column_id == c.column_id)), Nullable = c.is_nullable, @@ -102,7 +105,7 @@ public static void CreateTemporaryTable() if (!view.Name.IsTemporal) throw new InvalidOperationException($"Temporary tables should start with # (i.e. #myTable). Consider using {nameof(TableNameAttribute)}"); - SqlBuilder.CreateTableSql(view).ExecuteNonQuery(); + Connector.Current.SqlBuilder.CreateTableSql(view).ExecuteLeaves(); } public static IDisposable TemporaryTable() where T : IView @@ -123,7 +126,7 @@ public static void DropTemporaryTable() if (!view.Name.IsTemporal) throw new InvalidOperationException($"Temporary tables should start with # (i.e. #myTable). Consider using {nameof(TableNameAttribute)}"); - SqlBuilder.DropTable(view.Name).ExecuteNonQuery(); + Connector.Current.SqlBuilder.DropTable(view.Name).ExecuteNonQuery(); } public static void CreateTemporaryIndex(Expression> fields, bool unique = false) @@ -137,7 +140,7 @@ public static void CreateTemporaryIndex(Expression> fields, b new UniqueTableIndex(view, columns) : new TableIndex(view, columns); - SqlBuilder.CreateIndex(index, checkUnique: null).ExecuteLeaves(); + Connector.Current.SqlBuilder.CreateIndex(index, checkUnique: null).ExecuteLeaves(); } internal static readonly ThreadVariable?> registeredViewNameReplacer = Statics.ThreadVariable?>("overrideDatabase"); @@ -180,6 +183,13 @@ public static bool ExistsTable(Type type) public static bool ExistsTable(ITable table) { SchemaName schema = table.Name.Schema; + if (Schema.Current.Settings.IsPostgres) + { + return (from t in Database.View() + join ns in Database.View() on t.relnamespace equals ns.oid + where t.relname == table.Name.Name && ns.nspname == schema.Name + select t).Any(); + } if (schema.Database != null && schema.Database.Server != null && !Database.View().Any(ss => ss.name == schema.Database!.Server!.Name)) return false; @@ -208,7 +218,7 @@ public static List TryRetrieveAll(Type type, Replacements replacements) { Table table = Schema.Current.Table(type); - using (Synchronizer.RenameTable(table, replacements)) + using (Synchronizer.UseOldTableName(table, replacements)) using (ExecutionMode.DisableCache()) { if (ExistsTable(table)) @@ -217,6 +227,8 @@ public static List TryRetrieveAll(Type type, Replacements replacements) } } + + public static IDisposable DisableIdentity() where T : Entity { @@ -224,38 +236,37 @@ public static IDisposable DisableIdentity() return DisableIdentity(table); } - public static IDisposable DisableIdentity(Expression>> mListField) + public static IDisposable? DisableIdentity(Expression>> mListField) where T : Entity { TableMList table = ((FieldMList)Schema.Current.Field(mListField)).TableMList; - return DisableIdentity(table.Name); + return DisableIdentity(table); } - public static IDisposable DisableIdentity(Table table) + public static bool IsIdentityBehaviourDisabled(ITable table) + { + return identityBehaviourDisabled.Value?.Contains(table) == true; + } + + static ThreadVariable?> identityBehaviourDisabled = Statics.ThreadVariable?>("identityBehaviourOverride"); + public static IDisposable DisableIdentity(ITable table, bool behaviourOnly = false) { if (!table.IdentityBehaviour) throw new InvalidOperationException("Identity is false already"); - table.IdentityBehaviour = false; - if (table.PrimaryKey.Default == null) - SqlBuilder.SetIdentityInsert(table.Name, true).ExecuteNonQuery(); - - return new Disposable(() => - { - table.IdentityBehaviour = true; - - if (table.PrimaryKey.Default == null) - SqlBuilder.SetIdentityInsert(table.Name, false).ExecuteNonQuery(); - }); - } + var sqlBuilder = Connector.Current.SqlBuilder; + var oldValue = identityBehaviourDisabled.Value ?? ImmutableStack.Empty; - public static IDisposable DisableIdentity(ObjectName tableName) - { - SqlBuilder.SetIdentityInsert(tableName, true).ExecuteNonQuery(); + identityBehaviourDisabled.Value = oldValue.Push(table); + if (table.PrimaryKey.Default == null && !sqlBuilder.IsPostgres && !behaviourOnly) + sqlBuilder.SetIdentityInsert(table.Name, true).ExecuteNonQuery(); return new Disposable(() => { - SqlBuilder.SetIdentityInsert(tableName, false).ExecuteNonQuery(); + identityBehaviourDisabled.Value = oldValue.IsEmpty ? null : oldValue; + + if (table.PrimaryKey.Default == null && !sqlBuilder.IsPostgres && !behaviourOnly) + sqlBuilder.SetIdentityInsert(table.Name, false).ExecuteNonQuery(); }); } @@ -270,7 +281,6 @@ public static void SaveDisableIdentity(T entities) } } - public static void SaveListDisableIdentity(IEnumerable entities) where T : Entity { @@ -409,6 +419,7 @@ static IDisposable PrepareForBathLoadScope(this Table table, bool disableForeign public static IDisposable PrepareTableForBatchLoadScope(ITable table, bool disableForeignKeys, bool disableMultipleIndexes, bool disableUniqueIndexes) { + var sqlBuilder = Connector.Current.SqlBuilder; SafeConsole.WriteColor(ConsoleColor.Magenta, table.Name + ":"); Action onDispose = () => SafeConsole.WriteColor(ConsoleColor.Magenta, table.Name + ":"); @@ -431,13 +442,13 @@ public static IDisposable PrepareTableForBatchLoadScope(ITable table, bool disab if (multiIndexes.Any()) { SafeConsole.WriteColor(ConsoleColor.DarkMagenta, " DISABLE Multiple Indexes"); - multiIndexes.Select(i => SqlBuilder.DisableIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); + multiIndexes.Select(i => sqlBuilder.DisableIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); Executor.ExecuteNonQuery(multiIndexes.ToString(i => "ALTER INDEX [{0}] ON {1} DISABLE".FormatWith(i, table.Name), "\r\n")); onDispose += () => { SafeConsole.WriteColor(ConsoleColor.DarkMagenta, " REBUILD Multiple Indexes"); - multiIndexes.Select(i => SqlBuilder.RebuildIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); + multiIndexes.Select(i => sqlBuilder.RebuildIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); }; } } @@ -449,11 +460,11 @@ public static IDisposable PrepareTableForBatchLoadScope(ITable table, bool disab if (uniqueIndexes.Any()) { SafeConsole.WriteColor(ConsoleColor.DarkMagenta, " DISABLE Unique Indexes"); - uniqueIndexes.Select(i => SqlBuilder.DisableIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); + uniqueIndexes.Select(i => sqlBuilder.DisableIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); onDispose += () => { SafeConsole.WriteColor(ConsoleColor.DarkMagenta, " REBUILD Unique Indexes"); - uniqueIndexes.Select(i => SqlBuilder.RebuildIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); + uniqueIndexes.Select(i => sqlBuilder.RebuildIndex(table.Name, i)).Combine(Spacing.Simple)!.ExecuteLeaves(); }; } } @@ -485,19 +496,24 @@ public static void TruncateTable(Type type) public static void TruncateTableSystemVersioning(ITable table) { + var sqlBuilder = Connector.Current.SqlBuilder; + if(table.SystemVersioned == null) - SqlBuilder.TruncateTable(table.Name).ExecuteLeaves(); + sqlBuilder.TruncateTable(table.Name).ExecuteLeaves(); else { - SqlBuilder.AlterTableDisableSystemVersioning(table.Name).ExecuteLeaves(); - SqlBuilder.TruncateTable(table.Name).ExecuteLeaves(); - SqlBuilder.TruncateTable(table.SystemVersioned.TableName).ExecuteLeaves(); - SqlBuilder.AlterTableEnableSystemVersioning(table).ExecuteLeaves(); + sqlBuilder.AlterTableDisableSystemVersioning(table.Name).ExecuteLeaves(); + sqlBuilder.TruncateTable(table.Name).ExecuteLeaves(); + sqlBuilder.TruncateTable(table.SystemVersioned.TableName).ExecuteLeaves(); + sqlBuilder.AlterTableEnableSystemVersioning(table).ExecuteLeaves(); } } public static IDisposable DropAndCreateIncommingForeignKeys(Table table) { + var sqlBuilder = Connector.Current.SqlBuilder; + var isPostgres = Schema.Current.Settings.IsPostgres; + var foreignKeys = Administrator.OverrideDatabaseInSysViews(table.Name.Schema.Database).Using(_ => (from targetTable in Database.View() where targetTable.name == table.Name.Name && targetTable.Schema().name == table.Name.Schema.Name @@ -506,26 +522,27 @@ from ifk in targetTable.IncommingForeignKeys() select new { Name = ifk.name, - ParentTable = new ObjectName(new SchemaName(table.Name.Schema.Database, parentTable.Schema().name), parentTable.name), + ParentTable = new ObjectName(new SchemaName(table.Name.Schema.Database, parentTable.Schema().name, isPostgres), parentTable.name, isPostgres), ParentColumn = parentTable.Columns().SingleEx(c => c.column_id == ifk.ForeignKeyColumns().SingleEx().parent_column_id).name, }).ToList()); - foreignKeys.ForEach(fk => SqlBuilder.AlterTableDropConstraint(fk.ParentTable!, fk.Name! /*CSBUG*/).ExecuteLeaves()); + foreignKeys.ForEach(fk => sqlBuilder.AlterTableDropConstraint(fk.ParentTable!, fk.Name! /*CSBUG*/).ExecuteLeaves()); return new Disposable(() => { - foreignKeys.ToList().ForEach(fk => SqlBuilder.AlterTableAddConstraintForeignKey(fk.ParentTable!, fk.ParentColumn!, table.Name, table.PrimaryKey.Name)!.ExecuteLeaves()); + foreignKeys.ToList().ForEach(fk => sqlBuilder.AlterTableAddConstraintForeignKey(fk.ParentTable!, fk.ParentColumn!, table.Name, table.PrimaryKey.Name)!.ExecuteLeaves()); }); } public static IDisposable DisableUniqueIndex(UniqueTableIndex index) { + var sqlBuilder = Connector.Current.SqlBuilder; SafeConsole.WriteLineColor(ConsoleColor.DarkMagenta, " DISABLE Unique Index " + index.IndexName); - SqlBuilder.DisableIndex(index.Table.Name, index.IndexName).ExecuteLeaves(); + sqlBuilder.DisableIndex(index.Table.Name, index.IndexName).ExecuteLeaves(); return new Disposable(() => { SafeConsole.WriteLineColor(ConsoleColor.DarkMagenta, " REBUILD Unique Index " + index.IndexName); - SqlBuilder.RebuildIndex(index.Table.Name, index.IndexName).ExecuteLeaves(); + sqlBuilder.RebuildIndex(index.Table.Name, index.IndexName).ExecuteLeaves(); }); } @@ -545,11 +562,12 @@ from i in t.Indices() public static void DropUniqueIndexes() where T : Entity { + var sqlBuilder = Connector.Current.SqlBuilder; var table = Schema.Current.Table(); var indexesNames = Administrator.GetIndixesNames(table, unique: true); if (indexesNames.HasItems()) - indexesNames.Select(n => SqlBuilder.DropIndex(table.Name, n)).Combine(Spacing.Simple)!.ExecuteLeaves(); + indexesNames.Select(n => sqlBuilder.DropIndex(table.Name, n)).Combine(Spacing.Simple)!.ExecuteLeaves(); } @@ -608,12 +626,13 @@ static List MoveAllForeignKeysPrivate(Lite fromEntity, if (shouldMove != null) columns = columns.Where(p => shouldMove!(p.Table, p.Column)).ToList(); + var isPostgres = Schema.Current.Settings.IsPostgres; var pb = Connector.Current.ParameterBuilder; - return columns.Select(ct => new ColumnTableScript(ct, new SqlPreCommandSimple("UPDATE {0}\r\nSET {1} = @toEntity\r\nWHERE {1} = @fromEntity".FormatWith(ct.Table.Name, ct.Column.Name.SqlEscape()), new List - { - pb.CreateReferenceParameter("@fromEntity", fromEntity.Id, ct.Column), - pb.CreateReferenceParameter("@toEntity", toEntity.Id, ct.Column), - }))).ToList(); + return columns.Select(ct => new ColumnTableScript(ct, new SqlPreCommandSimple("UPDATE {0}\r\nSET {1} = @toEntity\r\nWHERE {1} = @fromEntity".FormatWith(ct.Table.Name, ct.Column.Name.SqlEscape(isPostgres)), new List + { + pb.CreateReferenceParameter("@fromEntity", fromEntity.Id, ct.Column), + pb.CreateReferenceParameter("@toEntity", toEntity.Id, ct.Column), + }))).ToList(); } class ColumnTable @@ -654,10 +673,10 @@ public static SqlPreCommand DeleteWhereScript(Table table, IColumn column, Prima throw new InvalidOperationException($"DeleteWhereScript can not be used for {table.Type.Name} because contains MLists"); if(id.VariableName.HasText()) - return new SqlPreCommandSimple("DELETE FROM {0} WHERE {1} = {2}".FormatWith(table.Name, column.Name, id.VariableName)); + return new SqlPreCommandSimple("DELETE FROM {0} WHERE {1} = {2};".FormatWith(table.Name, column.Name, id.VariableName)); var param = Connector.Current.ParameterBuilder.CreateReferenceParameter("@id", id, column); - return new SqlPreCommandSimple("DELETE FROM {0} WHERE {1} = {2}".FormatWith(table.Name, column.Name, param.ParameterName), new List { param }); + return new SqlPreCommandSimple("DELETE FROM {0} WHERE {1} = {2}:".FormatWith(table.Name, column.Name, param.ParameterName), new List { param }); } diff --git a/Signum.Engine/Basics/TypeLogic.cs b/Signum.Engine/Basics/TypeLogic.cs index 20d0101424..21b4ea56b9 100644 --- a/Signum.Engine/Basics/TypeLogic.cs +++ b/Signum.Engine/Basics/TypeLogic.cs @@ -44,11 +44,22 @@ public static void Start(SchemaBuilder sb) { if (sb.NotDefined(MethodInfo.GetCurrentMethod())) { - Schema current = Schema.Current; + Schema schema = Schema.Current; - current.SchemaCompleted += () => + sb.Include() + .WithQuery(() => t => new + { + Entity = t, + t.Id, + t.TableName, + t.CleanName, + t.ClassName, + t.Namespace, + }); + + schema.SchemaCompleted += () => { - var attributes = current.Tables.Keys.Select(t => KeyValuePair.Create(t, t.GetCustomAttribute(true))).ToList(); + var attributes = schema.Tables.Keys.Select(t => KeyValuePair.Create(t, t.GetCustomAttribute(true))).ToList(); var errors = attributes.Where(a => a.Value == null).ToString(a => "Type {0} does not have an EntityTypeAttribute".FormatWith(a.Key.Name), "\r\n"); @@ -56,25 +67,12 @@ public static void Start(SchemaBuilder sb) throw new InvalidOperationException(errors); }; - current.Initializing += () => + schema.Initializing += () => { - current.typeCachesLazy.Load(); + schema.typeCachesLazy.Load(); }; - current.typeCachesLazy = sb.GlobalLazy(() => new TypeCaches(current), - new InvalidateWith(typeof(TypeEntity)), - Schema.Current.InvalidateMetadata); - - sb.Include() - .WithQuery(() => t => new - { - Entity = t, - t.Id, - t.TableName, - t.CleanName, - t.ClassName, - t.Namespace, - }); + schema.typeCachesLazy = sb.GlobalLazy(() => new TypeCaches(schema), new InvalidateWith(typeof(TypeEntity)), Schema.Current.InvalidateMetadata); TypeEntity.SetTypeEntityCallbacks( t => TypeToEntity.GetOrThrow(t), @@ -92,6 +90,7 @@ join t in Schema.Current.Tables.Keys on dn.FullClassName equals (EnumEntity.Extr public static SqlPreCommand? Schema_Synchronizing(Replacements replacements) { var schema = Schema.Current; + var isPostgres = schema.Settings.IsPostgres; Dictionary should = GenerateSchemaTypes().ToDictionaryEx(s => s.TableName, "tableName in memory"); @@ -99,8 +98,8 @@ join t in Schema.Current.Tables.Keys on dn.FullClassName equals (EnumEntity.Extr { //External entities are nt asked in SchemaSynchronizer replacements.AskForReplacements( - currentList.Where(t => schema.IsExternalDatabase(ObjectName.Parse(t.TableName).Schema.Database)).Select(a => a.TableName).ToHashSet(), - should.Values.Where(t => schema.IsExternalDatabase(ObjectName.Parse(t.TableName).Schema.Database)).Select(a => a.TableName).ToHashSet(), + currentList.Where(t => schema.IsExternalDatabase(ObjectName.Parse(t.TableName, isPostgres).Schema.Database)).Select(a => a.TableName).ToHashSet(), + should.Values.Where(t => schema.IsExternalDatabase(ObjectName.Parse(t.TableName, isPostgres).Schema.Database)).Select(a => a.TableName).ToHashSet(), Replacements.KeyTables); } @@ -109,12 +108,12 @@ join t in Schema.Current.Tables.Keys on dn.FullClassName equals (EnumEntity.Extr { //Temporal solution until applications are updated var repeated = - should.Keys.Select(k => ObjectName.Parse(k)).GroupBy(a => a.Name).Where(a => a.Count() > 1).Select(a => a.Key).Concat( - current.Keys.Select(k => ObjectName.Parse(k)).GroupBy(a => a.Name).Where(a => a.Count() > 1).Select(a => a.Key)).ToList(); + should.Keys.Select(k => ObjectName.Parse(k, isPostgres)).GroupBy(a => a.Name).Where(a => a.Count() > 1).Select(a => a.Key).Concat( + current.Keys.Select(k => ObjectName.Parse(k, isPostgres)).GroupBy(a => a.Name).Where(a => a.Count() > 1).Select(a => a.Key)).ToList(); Func simplify = tn => { - ObjectName name = ObjectName.Parse(tn); + ObjectName name = ObjectName.Parse(tn, isPostgres); return repeated.Contains(name.Name) ? name.ToString() : name.Name; }; @@ -138,8 +137,8 @@ join t in Schema.Current.Tables.Keys on dn.FullClassName equals (EnumEntity.Extr if (c.TableName != s.TableName) { - var pc = ObjectName.Parse(c.TableName); - var ps = ObjectName.Parse(s.TableName); + var pc = ObjectName.Parse(c.TableName, isPostgres); + var ps = ObjectName.Parse(s.TableName, isPostgres); if (!EqualsIgnoringDatabasePrefix(pc, ps)) { diff --git a/Signum.Engine/BulkInserter.cs b/Signum.Engine/BulkInserter.cs index b560336872..095e0ca80a 100644 --- a/Signum.Engine/BulkInserter.cs +++ b/Signum.Engine/BulkInserter.cs @@ -28,7 +28,6 @@ public static int BulkInsert(this IEnumerable entities, using (HeavyProfiler.Log(nameof(BulkInsert), () => typeof(T).TypeName())) using (Transaction tr = new Transaction()) { - var table = Schema.Current.Table(typeof(T)); if (!disableIdentity && table.IdentityBehaviour && table.TablesMList().Any()) @@ -155,27 +154,28 @@ public static int BulkInsertTable(IEnumerable entities, var t = Schema.Current.Table(); bool disableIdentityBehaviour = copyOptions.HasFlag(SqlBulkCopyOptions.KeepIdentity); - bool oldIdentityBehaviour = t.IdentityBehaviour; DataTable dt = new DataTable(); - foreach (var c in t.Columns.Values.Where(c => !(c is SystemVersionedInfo.Column) && (disableIdentityBehaviour || !c.IdentityBehaviour))) + var columns = t.Columns.Values.Where(c => !(c is SystemVersionedInfo.SqlServerPeriodColumn) && (disableIdentityBehaviour || !c.IdentityBehaviour)).ToList(); + foreach (var c in columns) dt.Columns.Add(new DataColumn(c.Name, c.Type.UnNullify())); - if (disableIdentityBehaviour) t.IdentityBehaviour = false; - foreach (var e in list) + using (disableIdentityBehaviour ? Administrator.DisableIdentity(t, behaviourOnly: true) : null) { - if (!e.IsNew) - throw new InvalidOperationException("Entites should be new"); - t.SetToStrField(e); - dt.Rows.Add(t.BulkInsertDataRow(e)); + foreach (var e in list) + { + if (!e.IsNew) + throw new InvalidOperationException("Entites should be new"); + t.SetToStrField(e); + dt.Rows.Add(t.BulkInsertDataRow(e)); + } } - if (disableIdentityBehaviour) t.IdentityBehaviour = oldIdentityBehaviour; using (Transaction tr = new Transaction()) { Schema.Current.OnPreBulkInsert(typeof(T), inMListTable: false); - Executor.BulkCopy(dt, t.Name, copyOptions, timeout); + Executor.BulkCopy(dt, columns, t.Name, copyOptions, timeout); foreach (var item in list) item.SetNotModified(); @@ -272,8 +272,8 @@ public static int BulkInsertMListTable( var maxRowId = updateParentTicks.Value ? Database.MListQuery(mListProperty).Max(a => (PrimaryKey?)a.RowId) : null; DataTable dt = new DataTable(); - - foreach (var c in mlistTable.Columns.Values.Where(c => !(c is SystemVersionedInfo.Column) && !c.IdentityBehaviour)) + var columns = mlistTable.Columns.Values.Where(c => !(c is SystemVersionedInfo.SqlServerPeriodColumn) && !c.IdentityBehaviour).ToList(); + foreach (var c in columns) dt.Columns.Add(new DataColumn(c.Name, c.Type.UnNullify())); var list = mlistElements.ToList(); @@ -287,7 +287,7 @@ public static int BulkInsertMListTable( { Schema.Current.OnPreBulkInsert(typeof(E), inMListTable: true); - Executor.BulkCopy(dt, mlistTable.Name, copyOptions, timeout); + Executor.BulkCopy(dt, columns, mlistTable.Name, copyOptions, timeout); var result = list.Count; @@ -327,8 +327,9 @@ public static int BulkInsertView(this IEnumerable entities, bool disableIdentityBehaviour = copyOptions.HasFlag(SqlBulkCopyOptions.KeepIdentity); + var columns = t.Columns.Values.ToList(); DataTable dt = new DataTable(); - foreach (var c in t.Columns.Values) + foreach (var c in columns) dt.Columns.Add(new DataColumn(c.Name, c.Type.UnNullify())); foreach (var e in entities) @@ -340,7 +341,7 @@ public static int BulkInsertView(this IEnumerable entities, { Schema.Current.OnPreBulkInsert(typeof(T), inMListTable: false); - Executor.BulkCopy(dt, t.Name, copyOptions, timeout); + Executor.BulkCopy(dt, columns, t.Name, copyOptions, timeout); return tr.Commit(list.Count); } diff --git a/Signum.Engine/CodeGeneration/EntityCodeGenerator.cs b/Signum.Engine/CodeGeneration/EntityCodeGenerator.cs index 674f748af6..2ce50162f4 100644 --- a/Signum.Engine/CodeGeneration/EntityCodeGenerator.cs +++ b/Signum.Engine/CodeGeneration/EntityCodeGenerator.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Signum.Engine.Engine; using Signum.Engine.Maps; using Signum.Entities; using Signum.Entities.Reflection; @@ -26,7 +27,7 @@ public class EntityCodeGenerator public virtual void GenerateEntitiesFromDatabaseTables() { - CurrentSchema = Schema.Current; + CurrentSchema = Schema.Current; var tables = GetTables(); @@ -70,7 +71,9 @@ protected virtual string GetProjectFolder() protected virtual List GetTables() { - return SchemaSynchronizer.DefaultGetDatabaseDescription(Schema.Current.DatabaseNames()).Values.ToList(); + return Schema.Current.Settings.IsPostgres ? + PostgresCatalogSchema.GetDatabaseDescription(Schema.Current.DatabaseNames()).Values.ToList() : + SysTablesSchema.GetDatabaseDescription(Schema.Current.DatabaseNames()).Values.ToList(); } protected virtual void GetSolutionInfo(out string solutionFolder, out string solutionName) @@ -538,7 +541,7 @@ protected virtual string GetTicksColumnAttribute(DiffTable table) { StringBuilder sb = new StringBuilder(); sb.Append("TableName(\"" + objectName.Name + "\""); - if (objectName.Schema != SchemaName.Default) + if (objectName.Schema != SchemaName.Default(CurrentSchema.Settings.IsPostgres)) sb.Append(", SchemaName = \"" + objectName.Schema.Name + "\""); if (objectName.Schema.Database != null) @@ -636,7 +639,7 @@ protected virtual IEnumerable GetPropertyAttributes(DiffTable table, Dif parts.Add("Min = " + min); if (col.Length != -1) - parts.Add("Max = " + col.Length / DiffColumn.BytesPerChar(col.SqlDbType)); + parts.Add("Max = " + col.Length); return "StringLengthValidator(" + parts.ToString(", ") + ")"; } @@ -706,7 +709,7 @@ protected virtual bool HasUniqueIndex(DiffTable table, DiffColumn col) ix.FilterDefinition == null && ix.Columns.Only()?.Let(ic => ic.ColumnName == col.Name && ic.IsIncluded == false) == true && ix.IsUnique && - ix.Type == DiffIndexType.NonClustered); + ix.IsPrimary); } protected virtual string DefaultColumnName(DiffTable table, DiffColumn col) @@ -743,19 +746,19 @@ protected virtual List GetSqlDbTypeParts(DiffColumn col, Type type) { List parts = new List(); var pair = CurrentSchema.Settings.GetSqlDbTypePair(type); - if (pair.SqlDbType != col.SqlDbType) - parts.Add("SqlDbType = SqlDbType." + col.SqlDbType); + if (pair.DbType.SqlServer != col.DbType.SqlServer) + parts.Add("SqlDbType = SqlDbType." + col.DbType.SqlServer); - var defaultSize = CurrentSchema.Settings.GetSqlSize(null, null, pair.SqlDbType); + var defaultSize = CurrentSchema.Settings.GetSqlSize(null, null, pair.DbType); if (defaultSize != null) { - if (!(defaultSize == col.Precision || defaultSize == col.Length / DiffColumn.BytesPerChar(col.SqlDbType) || defaultSize == int.MaxValue && col.Length == -1)) + if (!(defaultSize == col.Precision || defaultSize == col.Length || defaultSize == int.MaxValue && col.Length == -1)) parts.Add("Size = " + (col.Length == -1 ? "int.MaxValue" : - col.Length != 0 ? (col.Length / DiffColumn.BytesPerChar(col.SqlDbType)).ToString() : + col.Length != 0 ? col.Length.ToString() : col.Precision != 0 ? col.Precision.ToString() : "0")); } - var defaultScale = CurrentSchema.Settings.GetSqlScale(null, null, col.SqlDbType); + var defaultScale = CurrentSchema.Settings.GetSqlScale(null, null, col.DbType); if (defaultScale != null) { if (!(col.Scale == defaultScale)) @@ -801,7 +804,7 @@ protected virtual bool IsLite(DiffTable table, DiffColumn col) protected internal virtual Type GetValueType(DiffColumn col) { - switch (col.SqlDbType) + switch (col.DbType.SqlServer) { case SqlDbType.BigInt: return typeof(long); case SqlDbType.Binary: return typeof(byte[]); @@ -834,7 +837,7 @@ protected internal virtual Type GetValueType(DiffColumn col) case SqlDbType.Udt: return Schema.Current.Settings.UdtSqlName .SingleOrDefaultEx(kvp => StringComparer.InvariantCultureIgnoreCase.Equals(kvp.Value, col.UserTypeName)) .Key; - default: throw new NotImplementedException("Unknown translation for " + col.SqlDbType); + default: throw new NotImplementedException("Unknown translation for " + col.DbType.SqlServer); } } diff --git a/Signum.Engine/CodeGeneration/LogicCodeGenerator.cs b/Signum.Engine/CodeGeneration/LogicCodeGenerator.cs index 15d5ee8f75..91be1ac500 100644 --- a/Signum.Engine/CodeGeneration/LogicCodeGenerator.cs +++ b/Signum.Engine/CodeGeneration/LogicCodeGenerator.cs @@ -426,7 +426,7 @@ protected virtual bool IsSimpleValueType(Type type) { var t = CurrentSchema.Settings.TryGetSqlDbTypePair(type.UnNullify()); - return t != null && t.UserDefinedTypeName == null && t.SqlDbType != SqlDbType.Image && t.SqlDbType != SqlDbType.VarBinary; + return t != null && t.UserDefinedTypeName == null && (t.DbType.IsNumber() || t.DbType.IsString() || t.DbType.IsDate()); } protected virtual string WriteOperations(Type type) diff --git a/Signum.Engine/Connection/Connector.cs b/Signum.Engine/Connection/Connector.cs index ff2908b2f7..760cf7c8c3 100644 --- a/Signum.Engine/Connection/Connector.cs +++ b/Signum.Engine/Connection/Connector.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using System.Threading; using System.Data.SqlClient; +using System.Collections.Generic; namespace Signum.Engine { @@ -19,6 +20,8 @@ public abstract class Connector { static readonly Variable currentConnector = Statics.ThreadVariable("connection"); + public SqlBuilder SqlBuilder; + public static IDisposable Override(Connector connector) { Connector oldConnection = currentConnector.Value; @@ -48,6 +51,7 @@ public Connector(Schema schema) { this.Schema = schema; this.IsolationLevel = IsolationLevel.Unspecified; + this.SqlBuilder = new SqlBuilder(this); } public Schema Schema { get; private set; } @@ -75,21 +79,22 @@ protected static void Log(SqlPreCommandSimple pcs) } } - public abstract SqlDbType GetSqlDbType(DbParameter p); + public abstract string GetSqlDbType(DbParameter p); protected internal abstract object? ExecuteScalar(SqlPreCommandSimple preCommand, CommandType commandType); protected internal abstract int ExecuteNonQuery(SqlPreCommandSimple preCommand, CommandType commandType); - protected internal abstract DataTable ExecuteDataTable(SqlPreCommandSimple command, CommandType commandType); - protected internal abstract DbDataReaderWithCommand UnsafeExecuteDataReader(SqlPreCommandSimple sqlPreCommandSimple, CommandType commandType); + protected internal abstract DataTable ExecuteDataTable(SqlPreCommandSimple preCommand, CommandType commandType); + protected internal abstract DbDataReaderWithCommand UnsafeExecuteDataReader(SqlPreCommandSimple preCommand, CommandType commandType); protected internal abstract Task UnsafeExecuteDataReaderAsync(SqlPreCommandSimple preCommand, CommandType commandType, CancellationToken token); - protected internal abstract DataSet ExecuteDataSet(SqlPreCommandSimple sqlPreCommandSimple, CommandType commandType); - protected internal abstract void BulkCopy(DataTable dt, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout); + protected internal abstract void BulkCopy(DataTable dt, List columns, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout); + + public abstract Connector ForDatabase(Maps.DatabaseName? database); public abstract string DatabaseName(); public abstract string DataSourceName(); - public virtual int MaxNameLength { get { return 128; } } + public abstract int MaxNameLength { get; } public abstract void SaveTransactionPoint(DbTransaction transaction, string savePointName); @@ -141,8 +146,6 @@ public static string ExtractCatalogPostfix(ref string connectionString, string c public abstract bool AllowsIndexWithWhere(string where); - public abstract SqlPreCommand ShrinkDatabase(string databaseName); - public abstract bool AllowsConvertToDate { get; } public abstract bool AllowsConvertToTime { get; } @@ -165,24 +168,18 @@ public static string GetParameterName(string name) public DbParameter CreateReferenceParameter(string parameterName, PrimaryKey? id, IColumn column) { - return CreateParameter(parameterName, column.SqlDbType, null, column.Nullable.ToBool(), id == null ? null : id.Value.Object); + return CreateParameter(parameterName, column.DbType, null, column.Nullable.ToBool(), id == null ? null : id.Value.Object); } public DbParameter CreateParameter(string parameterName, object? value, Type type) { var pair = Schema.Current.Settings.GetSqlDbTypePair(type.UnNullify()); - return CreateParameter(parameterName, pair.SqlDbType, pair.UserDefinedTypeName, type == null || type.IsByRef || type.IsNullable(), value); - } - - public abstract DbParameter CreateParameter(string parameterName, SqlDbType type, string? udtTypeName, bool nullable, object? value); - public abstract MemberInitExpression ParameterFactory(Expression parameterName, SqlDbType type, string? udtTypeName, bool nullable, Expression value); - - protected static bool IsDate(SqlDbType type) - { - return type == SqlDbType.Date || type == SqlDbType.DateTime || type == SqlDbType.DateTime2 || type == SqlDbType.SmallDateTime; + return CreateParameter(parameterName, pair.DbType, pair.UserDefinedTypeName, type == null || type.IsByRef || type.IsNullable(), value); } + public abstract DbParameter CreateParameter(string parameterName, AbstractDbType dbType, string? udtTypeName, bool nullable, object? value); + public abstract MemberInitExpression ParameterFactory(Expression parameterName, AbstractDbType dbType, string? udtTypeName, bool nullable, Expression value); protected static MethodInfo miAsserDateTime = ReflectionTools.GetMethodInfo(() => AssertDateTime(null)); diff --git a/Signum.Engine/Connection/Executor.cs b/Signum.Engine/Connection/Executor.cs index 79d19dc694..fd16b19204 100644 --- a/Signum.Engine/Connection/Executor.cs +++ b/Signum.Engine/Connection/Executor.cs @@ -61,17 +61,6 @@ public static DataTable ExecuteDataTable(this SqlPreCommandSimple preCommand, Co return Connector.Current.ExecuteDataTable(preCommand, commandType); } - - public static DataSet ExecuteDataSet(string sql, List? parameters = null, CommandType commandType = CommandType.Text) - { - return Connector.Current.ExecuteDataSet(new SqlPreCommandSimple(sql, parameters), commandType); - } - - public static DataSet ExecuteDataSet(this SqlPreCommandSimple preCommand, CommandType commandType = CommandType.Text) - { - return Connector.Current.ExecuteDataSet(preCommand, commandType); - } - public static void ExecuteLeaves(this SqlPreCommand preCommand, CommandType commandType = CommandType.Text) { foreach (var simple in preCommand.Leaves()) @@ -80,9 +69,9 @@ public static void ExecuteLeaves(this SqlPreCommand preCommand, CommandType comm } } - public static void BulkCopy(DataTable dt, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout) + public static void BulkCopy(DataTable dt, List column, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout) { - Connector.Current.BulkCopy(dt, destinationTable, options, timeout); + Connector.Current.BulkCopy(dt, column, destinationTable, options, timeout); } } } diff --git a/Signum.Engine/Connection/FieldReader.cs b/Signum.Engine/Connection/FieldReader.cs index 096f890390..857477e24c 100644 --- a/Signum.Engine/Connection/FieldReader.cs +++ b/Signum.Engine/Connection/FieldReader.cs @@ -14,6 +14,7 @@ using System.Data.SqlClient; using Microsoft.SqlServer.Server; using System.IO; +using Npgsql; namespace Signum.Engine { @@ -46,8 +47,11 @@ TypeCode GetTypeCode(int ordinal) return tc; } + bool isPostgres; + public FieldReader(DbDataReader reader) { + this.isPostgres = Schema.Current.Settings.IsPostgres; this.reader = reader; this.typeCodes = new TypeCode[reader.FieldCount]; @@ -457,6 +461,9 @@ public DateTimeOffset GetDateTimeOffset(int ordinal) switch (typeCodes[ordinal]) { case tcDateTimeOffset: + if (isPostgres) + throw new InvalidOperationException("DateTimeOffset not supported in Postgres"); + return ((SqlDataReader)reader).GetDateTimeOffset(ordinal); default: return ReflectionTools.ChangeType(reader.GetValue(ordinal)); @@ -480,7 +487,10 @@ public TimeSpan GetTimeSpan(int ordinal) switch (typeCodes[ordinal]) { case tcTimeSpan: - return ((SqlDataReader)reader).GetTimeSpan(ordinal); + if (isPostgres) + return ((NpgsqlDataReader)reader).GetTimeSpan(ordinal); + else + return ((SqlDataReader)reader).GetTimeSpan(ordinal); default: return ReflectionTools.ChangeType(reader.GetValue(ordinal)); } @@ -535,6 +545,38 @@ public T GetUdt(int ordinal) return udt; } + static MethodInfo miGetArray = ReflectionTools.GetMethodInfo((FieldReader r) => r.GetArray(0)).GetGenericMethodDefinition(); + + public T[] GetArray(int ordinal) + { + LastOrdinal = ordinal; + if (reader.IsDBNull(ordinal)) + { + return (T[])(object)null!; + } + + return (T[])this.reader[ordinal]; + } + + static MethodInfo miNullableGetRange = ReflectionTools.GetMethodInfo((FieldReader r) => r.GetNullableRange(0)).GetGenericMethodDefinition(); + public NpgsqlTypes.NpgsqlRange? GetNullableRange(int ordinal) + { + LastOrdinal = ordinal; + if (reader.IsDBNull(ordinal)) + { + return (NpgsqlTypes.NpgsqlRange)(object)null!; + } + + return (NpgsqlTypes.NpgsqlRange)this.reader[ordinal]; + } + + static MethodInfo miGetRange = ReflectionTools.GetMethodInfo((FieldReader r) => r.GetRange(0)).GetGenericMethodDefinition(); + public NpgsqlTypes.NpgsqlRange GetRange(int ordinal) + { + LastOrdinal = ordinal; + return (NpgsqlTypes.NpgsqlRange)this.reader[ordinal]; + } + static Dictionary methods = typeof(FieldReader).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(m => m.Name != "GetExpression" && m.Name != "IsNull") @@ -555,6 +597,21 @@ public static Expression GetExpression(Expression reader, int ordinal, Type type return Expression.Call(reader, miGetUdt.MakeGenericMethod(type.UnNullify()), Expression.Constant(ordinal)); } + if (type.IsArray) + { + return Expression.Call(reader, miGetArray.MakeGenericMethod(type.ElementType()!), Expression.Constant(ordinal)); + } + + if (type.IsInstantiationOf(typeof(NpgsqlTypes.NpgsqlRange<>))) + { + return Expression.Call(reader, miGetRange.MakeGenericMethod(type.GetGenericArguments()[0]!), Expression.Constant(ordinal)); + } + + if (type.IsNullable() && type.UnNullify().IsInstantiationOf(typeof(NpgsqlTypes.NpgsqlRange<>))) + { + return Expression.Call(reader, miGetRange.MakeGenericMethod(type.UnNullify().GetGenericArguments()[0]!), Expression.Constant(ordinal)); + } + throw new InvalidOperationException("Type {0} not supported".FormatWith(type)); } @@ -576,7 +633,7 @@ internal FieldReaderException CreateFieldReaderException(Exception ex) } [Serializable] - public class FieldReaderException : SqlTypeException + public class FieldReaderException : DbException { public FieldReaderException(Exception inner, int ordinal, string columnName, Type columnType) : base(null, inner) { diff --git a/Signum.Engine/Connection/PostgreSqlConnector.cs b/Signum.Engine/Connection/PostgreSqlConnector.cs new file mode 100644 index 0000000000..659b432c95 --- /dev/null +++ b/Signum.Engine/Connection/PostgreSqlConnector.cs @@ -0,0 +1,541 @@ +using Npgsql; +using NpgsqlTypes; +using Signum.Engine.Connection; +using Signum.Engine.Maps; +using Signum.Utilities; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Signum.Engine +{ + + public static class PostgresVersionDetector + { + public static Version Detect(string connectionString) + { + return SqlServerRetry.Retry(() => + { + using (NpgsqlConnection con = new NpgsqlConnection(connectionString)) + { + var sql = @"SHOW server_version;"; + + using (NpgsqlCommand cmd = new NpgsqlCommand(sql, con)) + { + NpgsqlDataAdapter da = new NpgsqlDataAdapter(cmd); + + DataTable result = new DataTable(); + da.Fill(result); + + var version = (string)result.Rows[0]["server_version"]!; + + return new Version(version); + } + } + }); + } + } + + public class PostgreSqlConnector : Connector + { + public override ParameterBuilder ParameterBuilder { get; protected set; } + + public Version PostgresVersion { get; set; } + + public PostgreSqlConnector(string connectionString, Schema schema, Version postgresVersion) : base(schema.Do(s => s.Settings.IsPostgres = true)) + { + this.ConnectionString = connectionString; + this.ParameterBuilder = new PostgreSqlParameterBuilder(); + this.PostgresVersion = postgresVersion; + } + + public override int MaxNameLength => 63; + + public int? CommandTimeout { get; set; } = null; + public string ConnectionString { get; set; } + + public override bool AllowsMultipleQueries => true; + + public override bool SupportsScalarSubquery => true; + + public override bool SupportsScalarSubqueryInAggregates => true; + + public override bool AllowsSetSnapshotIsolation => false; + + public override bool AllowsConvertToDate => true; + + public override bool AllowsConvertToTime => true; + + public override bool SupportsSqlDependency => false; + + public override bool SupportsFormat => true; + + public override bool SupportsTemporalTables => true; + + public override bool RequiresRetry => false; + + public override bool AllowsIndexWithWhere(string where) => true; + + public override Connector ForDatabase(Maps.DatabaseName? database) + { + if (database == null) + return this; + + throw new NotImplementedException("ForDatabase " + database); + } + + public override void CleanDatabase(DatabaseName? database) + { + PostgreSqlConnectorScripts.RemoveAllScript(database).ExecuteNonQuery(); + } + + public override DbParameter CloneParameter(DbParameter p) + { + NpgsqlParameter sp = (NpgsqlParameter)p; + return new NpgsqlParameter(sp.ParameterName, sp.Value) { IsNullable = sp.IsNullable, NpgsqlDbType = sp.NpgsqlDbType }; + } + + public override DbConnection CreateConnection() + { + return new NpgsqlConnection(ConnectionString); + } + + public override string DatabaseName() + { + return new NpgsqlConnection(ConnectionString).Database!; + } + + public override string DataSourceName() + { + return new NpgsqlConnection(ConnectionString).DataSource; + } + + public override string GetSqlDbType(DbParameter p) + { + return ((NpgsqlParameter)p).NpgsqlDbType.ToString().ToUpperInvariant(); + } + + public override void RollbackTransactionPoint(DbTransaction transaction, string savePointName) + { + ((NpgsqlTransaction)transaction).Rollback(savePointName); + } + + public override void SaveTransactionPoint(DbTransaction transaction, string savePointName) + { + ((NpgsqlTransaction)transaction).Save(savePointName); + } + + T EnsureConnectionRetry(Func action) + { + if (Transaction.HasTransaction) + return action(null); + + using (NpgsqlConnection con = new NpgsqlConnection(this.ConnectionString)) + { + con.Open(); + + return action(con); + } + } + + NpgsqlCommand NewCommand(SqlPreCommandSimple preCommand, NpgsqlConnection? overridenConnection, CommandType commandType) + { + NpgsqlCommand cmd = new NpgsqlCommand { CommandType = commandType }; + + int? timeout = Connector.ScopeTimeout ?? CommandTimeout; + if (timeout.HasValue) + cmd.CommandTimeout = timeout.Value; + + if (overridenConnection != null) + cmd.Connection = overridenConnection; + else + { + cmd.Connection = (NpgsqlConnection)Transaction.CurrentConnection!; + cmd.Transaction = (NpgsqlTransaction)Transaction.CurrentTransaccion!; + } + + cmd.CommandText = preCommand.Sql; + + if (preCommand.Parameters != null) + { + foreach (NpgsqlParameter param in preCommand.Parameters) + { + cmd.Parameters.Add(param); + } + } + + Log(preCommand); + + return cmd; + } + + protected internal override void BulkCopy(DataTable dt, List columns, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout) + { + EnsureConnectionRetry(con => + { + con = con ?? (NpgsqlConnection)Transaction.CurrentConnection!; + + bool isPostgres = true; + + var columnsSql = dt.Columns.Cast().ToString(a => a.ColumnName.SqlEscape(isPostgres), ", "); + using (var writer = con.BeginBinaryImport($"COPY {destinationTable} ({columnsSql}) FROM STDIN (FORMAT BINARY)")) + { + for (int i = 0; i < dt.Rows.Count; i++) + { + var row = dt.Rows[i]; + writer.StartRow(); + for (int j = 0; j < dt.Columns.Count; j++) + { + var col = dt.Columns[j]; + writer.Write(row[col], columns[j].DbType.PostgreSql); + } + } + + writer.Complete(); + return 0; + } + }); + } + + protected internal override DataTable ExecuteDataTable(SqlPreCommandSimple preCommand, CommandType commandType) + { + return EnsureConnectionRetry(con => + { + using (NpgsqlCommand cmd = NewCommand(preCommand, con, commandType)) + using (HeavyProfiler.Log("SQL", () => preCommand.sp_executesql())) + { + try + { + NpgsqlDataAdapter da = new NpgsqlDataAdapter(cmd); + + DataTable result = new DataTable(); + da.Fill(result); + return result; + } + catch (Exception ex) + { + var nex = HandleException(ex, preCommand); + if (nex == ex) + throw; + + throw nex; + } + } + }); + } + + protected internal override int ExecuteNonQuery(SqlPreCommandSimple preCommand, CommandType commandType) + { + return EnsureConnectionRetry(con => + { + using (NpgsqlCommand cmd = NewCommand(preCommand, con, commandType)) + using (HeavyProfiler.Log("SQL", () => preCommand.sp_executesql())) + { + try + { + int result = cmd.ExecuteNonQuery(); + return result; + } + catch (Exception ex) + { + var nex = HandleException(ex, preCommand); + if (nex == ex) + throw; + + throw nex; + } + } + }); + } + + protected internal override object? ExecuteScalar(SqlPreCommandSimple preCommand, CommandType commandType) + { + return EnsureConnectionRetry(con => + { + using (NpgsqlCommand cmd = NewCommand(preCommand, con, commandType)) + using (HeavyProfiler.Log("SQL", () => preCommand.sp_executesql())) + { + try + { + object result = cmd.ExecuteScalar(); + + if (result == null || result == DBNull.Value) + return null; + + return result; + } + catch (Exception ex) + { + var nex = HandleException(ex, preCommand); + if (nex == ex) + throw; + + throw nex; + } + } + }); + } + + protected internal override DbDataReaderWithCommand UnsafeExecuteDataReader(SqlPreCommandSimple preCommand, CommandType commandType) + { + try + { + var cmd = NewCommand(preCommand, null, commandType); + + var reader = cmd.ExecuteReader(); + + return new DbDataReaderWithCommand(cmd, reader); + } + catch (Exception ex) + { + var nex = HandleException(ex, preCommand); + if (nex == ex) + throw; + + throw nex; + } + } + + protected internal override async Task UnsafeExecuteDataReaderAsync(SqlPreCommandSimple preCommand, CommandType commandType, CancellationToken token) + { + try + { + var cmd = NewCommand(preCommand, null, commandType); + + var reader = await cmd.ExecuteReaderAsync(token); + + return new DbDataReaderWithCommand(cmd, reader); + } + catch (Exception ex) + { + var nex = HandleException(ex, preCommand); + if (nex == ex) + throw; + + throw nex; + } + } + + public Exception HandleException(Exception ex, SqlPreCommandSimple command) + { + var nex = ReplaceException(ex, command); + nex.Data["Sql"] = command.sp_executesql(); + return nex; + } + + Exception ReplaceException(Exception ex, SqlPreCommandSimple command) + { + //if (ex is Npgsql.PostgresException se) + //{ + // switch (se.Number) + // { + // case -2: return new TimeoutException(ex.Message, ex); + // case 2601: return new UniqueKeyException(ex); + // case 547: return new ForeignKeyException(ex); + // default: return ex; + // } + //} + + //if (ex is SqlTypeException ste && ex.Message.Contains("DateTime")) + //{ + // var mins = command.Parameters.Where(a => DateTime.MinValue.Equals(a.Value)); + + // if (mins.Any()) + // { + // return new ArgumentOutOfRangeException("{0} {1} not initialized and equal to DateTime.MinValue".FormatWith( + // mins.CommaAnd(a => a.ParameterName), + // mins.Count() == 1 ? "is" : "are"), ex); + // } + //} + + return ex; + } + } + + public static class PostgreSqlConnectorScripts + { + public static SqlPreCommandSimple RemoveAllScript(DatabaseName? databaseName) + { + if (databaseName != null) + throw new NotSupportedException(); + + return new SqlPreCommandSimple(@"-- Copyright © 2019 +-- mirabilos +-- +-- Provided that these terms and disclaimer and all copyright notices +-- are retained or reproduced in an accompanying document, permission +-- is granted to deal in this work without restriction, including un‐ +-- limited rights to use, publicly perform, distribute, sell, modify, +-- merge, give away, or sublicence. +-- +-- This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to +-- the utmost extent permitted by applicable law, neither express nor +-- implied; without malicious intent or gross negligence. In no event +-- may a licensor, author or contributor be held liable for indirect, +-- direct, other damage, loss, or other issues arising in any way out +-- of dealing in the work, even if advised of the possibility of such +-- damage or existence of a defect, except proven that it results out +-- of said person’s immediate fault when using the work as intended. +-- - +-- Drop everything from the PostgreSQL database. + +DO $$ +DECLARE + r RECORD; +BEGIN + -- triggers + FOR r IN (SELECT pns.nspname, pc.relname, pt.tgname + FROM pg_trigger pt, pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace AND pc.oid=pt.tgrelid + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pt.tgisinternal=false + ) LOOP + EXECUTE format('DROP TRIGGER %I ON %I.%I;', + r.tgname, r.nspname, r.relname); + END LOOP; + -- constraints #1: foreign key + FOR r IN (SELECT pns.nspname, pc.relname, pcon.conname + FROM pg_constraint pcon, pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace AND pc.oid=pcon.conrelid + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pcon.contype='f' + ) LOOP + EXECUTE format('ALTER TABLE ONLY %I.%I DROP CONSTRAINT %I;', + r.nspname, r.relname, r.conname); + END LOOP; + -- constraints #2: the rest + FOR r IN (SELECT pns.nspname, pc.relname, pcon.conname + FROM pg_constraint pcon, pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace AND pc.oid=pcon.conrelid + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pcon.contype<>'f' + ) LOOP + EXECUTE format('ALTER TABLE ONLY %I.%I DROP CONSTRAINT %I;', + r.nspname, r.relname, r.conname); + END LOOP; + -- indicēs + FOR r IN (SELECT pns.nspname, pc.relname + FROM pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pc.relkind='i' + ) LOOP + EXECUTE format('DROP INDEX %I.%I;', + r.nspname, r.relname); + END LOOP; + -- normal and materialised views + FOR r IN (SELECT pns.nspname, pc.relname + FROM pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pc.relkind IN ('v', 'm') + ) LOOP + EXECUTE format('DROP VIEW %I.%I;', + r.nspname, r.relname); + END LOOP; + -- tables + FOR r IN (SELECT pns.nspname, pc.relname + FROM pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pc.relkind='r' + ) LOOP + EXECUTE format('DROP TABLE %I.%I;', + r.nspname, r.relname); + END LOOP; + -- sequences + FOR r IN (SELECT pns.nspname, pc.relname + FROM pg_class pc, pg_namespace pns + WHERE pns.oid=pc.relnamespace + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + AND pc.relkind='S' + ) LOOP + EXECUTE format('DROP SEQUENCE %I.%I;', + r.nspname, r.relname); + END LOOP; + -- extensions (see below), only if necessary + FOR r IN (SELECT pns.nspname, pe.extname + FROM pg_extension pe, pg_namespace pns + WHERE pns.oid=pe.extnamespace + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + ) LOOP + EXECUTE format('DROP EXTENSION %I;', r.extname); + END LOOP; + -- functions / procedures + FOR r IN (SELECT pns.nspname, pp.proname, pp.oid + FROM pg_proc pp, pg_namespace pns + WHERE pns.oid=pp.pronamespace + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') + ) LOOP + EXECUTE format('DROP FUNCTION %I.%I(%s);', + r.nspname, r.proname, + pg_get_function_identity_arguments(r.oid)); + END LOOP; + -- nōn-default schemata we own; assume to be run by a not-superuser + FOR r IN (SELECT pns.nspname + FROM pg_namespace pns, pg_roles pr + WHERE pr.oid=pns.nspowner + AND pns.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'public') + AND pr.rolname=current_user + ) LOOP + EXECUTE format('DROP SCHEMA %I;', r.nspname); + END LOOP; + -- voilà + RAISE NOTICE 'Database cleared!'; +END; $$;"); + } + } + + public class PostgreSqlParameterBuilder : ParameterBuilder + { + public override DbParameter CreateParameter(string parameterName, AbstractDbType dbType, string? udtTypeName, bool nullable, object? value) + { + if (dbType.IsDate()) + AssertDateTime((DateTime?)value); + + var result = new Npgsql.NpgsqlParameter(parameterName, value ?? DBNull.Value) + { + IsNullable = nullable + }; + + result.NpgsqlDbType = dbType.PostgreSql; + if (udtTypeName != null) + result.DataTypeName = udtTypeName; + + + return result; + } + + public override MemberInitExpression ParameterFactory(Expression parameterName, AbstractDbType dbType, string? udtTypeName, bool nullable, Expression value) + { + Expression valueExpr = Expression.Convert(dbType.IsDate() ? Expression.Call(miAsserDateTime, Expression.Convert(value, typeof(DateTime?))) : value, typeof(object)); + + if (nullable) + valueExpr = Expression.Condition(Expression.Equal(value, Expression.Constant(null, value.Type)), + Expression.Constant(DBNull.Value, typeof(object)), + valueExpr); + + NewExpression newExpr = Expression.New(typeof(NpgsqlParameter).GetConstructor(new[] { typeof(string), typeof(object) }), parameterName, valueExpr); + + + List mb = new List() + { + Expression.Bind(typeof(NpgsqlParameter).GetProperty(nameof(NpgsqlParameter.IsNullable)), Expression.Constant(nullable)), + Expression.Bind(typeof(NpgsqlParameter).GetProperty(nameof(NpgsqlParameter.NpgsqlDbType)), Expression.Constant(dbType.PostgreSql)), + }; + + if (udtTypeName != null) + mb.Add(Expression.Bind(typeof(NpgsqlParameter).GetProperty(nameof(NpgsqlParameter.DataTypeName)), Expression.Constant(udtTypeName))); + + return Expression.MemberInit(newExpr, mb); + } + } +} diff --git a/Signum.Engine/Connection/SqlConnector.cs b/Signum.Engine/Connection/SqlConnector.cs index 4d667b48f3..c01acb18c8 100644 --- a/Signum.Engine/Connection/SqlConnector.cs +++ b/Signum.Engine/Connection/SqlConnector.cs @@ -84,37 +84,22 @@ public enum EngineEdition public class SqlConnector : Connector { - int? commandTimeout = null; - string connectionString; + public static ResetLazy> DateFirstLazy = new ResetLazy>(() => Tuple.Create((byte)Executor.ExecuteScalar("SELECT @@DATEFIRST")!)); + public byte DateFirst => DateFirstLazy.Value.Item1; public SqlServerVersion Version { get; set; } public SqlConnector(string connectionString, Schema schema, SqlServerVersion version) : base(schema) { - this.connectionString = connectionString; + this.ConnectionString = connectionString; this.ParameterBuilder = new SqlParameterBuilder(); this.Version = version; - if (version >= SqlServerVersion.SqlServer2008 && schema != null) - { - var s = schema.Settings; - - if (!s.TypeValues.ContainsKey(typeof(TimeSpan))) - schema.Settings.TypeValues.Add(typeof(TimeSpan), SqlDbType.Time); - } } - public int? CommandTimeout - { - get { return commandTimeout; } - set { commandTimeout = value; } - } + public int? CommandTimeout { get; set; } = null; - public string ConnectionString - { - get { return connectionString; } - set { connectionString = value; } - } + public string ConnectionString { get; set; } public override bool SupportsScalarSubquery { get { return true; } } public override bool SupportsScalarSubqueryInAggregates { get { return false; } } @@ -354,32 +339,6 @@ protected internal override DataTable ExecuteDataTable(SqlPreCommandSimple preCo }); } - protected internal override DataSet ExecuteDataSet(SqlPreCommandSimple preCommand, CommandType commandType) - { - return EnsureConnectionRetry(con => - { - using (SqlCommand cmd = NewCommand(preCommand, con, commandType)) - using (HeavyProfiler.Log("SQL", () => preCommand.sp_executesql())) - { - try - { - SqlDataAdapter da = new SqlDataAdapter(cmd); - DataSet result = new DataSet(); - da.Fill(result); - return result; - } - catch (Exception ex) - { - var nex = HandleException(ex, preCommand); - if (nex == ex) - throw; - - throw nex; - } - } - }); - } - public Exception HandleException(Exception ex, SqlPreCommandSimple command) { var nex = ReplaceException(ex, command); @@ -415,7 +374,7 @@ Exception ReplaceException(Exception ex, SqlPreCommandSimple command) return ex; } - protected internal override void BulkCopy(DataTable dt, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout) + protected internal override void BulkCopy(DataTable dt, List columns, ObjectName destinationTable, SqlBulkCopyOptions options, int? timeout) { EnsureConnectionRetry(con => { @@ -439,12 +398,12 @@ protected internal override void BulkCopy(DataTable dt, ObjectName destinationTa public override string DatabaseName() { - return new SqlConnection(connectionString).Database; + return new SqlConnection(ConnectionString).Database; } public override string DataSourceName() { - return new SqlConnection(connectionString).DataSource; + return new SqlConnection(ConnectionString).DataSource; } public override void SaveTransactionPoint(DbTransaction transaction, string savePointName) @@ -457,9 +416,9 @@ public override void RollbackTransactionPoint(DbTransaction transaction, string ((SqlTransaction)transaction).Rollback(savePointName); } - public override SqlDbType GetSqlDbType(DbParameter p) + public override string GetSqlDbType(DbParameter p) { - return ((SqlParameter)p).SqlDbType; + return ((SqlParameter)p).SqlDbType.ToString().ToUpperInvariant(); } public override DbParameter CloneParameter(DbParameter p) @@ -475,10 +434,10 @@ public override DbConnection CreateConnection() public override ParameterBuilder ParameterBuilder { get; protected set; } - public override void CleanDatabase(DatabaseName? databaseName) + public override void CleanDatabase(DatabaseName? database) { - SqlConnectorScripts.RemoveAllScript(databaseName).ExecuteLeaves(); - SqlConnectorScripts.ShrinkDatabase(DatabaseName()); + SqlConnectorScripts.RemoveAllScript(database).ExecuteLeaves(); + ShrinkDatabase(database?.ToString() ?? DatabaseName()); } public override bool AllowsMultipleQueries @@ -486,12 +445,12 @@ public override bool AllowsMultipleQueries get { return true; } } - public SqlConnector ForDatabase(Maps.DatabaseName? database) + public override Connector ForDatabase(Maps.DatabaseName? database) { if (database == null) return this; - return new SqlConnector(Replace(connectionString, database), this.Schema, this.Version); + return new SqlConnector(Replace(ConnectionString, database), this.Schema, this.Version); } private static string Replace(string connectionString, DatabaseName item) @@ -514,7 +473,7 @@ public override bool AllowsIndexWithWhere(string Where) public static List ComplexWhereKeywords = new List { "OR" }; - public override SqlPreCommand ShrinkDatabase(string databaseName) + public SqlPreCommand ShrinkDatabase(string databaseName) { return new[] { @@ -559,15 +518,16 @@ public override bool SupportsTemporalTables get { return Version >= SqlServerVersion.SqlServer2016; } } - + public override int MaxNameLength => 128; + public override string ToString() => $"SqlConnector({Version})"; } public class SqlParameterBuilder : ParameterBuilder { - public override DbParameter CreateParameter(string parameterName, SqlDbType sqlType, string? udtTypeName, bool nullable, object? value) + public override DbParameter CreateParameter(string parameterName, AbstractDbType dbType, string? udtTypeName, bool nullable, object? value) { - if (IsDate(sqlType)) + if (dbType.IsDate()) AssertDateTime((DateTime?)value); var result = new SqlParameter(parameterName, value ?? DBNull.Value) @@ -575,18 +535,16 @@ public override DbParameter CreateParameter(string parameterName, SqlDbType sqlT IsNullable = nullable }; - result.SqlDbType = sqlType; - - if (sqlType == SqlDbType.Udt) + result.SqlDbType = dbType.SqlServer; + if (udtTypeName != null) result.UdtTypeName = udtTypeName; - return result; } - public override MemberInitExpression ParameterFactory(Expression parameterName, SqlDbType sqlType, string? udtTypeName, bool nullable, Expression value) + public override MemberInitExpression ParameterFactory(Expression parameterName, AbstractDbType dbType, string? udtTypeName, bool nullable, Expression value) { - Expression valueExpr = Expression.Convert(IsDate(sqlType) ? Expression.Call(miAsserDateTime, Expression.Convert(value, typeof(DateTime?))) : value, typeof(object)); + Expression valueExpr = Expression.Convert(dbType.IsDate() ? Expression.Call(miAsserDateTime, Expression.Convert(value, typeof(DateTime?))) : value, typeof(object)); if (nullable) valueExpr = Expression.Condition(Expression.Equal(value, Expression.Constant(null, value.Type)), @@ -599,10 +557,10 @@ public override MemberInitExpression ParameterFactory(Expression parameterName, List mb = new List() { Expression.Bind(typeof(SqlParameter).GetProperty("IsNullable"), Expression.Constant(nullable)), - Expression.Bind(typeof(SqlParameter).GetProperty("SqlDbType"), Expression.Constant(sqlType)), + Expression.Bind(typeof(SqlParameter).GetProperty("SqlDbType"), Expression.Constant(dbType.SqlServer)), }; - if (sqlType == SqlDbType.Udt) + if (udtTypeName != null) mb.Add(Expression.Bind(typeof(SqlParameter).GetProperty("UdtTypeName"), Expression.Constant(udtTypeName))); return Expression.MemberInit(newExpr, mb); @@ -725,8 +683,9 @@ close cur public static SqlPreCommand RemoveAllScript(DatabaseName? databaseName) { - var systemSchemas = SqlBuilder.SystemSchemas.ToString(a => "'" + a + "'", ", "); - var systemSchemasExeptDbo = SqlBuilder.SystemSchemas.Where(s => s != "dbo").ToString(a => "'" + a + "'", ", "); + var sqlBuilder = Connector.Current.SqlBuilder; + var systemSchemas = sqlBuilder.SystemSchemas.ToString(a => "'" + a + "'", ", "); + var systemSchemasExeptDbo = sqlBuilder.SystemSchemas.Where(s => s != "dbo").ToString(a => "'" + a + "'", ", "); return SqlPreCommand.Combine(Spacing.Double, new SqlPreCommandSimple(Use(databaseName, RemoveAllProceduresScript)), @@ -745,11 +704,5 @@ static string Use(DatabaseName? databaseName, string script) return "use " + databaseName + "\r\n" + script; } - - internal static SqlPreCommand ShrinkDatabase(string databaseName) - { - return Connector.Current.ShrinkDatabase(databaseName); - - } } } diff --git a/Signum.Engine/Database.cs b/Signum.Engine/Database.cs index a0a0c72dec..7c7aeed191 100644 --- a/Signum.Engine/Database.cs +++ b/Signum.Engine/Database.cs @@ -74,10 +74,11 @@ public static T Save(this T entity) public static int InsertView(this T viewObject) where T : IView { - var view = Schema.Current.View(); + var schema = Schema.Current; + var view = schema.View(); var parameters = view.GetInsertParameters(viewObject); - var sql = $@"INSERT {view.Name} ({view.Columns.ToString(p => p.Key.SqlEscape(), ", ")}) + var sql = $@"INSERT {view.Name} ({view.Columns.ToString(p => p.Key.SqlEscape(schema.Settings.IsPostgres), ", ")}) VALUES ({parameters.ToString(p => p.ParameterName, ", ")})"; return Executor.ExecuteNonQuery(sql, parameters); @@ -1466,7 +1467,7 @@ public static int UnsafeInsertDisableIdentity(this IQueryable query, strin using (Transaction tr = new Transaction()) { int result; - using (Administrator.DisableIdentity(Schema.Current.Table(typeof(E)).Name)) + using (Administrator.DisableIdentity(Schema.Current.Table(typeof(E)))) result = query.UnsafeInsert(a => a, message); return tr.Commit(result); } @@ -1478,7 +1479,7 @@ public static int UnsafeInsertDisableIdentity(this IQueryable query, Ex using (Transaction tr = new Transaction()) { int result; - using (Administrator.DisableIdentity(Schema.Current.Table(typeof(E)).Name)) + using (Administrator.DisableIdentity(Schema.Current.Table(typeof(E)))) result = query.UnsafeInsert(constructor, message); return tr.Commit(result); } diff --git a/Signum.Engine/Engine/PostgresCatalog.cs b/Signum.Engine/Engine/PostgresCatalog.cs new file mode 100644 index 0000000000..1175de4b36 --- /dev/null +++ b/Signum.Engine/Engine/PostgresCatalog.cs @@ -0,0 +1,216 @@ +using Signum.Entities; +using Signum.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. +namespace Signum.Engine.PostgresCatalog +{ + [TableName("pg_catalog.pg_namespace")] + public class PgNamespace : IView + { + [ViewPrimaryKey] + public int oid; + + public string nspname; + + [AutoExpressionField] + public IQueryable Tables() => + As.Expression(() => Database.View().Where(t => t.relnamespace == this.oid && t.relkind == RelKind.Table)); + } + + [TableName("pg_catalog.pg_class")] + public class PgClass : IView + { + [ViewPrimaryKey] + public int oid; + + public string relname; + public int relnamespace; + public char relkind; + public int reltuples; + + [AutoExpressionField] + public IQueryable Triggers() => + As.Expression(() => Database.View().Where(t => t.tgrelid == this.oid)); + + [AutoExpressionField] + public IQueryable Indices() => + As.Expression(() => Database.View().Where(t => t.indrelid == this.oid)); + + [AutoExpressionField] + public IQueryable Attributes() => + As.Expression(() => Database.View().Where(t => t.attrelid == this.oid)); + + [AutoExpressionField] + public IQueryable Constraints() => + As.Expression(() => Database.View().Where(t => t.conrelid == this.oid)); + + + [AutoExpressionField] + public PgNamespace Namespace() => + As.Expression(() => Database.View().SingleOrDefault(t => t.oid == this.relnamespace)); + } + + public static class RelKind + { + public const char Table = 'r'; + public const char Index = 'i'; + public const char Sequence = 's'; + public const char Toast = 't'; + public const char View = 'v'; + public const char MaterializedView = 'n'; + public const char CompositeType = 'c'; + public const char ForeignKey = 'f'; + public const char PartitionTable = 'p'; + public const char PartitionIndex = 'I'; + } + + [TableName("pg_catalog.pg_attribute")] + public class PgAttribute : IView + { + [ViewPrimaryKey] + public int attrelid; + + [ViewPrimaryKey] + public string attname; + + public int atttypid; + public int atttypmod; + + public short attlen; + public short attnum; + public bool attnotnull; + public char attidentity; + + [AutoExpressionField] + public PgType Type() => As.Expression(() => Database.View().SingleOrDefault(t => t.oid == this.atttypid)); + + [AutoExpressionField] + public PgAttributeDef? Default() => As.Expression(() => Database.View().SingleOrDefault(d => d.adrelid == this.attrelid && d.adnum == this.attnum)); + } + + [TableName("pg_catalog.pg_attrdef")] + public class PgAttributeDef : IView + { + [ViewPrimaryKey] + public int oid; + + public int adrelid; + + public short adnum; + + public string /*not really*/ adbin; + } + + [TableName("pg_catalog.pg_type")] + public class PgType : IView + { + [ViewPrimaryKey] + public int oid; + + public string typname; + + public int typnamespace; + public short typlen; + public bool typbyval; + } + + [TableName("pg_catalog.pg_trigger")] + public class PgTrigger : IView + { + [ViewPrimaryKey] + public int oid; + + public int tgrelid; + public string tgname; + public int tgfoid; + public byte[] tgargs; + + [AutoExpressionField] + public PgProc Proc() => + As.Expression(() => Database.View().SingleOrDefault(p => p.oid == this.tgfoid)); + } + + [TableName("pg_catalog.pg_proc")] + public class PgProc : IView + { + [ViewPrimaryKey] + public int oid; + + public int pronamespace; + + [AutoExpressionField] + public PgNamespace Namespace() => + As.Expression(() => Database.View().SingleOrDefault(t => t.oid == this.pronamespace)); + + public string proname; + } + + [TableName("pg_catalog.pg_index")] + public class PgIndex : IView + { + [ViewPrimaryKey] + public int indexrelid; + + public int indrelid; + + public short indnatts; + + public short indnkeyatts; + + public bool indisunique; + + public bool indisprimary; + + public short[] indkey; + + public string? indexprs; + public string? indpred; + + [AutoExpressionField] + public PgClass Class() => + As.Expression(() => Database.View().Single(t => t.oid == this.indexrelid)); + } + + + [TableName("pg_catalog.pg_constraint")] + public class PgConstraint : IView + { + [ViewPrimaryKey] + public int oid; + + public string conname; + + public int connamespace; + + public char contype; + + public int conrelid; + + public short[] conkey; + + public int confrelid; + public short[] confkey; + + [AutoExpressionField] + public PgClass TargetTable() => + As.Expression(() => Database.View().Single(t => t.oid == this.confrelid)); + + [AutoExpressionField] + public PgNamespace Namespace() => + As.Expression(() => Database.View().SingleOrDefault(t => t.oid == this.connamespace)); + } + + public static class ConstraintType + { + public const char Check = 'c'; + public const char ForeignKey = 'f'; + public const char PrimaryKey = 'p'; + public const char Unique = 'u'; + public const char Trigger= 't'; + public const char Exclusion = 'x'; + } +} +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. diff --git a/Signum.Engine/Engine/PostgresCatalogSchema.cs b/Signum.Engine/Engine/PostgresCatalogSchema.cs new file mode 100644 index 0000000000..8a280a9f4d --- /dev/null +++ b/Signum.Engine/Engine/PostgresCatalogSchema.cs @@ -0,0 +1,240 @@ +using Signum.Engine.Maps; +using Signum.Engine.PostgresCatalog; +using Signum.Utilities; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using NpgsqlTypes; +using static Signum.Engine.PostgresCatalog.PostgresFunctions; + +namespace Signum.Engine.Engine +{ + public static class PostgresCatalogSchema + { + public static string[] systemSchemas = new[] { "pg_catalog", "pg_toast", "information_schema" }; + + public static Dictionary GetDatabaseDescription(List databases) + { + List allTables = new List(); + + var isPostgres = Schema.Current.Settings.IsPostgres; + + foreach (var db in databases) + { + SafeConsole.WriteColor(ConsoleColor.Cyan, '.'); + + using (Administrator.OverrideDatabaseInSysViews(db)) + { + var databaseName = db == null ? Connector.Current.DatabaseName() : db.Name; + + //var sysDb = Database.View().Single(a => a.name == databaseName); + + var con = Connector.Current; + + var tables = + (from s in Database.View() + where !systemSchemas.Contains(s.nspname) + from t in s.Tables() + select new DiffTable + { + Name = new ObjectName(new SchemaName(db, s.nspname, isPostgres), t.relname, isPostgres), + + TemporalType = t.Triggers().Any(t => t.Proc().proname == "versioning") ? Signum.Engine.SysTableTemporalType.SystemVersionTemporalTable : SysTableTemporalType.None, + + //Period = !con.SupportsTemporalTables ? null : + + //(from p in t.Periods() + // join sc in t.Columns() on p.start_column_id equals sc.column_id + // join ec in t.Columns() on p.end_column_id equals ec.column_id + // select new DiffPeriod + // { + // StartColumnName = sc.name, + // EndColumnName = ec.name, + // }).SingleOrDefaultEx(), + + TemporalTableName = t.Triggers() + .Where(t => t.Proc().proname == "versioning") + .Select(t => ParseVersionFunctionParam(t.tgargs)) + .SingleOrDefaultEx(), + + //TemporalTableName = !con.SupportsTemporalTables || t.history_table_id == null ? null : + // Database.View() + // .Where(ht => ht.object_id == t.history_table_id) + // .Select(ht => new ObjectName(new SchemaName(db, ht.Schema().name, isPostgres), ht.name, isPostgres)) + // .SingleOrDefault(), + + PrimaryKeyName = (from c in t.Constraints() + where c.contype == ConstraintType.PrimaryKey + select c.conname == null ? null : new ObjectName(new SchemaName(db, c.Namespace().nspname, isPostgres), c.conname, isPostgres)) + .SingleOrDefaultEx(), + + Columns = (from c in t.Attributes() + let def = c.Default() + select new DiffColumn + { + Name = c.attname, + DbType = new AbstractDbType(ToNpgsqlDbType(c.Type().typname)), + UserTypeName = null, + Nullable = !c.attnotnull, + Collation = null, + Length = PostgresFunctions._pg_char_max_length(c.atttypid, c.atttypmod) ?? -1, + Precision = c.atttypid == 1700 /*numeric*/ ? ((c.atttypmod - 4) >> 16) & 65535 : 0, + Scale = c.atttypid == 1700 /*numeric*/ ? (c.atttypmod - 4) & 65535 : 0, + Identity = c.attidentity == 'a', + GeneratedAlwaysType = GeneratedAlwaysType.None, + DefaultConstraint = def == null ? null : new DiffDefaultConstraint + { + Definition = pg_get_expr(def.adbin, def.adrelid), + }, + PrimaryKey = t.Indices().Any(i => i.indisprimary && i.indkey.Contains(c.attnum)), + }).ToDictionaryEx(a => a.Name, "columns"), + + MultiForeignKeys = (from fk in t.Constraints() + where fk.contype == ConstraintType.ForeignKey + select new DiffForeignKey + { + Name = new ObjectName(new SchemaName(db, fk.Namespace().nspname, isPostgres), fk.conname, isPostgres), + IsDisabled = false, + TargetTable = new ObjectName(new SchemaName(db, fk.TargetTable().Namespace().nspname, isPostgres), fk.TargetTable().relname, isPostgres), + Columns = PostgresFunctions.generate_subscripts(fk.conkey, 1).Select(i => new DiffForeignKeyColumn + { + Parent = t.Attributes().Single(c => c.attnum == fk.conkey[i]).attname, + Referenced = fk.TargetTable().Attributes().Single(c => c.attnum == fk.confkey[i]).attname, + }).ToList(), + }).ToList(), + + SimpleIndices = (from ix in t.Indices() + select new DiffIndex + { + IsUnique = ix.indisunique, + IsPrimary = ix.indisprimary, + IndexName = ix.Class().relname, + FilterDefinition = PostgresFunctions.pg_get_expr(ix.indpred!, ix.indrelid), + Type = DiffIndexType.NonClustered, + Columns = (from i in PostgresFunctions.generate_subscripts(ix.indkey, 1) + let at = t.Attributes().Single(a => a.attnum == ix.indkey[i]) + orderby i + select new DiffIndexColumn { ColumnName = at.attname, IsIncluded = i >= ix.indnkeyatts }).ToList() + }).ToList(), + + ViewIndices = new List(), + + Stats = new List(), + + }).ToList(); + + + if (SchemaSynchronizer.IgnoreTable != null) + tables.RemoveAll(SchemaSynchronizer.IgnoreTable); + + tables.ForEach(t => t.Columns.RemoveAll(c => c.Value.DbType.PostgreSql == (NpgsqlDbType)(-1))); + + tables.ForEach(t => t.ForeignKeysToColumns()); + + allTables.AddRange(tables); + } + } + + var database = allTables.ToDictionary(t => t.Name.ToString()); + + var historyTables = database.Values.Select(a => a.TemporalTableName).NotNull().ToList(); + + historyTables.ForEach(h => + { + var t = database.TryGetC(h.ToString()); + if (t != null) + t.TemporalType = SysTableTemporalType.HistoryTable; + }); + + return database; + } + + private static ObjectName? ParseVersionFunctionParam(byte[]? tgargs) + { + if (tgargs == null) + return null; + + var str = Encoding.UTF8.GetString(tgargs!); + + var args = str.Split("\0"); + + return ObjectName.Parse(args[1], isPostgres: true); + } + + public static NpgsqlDbType ToNpgsqlDbType(string str) + { + switch (str) + { + case "bool": return NpgsqlDbType.Boolean; + case "bytea": return NpgsqlDbType.Bytea; + case "char": return NpgsqlDbType.Char; + case "int8": return NpgsqlDbType.Bigint; + case "int2": return NpgsqlDbType.Smallint; + case "float4": return NpgsqlDbType.Real; + case "float8": return NpgsqlDbType.Double; + case "int2vector": return NpgsqlDbType.Smallint | NpgsqlDbType.Array; + case "int4": return NpgsqlDbType.Integer; + case "text": return NpgsqlDbType.Text; + case "json": return NpgsqlDbType.Json; + case "xml": return NpgsqlDbType.Xml; + case "point": return NpgsqlDbType.Point; + case "lseg": return NpgsqlDbType.LSeg; + case "path": return NpgsqlDbType.Path; + case "box": return NpgsqlDbType.Box; + case "polygon": return NpgsqlDbType.Polygon; + case "line": return NpgsqlDbType.Line; + case "circle": return NpgsqlDbType.Circle; + case "money": return NpgsqlDbType.Money; + case "macaddr": return NpgsqlDbType.MacAddr; + case "macaddr8": return NpgsqlDbType.MacAddr8; + case "inet": return NpgsqlDbType.Inet; + case "varchar": return NpgsqlDbType.Varchar; + case "date": return NpgsqlDbType.Date; + case "time": return NpgsqlDbType.Time; + case "timestamp": return NpgsqlDbType.Timestamp; + case "timestamptz": return NpgsqlDbType.TimestampTz; + case "interval": return NpgsqlDbType.Interval; + case "timetz": return NpgsqlDbType.TimestampTz; + case "bit": return NpgsqlDbType.Bit; + case "varbit": return NpgsqlDbType.Varbit; + case "numeric": return NpgsqlDbType.Numeric; + case "uuid": return NpgsqlDbType.Uuid; + case "tsvector": return NpgsqlDbType.TsVector; + case "tsquery": return NpgsqlDbType.TsQuery; + case "jsonb": return NpgsqlDbType.Jsonb; + case "int4range": return NpgsqlDbType.Range | NpgsqlDbType.Integer; + case "numrange": return NpgsqlDbType.Range | NpgsqlDbType.Numeric; + case "tsrange": return NpgsqlDbType.Range | NpgsqlDbType.Timestamp; + case "tstzrange": return NpgsqlDbType.Range | NpgsqlDbType.TimestampTz; + case "daterange": return NpgsqlDbType.Range | NpgsqlDbType.Date; + case "int8range": return NpgsqlDbType.Range | NpgsqlDbType.Bigint; + case "oid": + case "cid": + case "xid": + case "tid": + return (NpgsqlDbType)(-1); + default: + return (NpgsqlDbType)(-1); + } + + } + + public static HashSet GetSchemaNames(List list) + { + var sqlBuilder = Connector.Current.SqlBuilder; + var isPostgres = sqlBuilder.IsPostgres; + HashSet result = new HashSet(); + foreach (var db in list) + { + using (Administrator.OverrideDatabaseInSysViews(db)) + { + var schemaNames = Database.View().Select(s => s.nspname).ToList(); + + result.AddRange(schemaNames.Except(systemSchemas).Select(sn => new SchemaName(db, sn, isPostgres)).Where(a => !SchemaSynchronizer.IgnoreSchema(a))); + } + } + return result; + } + } +} diff --git a/Signum.Engine/Engine/PostgresFunctions.cs b/Signum.Engine/Engine/PostgresFunctions.cs new file mode 100644 index 0000000000..ec2ee8b7cd --- /dev/null +++ b/Signum.Engine/Engine/PostgresFunctions.cs @@ -0,0 +1,48 @@ +using Microsoft.SqlServer.Server; +using System; +using System.Linq; + +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. +namespace Signum.Engine.PostgresCatalog +{ + public class PostgresFunctions + { + [SqlMethod(Name = "pg_catalog.string_to_array")] + public static string[] string_to_array(string str, string separator) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.encode")] + public static string encode(byte[] bytea, string format) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.pg_get_expr")] + public static string pg_get_expr(string adbin, int adrelid) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.pg_get_viewdef")] + public static string pg_get_viewdef(int oid) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.pg_get_functiondef")] + public static string pg_get_functiondef(int oid) => throw new NotImplementedException(); + + [SqlMethod(Name = "information_schema._pg_char_max_length")] + public static int? _pg_char_max_length(int atttypeid, int atttypmod) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.unnest")] + public static IQueryable unnest(T[] array) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.generate_series")] + public static IQueryable generate_series(int start, int stopIncluded) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.generate_series")] + public static IQueryable generate_series(int start, int stopIncluded, int step) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.generate_subscripts")] + public static IQueryable generate_subscripts(Array array, int dimension) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.generate_subscripts")] + public static IQueryable generate_subscripts(Array array, int dimension, bool reverse) => throw new NotImplementedException(); + + [SqlMethod(Name = "pg_catalog.pg_total_relation_size")] + public static int pg_total_relation_size(int oid) => throw new NotImplementedException(); + } + +} +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. diff --git a/Signum.Engine/Engine/ProgressExtensions.cs b/Signum.Engine/Engine/ProgressExtensions.cs index 4deb7c1d57..2acc15e443 100644 --- a/Signum.Engine/Engine/ProgressExtensions.cs +++ b/Signum.Engine/Engine/ProgressExtensions.cs @@ -77,28 +77,15 @@ public static void ProgressForeach(this IEnumerable collection, Table table = Schema.Current.Table(disableIdentityFor); - if (!table.IdentityBehaviour) - throw new InvalidOperationException("Identity is false already"); - - table.IdentityBehaviour = false; - try + collection.ProgressForeachInternal(elementID, writer, parallelOptions, transactional, showProgress, action: item => { - collection.ProgressForeachInternal(elementID, writer, parallelOptions, transactional, showProgress, action: item => + using (Transaction tr = Transaction.ForceNew()) { - using (Transaction tr = Transaction.ForceNew()) - { - using (table.PrimaryKey.Default != null - ? null - : Administrator.DisableIdentity(table.Name)) - action!(item); - tr.Commit(); - } - }); - } - finally - { - table.IdentityBehaviour = true; - } + using (table.PrimaryKey.Default != null ? null : Administrator.DisableIdentity(table)) + action!(item); + tr.Commit(); + } + }); } } @@ -292,6 +279,5 @@ public static LogWriter GetConsoleWriter() public delegate void LogWriter(ConsoleColor color, string text, params object[] parameters); - } } diff --git a/Signum.Engine/Engine/SchemaGenerator.cs b/Signum.Engine/Engine/SchemaGenerator.cs index f364c5893d..4d953ae185 100644 --- a/Signum.Engine/Engine/SchemaGenerator.cs +++ b/Signum.Engine/Engine/SchemaGenerator.cs @@ -4,6 +4,7 @@ using Signum.Utilities; using Signum.Entities; using Signum.Engine.SchemaInfoTables; +using System.IO; namespace Signum.Engine { @@ -12,32 +13,37 @@ public static class SchemaGenerator public static SqlPreCommand? CreateSchemasScript() { Schema s = Schema.Current; + var sqlBuilder = Connector.Current.SqlBuilder; + var defaultSchema = SchemaName.Default(s.Settings.IsPostgres); - return s.GetDatabaseTables() + var schemas = s.GetDatabaseTables() .Select(a => a.Name.Schema) - .Where(sn => sn.Name != "dbo" && !s.IsExternalDatabase(sn.Database)) - .Distinct() - .Select(SqlBuilder.CreateSchema) + .Where(sn => !sn.Equals(defaultSchema) && !s.IsExternalDatabase(sn.Database)) + .Distinct(); + + return schemas + .Select(sqlBuilder.CreateSchema) .Combine(Spacing.Simple); } public static SqlPreCommand? CreateTablesScript() { + var sqlBuilder = Connector.Current.SqlBuilder; Schema s = Schema.Current; List tables = s.GetDatabaseTables().Where(t => !s.IsExternalDatabase(t.Name.Schema.Database)).ToList(); - SqlPreCommand? createTables = tables.Select(t => SqlBuilder.CreateTableSql(t)).Combine(Spacing.Double)?.PlainSqlCommand(); + SqlPreCommand? createTables = tables.Select(t => sqlBuilder.CreateTableSql(t)).Combine(Spacing.Double)?.PlainSqlCommand(); - SqlPreCommand? foreignKeys = tables.Select(SqlBuilder.AlterTableForeignKeys).Combine(Spacing.Double)?.PlainSqlCommand(); + SqlPreCommand? foreignKeys = tables.Select(sqlBuilder.AlterTableForeignKeys).Combine(Spacing.Double)?.PlainSqlCommand(); SqlPreCommand? indices = tables.Select(t => { - var allIndexes = t.GeneratAllIndexes().Where(a => !(a is PrimaryClusteredIndex)); ; + var allIndexes = t.GeneratAllIndexes().Where(a => !(a is PrimaryKeyIndex)); ; - var mainIndices = allIndexes.Select(ix => SqlBuilder.CreateIndex(ix, checkUnique: null)).Combine(Spacing.Simple); + var mainIndices = allIndexes.Select(ix => sqlBuilder.CreateIndex(ix, checkUnique: null)).Combine(Spacing.Simple); var historyIndices = t.SystemVersioned == null ? null : - allIndexes.Where(a => a.GetType() == typeof(TableIndex)).Select(mix => SqlBuilder.CreateIndexBasic(mix, forHistoryTable: true)).Combine(Spacing.Simple); + allIndexes.Where(a => a.GetType() == typeof(TableIndex)).Select(mix => sqlBuilder.CreateIndexBasic(mix, forHistoryTable: true)).Combine(Spacing.Simple); return SqlPreCommand.Combine(Spacing.Double, mainIndices, historyIndices); @@ -56,28 +62,56 @@ select EnumEntity.GetEntities(enumType).Select((e, i) => t.InsertSqlSync(e, suff ).Combine(Spacing.Double)?.PlainSqlCommand(); } + public static SqlPreCommand? PostgresExtensions() + { + if (!Schema.Current.Settings.IsPostgres) + return null; + + return Schema.Current.PostgresExtensions.Select(p => Connector.Current.SqlBuilder.CreateExtensionIfNotExist(p)).Combine(Spacing.Simple); + } + + public static SqlPreCommand? PostgreeTemporalTableScript() + { + if (!Schema.Current.Settings.IsPostgres) + return null; + + if (!Schema.Current.Tables.Any(t => t.Value.SystemVersioned != null)) + return null; + + var file = Schema.Current.Settings.PostresVersioningFunctionNoChecks ? + "versioning_function_nochecks.sql" : + "versioning_function.sql"; + + var text = new StreamReader(typeof(Schema).Assembly.GetManifestResourceStream($"Signum.Engine.Engine.Scripts.{file}")!).Using(a => a.ReadToEnd()); + + return new SqlPreCommandSimple(text); + } public static SqlPreCommand? SnapshotIsolation() { - if (!Connector.Current.AllowsSetSnapshotIsolation) + var connector = Connector.Current; + + if (!connector.AllowsSetSnapshotIsolation) return null; - var list = Schema.Current.DatabaseNames().Select(a => a?.ToString()).ToList(); + var list = connector.Schema.DatabaseNames().Select(a => a?.ToString()).ToList(); if (list.Contains(null)) { list.Remove(null); - list.Add(Connector.Current.DatabaseName()); + list.Add(connector.DatabaseName()); } + var sqlBuilder = connector.SqlBuilder; + var cmd = list.NotNull() .Where(db => !SnapshotIsolationEnabled(db)) .Select(db => SqlPreCommand.Combine(Spacing.Simple, - SqlBuilder.SetSingleUser(db), - SqlBuilder.SetSnapshotIsolation(db, true), - SqlBuilder.MakeSnapshotIsolationDefault(db, true), - SqlBuilder.SetMultiUser(db)) + sqlBuilder.SetSingleUser(db), + sqlBuilder.SetSnapshotIsolation(db, true), + sqlBuilder.MakeSnapshotIsolationDefault(db, true), + sqlBuilder.SetMultiUser(db)) ).Combine(Spacing.Double); return cmd; diff --git a/Signum.Engine/Engine/SchemaSynchronizer.cs b/Signum.Engine/Engine/SchemaSynchronizer.cs index 048003d9cf..c3b417e674 100644 --- a/Signum.Engine/Engine/SchemaSynchronizer.cs +++ b/Signum.Engine/Engine/SchemaSynchronizer.cs @@ -1,3 +1,5 @@ +using NpgsqlTypes; +using Signum.Engine.Engine; using Signum.Engine.Linq; using Signum.Engine.Maps; using Signum.Engine.SchemaInfoTables; @@ -22,13 +24,20 @@ public static class SchemaSynchronizer { Schema s = Schema.Current; + var sqlBuilder = Connector.Current.SqlBuilder; + Dictionary modelTables = s.GetDatabaseTables().Where(t => !s.IsExternalDatabase(t.Name.Schema.Database)).ToDictionaryEx(a => a.Name.ToString(), "schema tables"); var modelTablesHistory = modelTables.Values.Where(a => a.SystemVersioned != null).ToDictionaryEx(a => a.SystemVersioned!.TableName.ToString(), "history schema tables"); - HashSet modelSchemas = modelTables.Values.Select(a => a.Name.Schema).Where(a => !SqlBuilder.SystemSchemas.Contains(a.Name)).ToHashSet(); + HashSet modelSchemas = modelTables.Values.Select(a => a.Name.Schema).Where(a => !sqlBuilder.SystemSchemas.Contains(a.Name)).ToHashSet(); - Dictionary databaseTables = DefaultGetDatabaseDescription(s.DatabaseNames()); + Dictionary databaseTables = Schema.Current.Settings.IsPostgres ? + PostgresCatalogSchema.GetDatabaseDescription(Schema.Current.DatabaseNames()) : + SysTablesSchema.GetDatabaseDescription(s.DatabaseNames()); + var databaseTablesHistory = databaseTables.Extract((key, val) => val.TemporalType == SysTableTemporalType.HistoryTable); - HashSet databaseSchemas = DefaultGetSchemas(s.DatabaseNames()); + HashSet databaseSchemas = Schema.Current.Settings.IsPostgres ? + PostgresCatalogSchema.GetSchemaNames(s.DatabaseNames()): + SysTablesSchema.GetSchemaNames(s.DatabaseNames()); SimplifyDiffTables?.Invoke(databaseTables); @@ -103,7 +112,7 @@ public static class SchemaSynchronizer using (replacements.WithReplacedDatabaseName()) { SqlPreCommand? preRenameColumns = preRenameColumnsList - .Select(kvp => kvp.Value.Select(kvp2 => SqlBuilder.RenameColumn(kvp.Key, kvp2.Key, kvp2.Value)).Combine(Spacing.Simple)) + .Select(kvp => kvp.Value.Select(kvp2 => sqlBuilder.RenameColumn(kvp.Key, kvp2.Key, kvp2.Value)).Combine(Spacing.Simple)) .Combine(Spacing.Double); if (preRenameColumns != null) @@ -112,28 +121,28 @@ public static class SchemaSynchronizer SqlPreCommand? createSchemas = Synchronizer.SynchronizeScriptReplacing(replacements, "Schemas", Spacing.Double, modelSchemas.ToDictionary(a => a.ToString()), databaseSchemas.ToDictionary(a => a.ToString()), - createNew: (_, newSN) => SqlBuilder.CreateSchema(newSN), + createNew: (_, newSN) => sqlBuilder.CreateSchema(newSN), removeOld: null, - mergeBoth: (_, newSN, oldSN) => newSN.Equals(oldSN) ? null : SqlBuilder.CreateSchema(newSN) + mergeBoth: (_, newSN, oldSN) => newSN.Equals(oldSN) ? null : sqlBuilder.CreateSchema(newSN) ); //use database without replacements to just remove indexes SqlPreCommand? dropStatistics = Synchronizer.SynchronizeScript(Spacing.Double, modelTables, databaseTables, createNew: null, - removeOld: (tn, dif) => SqlBuilder.DropStatistics(tn, dif.Stats), + removeOld: (tn, dif) => sqlBuilder.DropStatistics(tn, dif.Stats), mergeBoth: (tn, tab, dif) => { var removedColums = dif.Columns.Keys.Except(tab.Columns.Keys).ToHashSet(); - return SqlBuilder.DropStatistics(tn, dif.Stats.Where(a => a.Columns.Any(removedColums.Contains)).ToList()); + return sqlBuilder.DropStatistics(tn, dif.Stats.Where(a => a.Columns.Any(removedColums.Contains)).ToList()); }); SqlPreCommand? dropIndices = Synchronizer.SynchronizeScript(Spacing.Double, modelTables, databaseTables, createNew: null, - removeOld: (tn, dif) => dif.Indices.Values.Where(ix => !ix.IsPrimary).Select(ix => SqlBuilder.DropIndex(dif.Name, ix)).Combine(Spacing.Simple), + removeOld: (tn, dif) => dif.Indices.Values.Where(ix => !ix.IsPrimary).Select(ix => sqlBuilder.DropIndex(dif.Name, ix)).Combine(Spacing.Simple), mergeBoth: (tn, tab, dif) => { Dictionary modelIxs = modelIndices[tab]; @@ -141,11 +150,11 @@ public static class SchemaSynchronizer var removedColums = dif.Columns.Keys.Except(tab.Columns.Keys).ToHashSet(); var changes = Synchronizer.SynchronizeScript(Spacing.Simple, - modelIxs.Where(kvp => !(kvp.Value is PrimaryClusteredIndex)).ToDictionary(), + modelIxs.Where(kvp => !(kvp.Value is PrimaryKeyIndex)).ToDictionary(), dif.Indices.Where(kvp =>!kvp.Value.IsPrimary).ToDictionary(), createNew: null, - removeOld: (i, dix) => dix.Columns.Any(c => removedColums.Contains(c.ColumnName)) || dix.IsControlledIndex ? SqlBuilder.DropIndex(dif.Name, dix) : null, - mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? SqlBuilder.DropIndex(dif.Name, dix) : null + removeOld: (i, dix) => dix.Columns.Any(c => removedColums.Contains(c.ColumnName)) || dix.IsControlledIndex ? sqlBuilder.DropIndex(dif.Name, dix) : null, + mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? sqlBuilder.DropIndex(dif.Name, dix) : null ); return changes; @@ -154,7 +163,7 @@ public static class SchemaSynchronizer SqlPreCommand? dropIndicesHistory = Synchronizer.SynchronizeScript(Spacing.Double, modelTablesHistory, databaseTablesHistory, createNew: null, - removeOld: (tn, dif) => dif.Indices.Values.Where(ix => ix.Type != DiffIndexType.Clustered).Select(ix => SqlBuilder.DropIndex(dif.Name, ix)).Combine(Spacing.Simple), + removeOld: (tn, dif) => dif.Indices.Values.Where(ix => !ix.IsPrimary).Select(ix => sqlBuilder.DropIndex(dif.Name, ix)).Combine(Spacing.Simple), mergeBoth: (tn, tab, dif) => { Dictionary modelIxs = modelIndices[tab]; @@ -163,10 +172,10 @@ public static class SchemaSynchronizer var changes = Synchronizer.SynchronizeScript(Spacing.Simple, modelIxs.Where(kvp => kvp.Value.GetType() == typeof(TableIndex)).ToDictionary(), - dif.Indices.Where(kvp => kvp.Value.Type != DiffIndexType.Clustered).ToDictionary(), + dif.Indices.Where(kvp => !kvp.Value.IsPrimary).ToDictionary(), createNew: null, - removeOld: (i, dix) => dix.Columns.Any(c => removedColums.Contains(c.ColumnName)) || dix.IsControlledIndex ? SqlBuilder.DropIndex(dif.Name, dix) : null, - mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? SqlBuilder.DropIndex(dif.Name, dix) : null + removeOld: (i, dix) => dix.Columns.Any(c => removedColums.Contains(c.ColumnName)) || dix.IsControlledIndex ? sqlBuilder.DropIndex(dif.Name, dix) : null, + mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? sqlBuilder.DropIndex(dif.Name, dix) : null ); return changes; @@ -177,20 +186,20 @@ public static class SchemaSynchronizer modelTables, databaseTables, createNew: null, - removeOld: (tn, dif) => dif.Columns.Values.Select(c => c.ForeignKey != null ? SqlBuilder.AlterTableDropConstraint(dif.Name, c.ForeignKey.Name) : null) - .Concat(dif.MultiForeignKeys.Select(fk => SqlBuilder.AlterTableDropConstraint(dif.Name, fk.Name))).Combine(Spacing.Simple), + removeOld: (tn, dif) => dif.Columns.Values.Select(c => c.ForeignKey != null ? sqlBuilder.AlterTableDropConstraint(dif.Name, c.ForeignKey.Name) : null) + .Concat(dif.MultiForeignKeys.Select(fk => sqlBuilder.AlterTableDropConstraint(dif.Name, fk.Name))).Combine(Spacing.Simple), mergeBoth: (tn, tab, dif) => SqlPreCommand.Combine(Spacing.Simple, Synchronizer.SynchronizeScript( Spacing.Simple, tab.Columns, dif.Columns, createNew: null, - removeOld: (cn, colDb) => colDb.ForeignKey != null ? SqlBuilder.AlterTableDropConstraint(dif.Name, colDb.ForeignKey.Name) : null, + removeOld: (cn, colDb) => colDb.ForeignKey != null ? sqlBuilder.AlterTableDropConstraint(dif.Name, colDb.ForeignKey.Name) : null, mergeBoth: (cn, colModel, colDb) => colDb.ForeignKey == null ? null : - colModel.ReferenceTable == null || colModel.AvoidForeignKey || !colModel.ReferenceTable.Name.Equals(ChangeName(colDb.ForeignKey.TargetTable)) || DifferentDatabase(tab.Name, colModel.ReferenceTable.Name) || colDb.SqlDbType != colModel.SqlDbType ? - SqlBuilder.AlterTableDropConstraint(dif.Name, colDb.ForeignKey.Name) : + colModel.ReferenceTable == null || colModel.AvoidForeignKey || !colModel.ReferenceTable.Name.Equals(ChangeName(colDb.ForeignKey.TargetTable)) || DifferentDatabase(tab.Name, colModel.ReferenceTable.Name) || !colDb.DbType.Equals(colModel.DbType) ? + sqlBuilder.AlterTableDropConstraint(dif.Name, colDb.ForeignKey.Name) : null), - dif.MultiForeignKeys.Select(fk => SqlBuilder.AlterTableDropConstraint(dif.Name, fk.Name)).Combine(Spacing.Simple)) + dif.MultiForeignKeys.Select(fk => sqlBuilder.AlterTableDropConstraint(dif.Name, fk.Name)).Combine(Spacing.Simple)) ); HashSet hasValueFalse = new HashSet(); @@ -205,30 +214,30 @@ public static class SchemaSynchronizer modelTables, databaseTables, createNew: (tn, tab) => SqlPreCommand.Combine(Spacing.Double, - SqlBuilder.CreateTableSql(tab) + sqlBuilder.CreateTableSql(tab) ), - removeOld: (tn, dif) => SqlBuilder.DropTable(dif), + removeOld: (tn, dif) => sqlBuilder.DropTable(dif), mergeBoth: (tn, tab, dif) => { - var rename = !object.Equals(dif.Name, tab.Name) ? SqlBuilder.RenameOrMove(dif, tab, tab.Name) : null; + var rename = !object.Equals(dif.Name, tab.Name) ? sqlBuilder.RenameOrMove(dif, tab, tab.Name) : null; bool disableEnableSystemVersioning = false; - var disableSystemVersioning = (dif.TemporalType != SysTableTemporalType.None && + var disableSystemVersioning = !sqlBuilder.IsPostgres && dif.TemporalType != SysTableTemporalType.None && (tab.SystemVersioned == null || !object.Equals(replacements.Apply(Replacements.KeyTables, dif.TemporalTableName!.ToString()), tab.SystemVersioned.TableName.ToString()) || - (disableEnableSystemVersioning = StrongColumnChanges(tab, dif)))) ? - SqlBuilder.AlterTableDisableSystemVersioning(tab.Name).Do(a => a.GoAfter = true) : + (disableEnableSystemVersioning = StrongColumnChanges(tab, dif))) ? + sqlBuilder.AlterTableDisableSystemVersioning(tab.Name).Do(a => a.GoAfter = true) : null; - var dropPeriod = (dif.Period != null && + var dropPeriod = !sqlBuilder.IsPostgres && dif.Period != null && (tab.SystemVersioned == null || !dif.Period.PeriodEquals(tab.SystemVersioned)) ? - SqlBuilder.AlterTableDropPeriod(tab) : null); + sqlBuilder.AlterTableDropPeriod(tab) : null; - var modelPK = modelIndices[tab].Values.OfType().SingleOrDefaultEx(); - var diffPK = dif.Indices.Values.SingleOrDefaultEx(a => a.Type == DiffIndexType.Clustered); + var modelPK = modelIndices[tab].Values.OfType().SingleOrDefaultEx(); + var diffPK = dif.Indices.Values.SingleOrDefaultEx(a => a.IsPrimary); - var dropPrimaryKey = diffPK != null && (modelPK == null || !diffPK.IndexEquals(dif, modelPK)) ? SqlBuilder.DropIndex(tab.Name, diffPK) : null; + var dropPrimaryKey = diffPK != null && (modelPK == null || !diffPK.IndexEquals(dif, modelPK)) ? sqlBuilder.DropIndex(tab.Name, diffPK) : null; var columns = Synchronizer.SynchronizeScript( Spacing.Simple, @@ -236,14 +245,14 @@ public static class SchemaSynchronizer dif.Columns, createNew: (cn, tabCol) => SqlPreCommand.Combine(Spacing.Simple, - tabCol.PrimaryKey && dif.PrimaryKeyName != null ? SqlBuilder.DropPrimaryKeyConstraint(tab.Name) : null, - AlterTableAddColumnDefault(tab, tabCol, replacements, + tabCol.PrimaryKey && dif.PrimaryKeyName != null ? sqlBuilder.DropPrimaryKeyConstraint(tab.Name) : null, + AlterTableAddColumnDefault(sqlBuilder, tab, tabCol, replacements, forceDefaultValue: cn.EndsWith("_HasValue") && dif.Columns.Values.Any(c => c.Name.StartsWith(cn.Before("HasValue")) && c.Nullable == false) ? "1" : null, hasValueFalse: hasValueFalse)), removeOld: (cn, difCol) => SqlPreCommand.Combine(Spacing.Simple, - difCol.DefaultConstraint != null ? SqlBuilder.AlterTableDropConstraint(tab.Name, difCol.DefaultConstraint.Name) : null, - SqlBuilder.AlterTableDropColumn(tab, cn)), + difCol.DefaultConstraint != null && difCol.DefaultConstraint.Name != null ? sqlBuilder.AlterTableDropConstraint(tab.Name, difCol.DefaultConstraint!.Name) : null, + sqlBuilder.AlterTableDropColumn(tab, cn)), mergeBoth: (cn, tabCol, difCol) => { @@ -251,30 +260,32 @@ public static class SchemaSynchronizer { return SqlPreCommand.Combine(Spacing.Simple, - difCol.Name == tabCol.Name ? null : SqlBuilder.RenameColumn(tab.Name, difCol.Name, tabCol.Name), + difCol.Name == tabCol.Name ? null : sqlBuilder.RenameColumn(tab.Name, difCol.Name, tabCol.Name), difCol.ColumnEquals(tabCol, ignorePrimaryKey: true, ignoreIdentity: false, ignoreGenerateAlways: true) ? null : SqlPreCommand.Combine(Spacing.Simple, - tabCol.PrimaryKey && !difCol.PrimaryKey && dif.PrimaryKeyName != null ? SqlBuilder.DropPrimaryKeyConstraint(tab.Name) : null, - UpdateCompatible(replacements, tab, dif, tabCol, difCol), - tabCol.SqlDbType == SqlDbType.NVarChar && difCol.SqlDbType == SqlDbType.NChar ? SqlBuilder.UpdateTrim(tab, tabCol) : null), + tabCol.PrimaryKey && !difCol.PrimaryKey && dif.PrimaryKeyName != null ? sqlBuilder.DropPrimaryKeyConstraint(tab.Name) : null, + UpdateCompatible(sqlBuilder, replacements, tab, dif, tabCol, difCol), + (sqlBuilder.IsPostgres ? + tabCol.DbType.PostgreSql == NpgsqlDbType.Varchar && difCol.DbType.PostgreSql == NpgsqlDbType.Char : + tabCol.DbType.SqlServer == SqlDbType.NVarChar && difCol.DbType.SqlServer == SqlDbType.NChar)? sqlBuilder.UpdateTrim(tab, tabCol) : null), UpdateByFkChange(tn, difCol, tabCol, ChangeName), difCol.DefaultEquals(tabCol) ? null : SqlPreCommand.Combine(Spacing.Simple, - difCol.DefaultConstraint != null ? SqlBuilder.AlterTableDropConstraint(tab.Name, difCol.DefaultConstraint.Name) : null, - tabCol.Default != null ? SqlBuilder.AlterTableAddDefaultConstraint(tab.Name, SqlBuilder.GetDefaultConstaint(tab, tabCol)!) : null) + difCol.DefaultConstraint != null ? sqlBuilder.AlterTableDropDefaultConstaint(tab.Name, difCol) : null, + tabCol.Default != null ? sqlBuilder.AlterTableAddDefaultConstraint(tab.Name, sqlBuilder.GetDefaultConstaint(tab, tabCol)!) : null) ); } else { - var update = difCol.PrimaryKey ? null : UpdateForeignKeyTypeChanged(tab, dif, tabCol, difCol, ChangeName, preRenameColumnsList) ?? UpdateCustom(tab, tabCol, difCol); - var drop = SqlBuilder.AlterTableDropColumn(tab, difCol.Name); + var update = difCol.PrimaryKey ? null : UpdateForeignKeyTypeChanged(sqlBuilder, tab, dif, tabCol, difCol, ChangeName, preRenameColumnsList) ?? UpdateCustom(tab, tabCol, difCol); + var drop = sqlBuilder.AlterTableDropColumn(tab, difCol.Name); delayedUpdates.Add(update); delayedDrops.Add(SqlPreCommand.Combine(Spacing.Simple, - difCol.DefaultConstraint != null ? SqlBuilder.AlterTableDropConstraint(tab.Name, difCol.DefaultConstraint.Name) : null, + difCol.DefaultConstraint != null ? sqlBuilder.AlterTableDropDefaultConstaint(tab.Name, difCol) : null, drop )); @@ -285,26 +296,26 @@ public static class SchemaSynchronizer } return SqlPreCommand.Combine(Spacing.Simple, - AlterTableAddColumnDefaultZero(tab, tabCol) + AlterTableAddColumnDefaultZero(sqlBuilder, tab, tabCol) ); } } ); - var createPrimaryKey = modelPK != null && (diffPK == null || !diffPK.IndexEquals(dif, modelPK)) ? SqlBuilder.CreateIndex(modelPK, checkUnique: null) : null; + var createPrimaryKey = modelPK != null && (diffPK == null || !diffPK.IndexEquals(dif, modelPK)) ? sqlBuilder.CreateIndex(modelPK, checkUnique: null) : null; var columnsHistory = columns != null && disableEnableSystemVersioning ? ForHistoryTable(columns, tab).Replace(new Regex(" IDENTITY "), m => " ") : null;/*HACK*/ - var addPeriod = ((tab.SystemVersioned != null && + var addPeriod = (!sqlBuilder.IsPostgres && tab.SystemVersioned != null && (dif.Period == null || !dif.Period.PeriodEquals(tab.SystemVersioned))) ? - (SqlPreCommandSimple)SqlBuilder.AlterTableAddPeriod(tab) : null); + (SqlPreCommandSimple)sqlBuilder.AlterTableAddPeriod(tab) : null; - var addSystemVersioning = (tab.SystemVersioned != null && + var addSystemVersioning = (!sqlBuilder.IsPostgres && tab.SystemVersioned != null && (dif.Period == null || dif.TemporalTableName == null || !object.Equals(replacements.Apply(Replacements.KeyTables, dif.TemporalTableName.ToString()), tab.SystemVersioned.TableName.ToString()) || disableEnableSystemVersioning) ? - SqlBuilder.AlterTableEnableSystemVersioning(tab).Do(a => a.GoBefore = true) : null); + sqlBuilder.AlterTableEnableSystemVersioning(tab).Do(a => a.GoBefore = true) : null); SqlPreCommand? combinedAddPeriod = null; @@ -341,8 +352,8 @@ public static class SchemaSynchronizer SqlPreCommand? historyTables = Synchronizer.SynchronizeScript(Spacing.Double, modelTablesHistory, databaseTablesHistory, createNew: null, - removeOld: (tn, dif) => SqlBuilder.DropTable(dif.Name), - mergeBoth: (tn, tab, dif) => !object.Equals(dif.Name, tab.SystemVersioned!.TableName) ? SqlBuilder.RenameOrMove(dif, tab, tab.SystemVersioned!.TableName) : null); + removeOld: (tn, dif) => sqlBuilder.DropTable(dif.Name), + mergeBoth: (tn, tab, dif) => !object.Equals(dif.Name, tab.SystemVersioned!.TableName) ? sqlBuilder.RenameOrMove(dif, tab, tab.SystemVersioned!.TableName) : null); SqlPreCommand? syncEnums = SynchronizeEnumsScript(replacements); @@ -352,7 +363,7 @@ public static class SchemaSynchronizer Spacing.Double, modelTables, databaseTables, - createNew: (tn, tab) => SqlBuilder.AlterTableForeignKeys(tab), + createNew: (tn, tab) => sqlBuilder.AlterTableForeignKeys(tab), removeOld: null, mergeBoth: (tn, tab, dif) => Synchronizer.SynchronizeScript( Spacing.Simple, @@ -360,7 +371,7 @@ public static class SchemaSynchronizer dif.Columns, createNew: (cn, colModel) => colModel.ReferenceTable == null || colModel.AvoidForeignKey || DifferentDatabase(tab.Name, colModel.ReferenceTable.Name) ? null : - SqlBuilder.AlterTableAddConstraintForeignKey(tab, colModel.Name, colModel.ReferenceTable), + sqlBuilder.AlterTableAddConstraintForeignKey(tab, colModel.Name, colModel.ReferenceTable), removeOld: null, @@ -369,20 +380,20 @@ public static class SchemaSynchronizer if (tabCol.ReferenceTable == null || tabCol.AvoidForeignKey || DifferentDatabase(tab.Name, tabCol.ReferenceTable.Name)) return null; - if (difCol.ForeignKey == null || !tabCol.ReferenceTable.Name.Equals(ChangeName(difCol.ForeignKey.TargetTable)) || difCol.SqlDbType != tabCol.SqlDbType) - return SqlBuilder.AlterTableAddConstraintForeignKey(tab, tabCol.Name, tabCol.ReferenceTable); + if (difCol.ForeignKey == null || !tabCol.ReferenceTable.Name.Equals(ChangeName(difCol.ForeignKey.TargetTable)) || !difCol.DbType.Equals(tabCol.DbType)) + return sqlBuilder.AlterTableAddConstraintForeignKey(tab, tabCol.Name, tabCol.ReferenceTable); - var name = SqlBuilder.ForeignKeyName(tab.Name.Name, tabCol.Name); + var name = sqlBuilder.ForeignKeyName(tab.Name.Name, tabCol.Name); return SqlPreCommand.Combine(Spacing.Simple, - name != difCol.ForeignKey.Name.Name ? SqlBuilder.RenameForeignKey(difCol.ForeignKey.Name.OnSchema(tab.Name.Schema), name) : null, - (difCol.ForeignKey.IsDisabled || difCol.ForeignKey.IsNotTrusted) && !replacements.SchemaOnly ? SqlBuilder.EnableForeignKey(tab.Name, name) : null); + name != difCol.ForeignKey.Name.Name ? sqlBuilder.RenameForeignKey(tab.Name, difCol.ForeignKey.Name.OnSchema(tab.Name.Schema), name) : null, + (difCol.ForeignKey.IsDisabled || difCol.ForeignKey.IsNotTrusted) && !replacements.SchemaOnly ? sqlBuilder.EnableForeignKey(tab.Name, name) : null); }) ); SqlPreCommand? addIndices = Synchronizer.SynchronizeScript(Spacing.Double, modelTables, databaseTables, - createNew: (tn, tab) => modelIndices[tab].Values.Where(a => !(a is PrimaryClusteredIndex)).Select(index => SqlBuilder.CreateIndex(index, null)).Combine(Spacing.Simple), + createNew: (tn, tab) => modelIndices[tab].Values.Where(a => !(a is PrimaryKeyIndex)).Select(index => sqlBuilder.CreateIndex(index, null)).Combine(Spacing.Simple), removeOld: null, mergeBoth: (tn, tab, dif) => { @@ -393,19 +404,19 @@ public static class SchemaSynchronizer Dictionary modelIxs = modelIndices[tab]; var controlledIndexes = Synchronizer.SynchronizeScript(Spacing.Simple, - modelIxs.Where(kvp => !(kvp.Value is PrimaryClusteredIndex)).ToDictionary(), + modelIxs.Where(kvp => !(kvp.Value is PrimaryKeyIndex)).ToDictionary(), dif.Indices.Where(kvp => !kvp.Value.IsPrimary).ToDictionary(), - createNew: (i, mix) => mix is UniqueTableIndex || mix.Columns.Any(isNew) || (replacements.Interactive ? SafeConsole.Ask(ref createMissingFreeIndexes, "Create missing non-unique index {0} in {1}?".FormatWith(mix.IndexName, tab.Name)) : true) ? SqlBuilder.CreateIndex(mix, checkUnique: replacements) : null, + createNew: (i, mix) => mix is UniqueTableIndex || mix.Columns.Any(isNew) || (replacements.Interactive ? SafeConsole.Ask(ref createMissingFreeIndexes, "Create missing non-unique index {0} in {1}?".FormatWith(mix.IndexName, tab.Name)) : true) ? sqlBuilder.CreateIndex(mix, checkUnique: replacements) : null, removeOld: null, - mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? SqlBuilder.CreateIndex(mix, checkUnique: replacements) : - mix.IndexName != dix.IndexName ? SqlBuilder.RenameIndex(tab.Name, dix.IndexName, mix.IndexName) : null); + mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? sqlBuilder.CreateIndex(mix, checkUnique: replacements) : + mix.IndexName != dix.IndexName ? sqlBuilder.RenameIndex(tab.Name, dix.IndexName, mix.IndexName) : null); return SqlPreCommand.Combine(Spacing.Simple, controlledIndexes); }); SqlPreCommand? addIndicesHistory = Synchronizer.SynchronizeScript(Spacing.Double, modelTablesHistory, databaseTablesHistory, - createNew: (tn, tab) => modelIndices[tab].Values.Where(a => a.GetType() == typeof(TableIndex)).Select(mix => SqlBuilder.CreateIndexBasic(mix, forHistoryTable: true)).Combine(Spacing.Simple), + createNew: (tn, tab) => modelIndices[tab].Values.Where(a => a.GetType() == typeof(TableIndex)).Select(mix => sqlBuilder.CreateIndexBasic(mix, forHistoryTable: true)).Combine(Spacing.Simple), removeOld: null, mergeBoth: (tn, tab, dif) => { @@ -417,11 +428,11 @@ public static class SchemaSynchronizer var controlledIndexes = Synchronizer.SynchronizeScript(Spacing.Simple, modelIxs.Where(kvp => kvp.Value.GetType() == typeof(TableIndex)).ToDictionary(), - dif.Indices.Where(kvp => kvp.Value.Type != DiffIndexType.Clustered).ToDictionary(), - createNew: (i, mix) => mix is UniqueTableIndex || mix.Columns.Any(isNew) || (replacements.Interactive ? SafeConsole.Ask(ref createMissingFreeIndexes, "Create missing non-unique index {0} in {1}?".FormatWith(mix.IndexName, tab.Name)) : true) ? SqlBuilder.CreateIndexBasic(mix, forHistoryTable: true) : null, + dif.Indices.Where(kvp => !kvp.Value.IsPrimary).ToDictionary(), + createNew: (i, mix) => mix is UniqueTableIndex || mix.Columns.Any(isNew) || (replacements.Interactive ? SafeConsole.Ask(ref createMissingFreeIndexes, "Create missing non-unique index {0} in {1}?".FormatWith(mix.IndexName, tab.Name)) : true) ? sqlBuilder.CreateIndexBasic(mix, forHistoryTable: true) : null, removeOld: null, - mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? SqlBuilder.CreateIndexBasic(mix, forHistoryTable: true) : - mix.IndexName != dix.IndexName ? SqlBuilder.RenameIndex(tab.SystemVersioned!.TableName, dix.IndexName, mix.IndexName) : null); + mergeBoth: (i, mix, dix) => !dix.IndexEquals(dif, mix) ? sqlBuilder.CreateIndexBasic(mix, forHistoryTable: true) : + mix.GetIndexName(tab.SystemVersioned!.TableName) != dix.IndexName ? sqlBuilder.RenameIndex(tab.SystemVersioned!.TableName, dix.IndexName, mix.GetIndexName(tab.SystemVersioned!.TableName)) : null); return SqlPreCommand.Combine(Spacing.Simple, controlledIndexes); }); @@ -432,8 +443,8 @@ public static class SchemaSynchronizer modelSchemas.ToDictionary(a => a.ToString()), databaseSchemas.ToDictionary(a => a.ToString()), createNew: null, - removeOld: (_, oldSN) => DropSchema(oldSN) ? SqlBuilder.DropSchema(oldSN) : null, - mergeBoth: (_, newSN, oldSN) => newSN.Equals(oldSN) ? null : SqlBuilder.DropSchema(oldSN) + removeOld: (_, oldSN) => DropSchema(oldSN) ? sqlBuilder.DropSchema(oldSN) : null, + mergeBoth: (_, newSN, oldSN) => newSN.Equals(oldSN) ? null : sqlBuilder.DropSchema(oldSN) ); return SqlPreCommand.Combine(Spacing.Triple, @@ -459,7 +470,7 @@ private static SqlPreCommand ForHistoryTable(SqlPreCommand sqlCommand, ITable ta return sqlCommand.Replace(new Regex(@$"\b{Regex.Escape(tab.Name.Name)}\b"), m => tab.SystemVersioned!.TableName.Name); } - private static SqlPreCommand? UpdateForeignKeyTypeChanged(ITable tab, DiffTable dif, IColumn tabCol, DiffColumn difCol, Func changeName, Dictionary> preRenameColumnsList) + private static SqlPreCommand? UpdateForeignKeyTypeChanged(SqlBuilder sqlBuilder, ITable tab, DiffTable dif, IColumn tabCol, DiffColumn difCol, Func changeName, Dictionary> preRenameColumnsList) { if(difCol.ForeignKey != null && tabCol.ReferenceTable != null) { @@ -478,7 +489,7 @@ private static SqlPreCommand ForHistoryTable(SqlPreCommand sqlCommand, ITable ta return new SqlPreCommandSimple( @$"UPDATE {tabAlias} -SET {tabCol.Name} = {fkAlias}.{tabCol.ReferenceTable.PrimaryKey.Name.SqlEscape()} +SET {tabCol.Name} = {fkAlias}.{tabCol.ReferenceTable.PrimaryKey.Name.SqlEscape(sqlBuilder.IsPostgres)} FROM {tab.Name} {tabAlias} JOIN {tabCol.ReferenceTable.Name} {fkAlias} ON {tabAlias}.{difCol.Name} = {fkAlias}.{oldId} "); @@ -495,10 +506,10 @@ private static SqlPreCommand UpdateCustom(ITable tab, IColumn tabCol, DiffColumn private static string GetZero(IColumn column) { - return (SqlBuilder.IsNumber(column.SqlDbType) ? "0" : - SqlBuilder.IsString(column.SqlDbType) ? "''" : - //SqlBuilder.IsDate(column.SqlDbType) ? "GetDate()" : - column.SqlDbType == SqlDbType.UniqueIdentifier ? Guid.Empty.ToString() : + return (column.DbType.IsNumber() ? "0" : + column.DbType.IsString() ? "''" : + column.DbType.IsDate() ? "GetDate()" : + column.DbType.IsGuid() ? Guid.Empty.ToString() : "?"); } @@ -509,21 +520,21 @@ private static bool StrongColumnChanges(ITable tab, DiffTable dif) .Any(t => (!t.tabCol.Nullable.ToBool() && t.difCol.Nullable) || !t.difCol.CompatibleTypes(t.tabCol)); } - private static SqlPreCommand UpdateCompatible(Replacements replacements, ITable tab, DiffTable dif, IColumn tabCol, DiffColumn difCol) + private static SqlPreCommand UpdateCompatible(SqlBuilder sqlBuilder, Replacements replacements, ITable tab, DiffTable dif, IColumn tabCol, DiffColumn difCol) { if (!(difCol.Nullable && !tabCol.Nullable.ToBool())) - return SqlBuilder.AlterTableAlterColumn(tab, tabCol, difCol.DefaultConstraint?.Name); + return sqlBuilder.AlterTableAlterColumn(tab, tabCol, difCol.DefaultConstraint?.Name); var defaultValue = GetDefaultValue(tab, tabCol, replacements, forNewColumn: false); if (defaultValue == "force") - return SqlBuilder.AlterTableAlterColumn(tab, tabCol, difCol.DefaultConstraint?.Name); + return sqlBuilder.AlterTableAlterColumn(tab, tabCol, difCol.DefaultConstraint?.Name); bool goBefore = difCol.Name != tabCol.Name; return SqlPreCommand.Combine(Spacing.Simple, NotNullUpdate(tab.Name, tabCol, defaultValue, goBefore), - SqlBuilder.AlterTableAlterColumn(tab, tabCol, difCol.DefaultConstraint?.Name) + sqlBuilder.AlterTableAlterColumn(tab, tabCol, difCol.DefaultConstraint?.Name) )!; } @@ -539,50 +550,35 @@ private static bool DifferentDatabase(ObjectName name, ObjectName name2) public static Func IgnoreSchema = s => s.Name.Contains("\\"); - private static HashSet DefaultGetSchemas(List list) - { - HashSet result = new HashSet(); - foreach (var db in list) - { - using (Administrator.OverrideDatabaseInSysViews(db)) - { - var schemaNames = Database.View().Select(s => s.name).ToList().Except(SqlBuilder.SystemSchemas); - - result.AddRange(schemaNames.Select(sn => new SchemaName(db, sn)).Where(a => !IgnoreSchema(a))); - } - } - return result; - } - - private static SqlPreCommand AlterTableAddColumnDefault(ITable table, IColumn column, Replacements rep, string? forceDefaultValue, HashSet hasValueFalse) + private static SqlPreCommand AlterTableAddColumnDefault(SqlBuilder sqlBuilder, ITable table, IColumn column, Replacements rep, string? forceDefaultValue, HashSet hasValueFalse) { if (column.Nullable == IsNullable.Yes || column.Identity || column.Default != null || column is ImplementationColumn) - return SqlBuilder.AlterTableAddColumn(table, column); + return sqlBuilder.AlterTableAddColumn(table, column); if (column.Nullable == IsNullable.Forced) { var hasValueColumn = table.GetHasValueColumn(column); if (hasValueColumn != null && hasValueFalse.Contains(hasValueColumn)) - return SqlBuilder.AlterTableAddColumn(table, column); + return sqlBuilder.AlterTableAddColumn(table, column); var defaultValue = GetDefaultValue(table, column, rep, forNewColumn: true, forceDefaultValue: forceDefaultValue); if (defaultValue == "force") - return SqlBuilder.AlterTableAddColumn(table, column); + return sqlBuilder.AlterTableAddColumn(table, column); var where = hasValueColumn != null ? $"{hasValueColumn.Name} = 1" : "??"; return SqlPreCommand.Combine(Spacing.Simple, - SqlBuilder.AlterTableAddColumn(table, column).Do(a => a.GoAfter = true), + sqlBuilder.AlterTableAddColumn(table, column).Do(a => a.GoAfter = true), new SqlPreCommandSimple($@"UPDATE {table.Name} SET - {column.Name} = {SqlBuilder.Quote(column.SqlDbType, defaultValue)} + {column.Name} = {sqlBuilder.Quote(column.DbType, defaultValue)} WHERE {where}"))!; } else { var defaultValue = GetDefaultValue(table, column, rep, forNewColumn: true, forceDefaultValue: forceDefaultValue); if (defaultValue == "force") - return SqlBuilder.AlterTableAddColumn(table, column); + return sqlBuilder.AlterTableAddColumn(table, column); if (column is FieldEmbedded.EmbeddedHasValueColumn hv && defaultValue == "0") hasValueFalse.Add(hv); @@ -590,40 +586,43 @@ private static SqlPreCommand AlterTableAddColumnDefault(ITable table, IColumn co var tempDefault = new SqlBuilder.DefaultConstraint( columnName: column.Name, name: "DF_TEMP_" + column.Name, - quotedDefinition: SqlBuilder.Quote(column.SqlDbType, defaultValue) + quotedDefinition: sqlBuilder.Quote(column.DbType, defaultValue) ); return SqlPreCommand.Combine(Spacing.Simple, - SqlBuilder.AlterTableAddColumn(table, column, tempDefault), - SqlBuilder.AlterTableDropConstraint(table.Name, tempDefault.Name))!; + sqlBuilder.AlterTableAddColumn(table, column, tempDefault), + sqlBuilder.IsPostgres ? + sqlBuilder.AlterTableAlterColumnDropDefault(table.Name, column.Name): + sqlBuilder.AlterTableDropConstraint(table.Name, tempDefault.Name))!; } } - private static SqlPreCommand AlterTableAddColumnDefaultZero(ITable table, IColumn column) + private static SqlPreCommand AlterTableAddColumnDefaultZero(SqlBuilder sqlBuilder, ITable table, IColumn column) { if (column.Nullable == IsNullable.Yes || column.Identity || column.Default != null || column is ImplementationColumn) - return SqlBuilder.AlterTableAddColumn(table, column); + return sqlBuilder.AlterTableAddColumn(table, column); - var defaultValue = (SqlBuilder.IsNumber(column.SqlDbType) ? "0" : - SqlBuilder.IsString(column.SqlDbType) ? "''" : - SqlBuilder.IsDate(column.SqlDbType) ? "GetDate()" : - column.SqlDbType == SqlDbType.UniqueIdentifier ? "'00000000-0000-0000-0000-000000000000'" : - "?"); + var defaultValue = + column.DbType.IsNumber()? "0" : + column.DbType.IsString()? "''" : + column.DbType.IsDate() ? "GetDate()" : + column.DbType.IsGuid() ? "'00000000-0000-0000-0000-000000000000'" : + "?"; var tempDefault = new SqlBuilder.DefaultConstraint( columnName: column.Name, name: "DF_TEMP_COPY_" + column.Name, - quotedDefinition: SqlBuilder.Quote(column.SqlDbType, defaultValue) + quotedDefinition: sqlBuilder.Quote(column.DbType, defaultValue) ); return SqlPreCommand.Combine(Spacing.Simple, - SqlBuilder.AlterTableAddColumn(table, column, tempDefault), - SqlBuilder.AlterTableDropConstraint(table.Name, tempDefault.Name))!; + sqlBuilder.AlterTableAddColumn(table, column, tempDefault), + sqlBuilder.AlterTableDropConstraint(table.Name, tempDefault.Name))!; } public static string GetDefaultValue(ITable table, IColumn column, Replacements rep, bool forNewColumn, string? forceDefaultValue = null) { - if (column is SystemVersionedInfo.Column svc) + if (column is SystemVersionedInfo.SqlServerPeriodColumn svc) { var date = svc.SystemVersionColumnType == SystemVersionedInfo.ColumnType.Start ? DateTime.MinValue : DateTime.MaxValue; @@ -631,17 +630,17 @@ public static string GetDefaultValue(ITable table, IColumn column, Replacements } string typeDefault = forceDefaultValue ?? - (SqlBuilder.IsNumber(column.SqlDbType) ? "0" : - SqlBuilder.IsString(column.SqlDbType) ? "''" : - SqlBuilder.IsDate(column.SqlDbType) ? "GetDate()" : - column.SqlDbType == SqlDbType.UniqueIdentifier ? "NEWID()" : + (column.DbType.IsNumber() ? "0" : + column.DbType.IsString() ? "''" : + column.DbType.IsDate() ? "GetDate()" : + column.DbType.IsGuid() ? "NEWID()" : "?"); string defaultValue = rep.Interactive ? SafeConsole.AskString($"Default value for '{table.Name.Name}.{column.Name}'? ([Enter] for {typeDefault} or 'force' if there are no {(forNewColumn ? "rows" : "nulls")}) ", stringValidator: str => null) : ""; if (defaultValue == "force") return defaultValue; - if (defaultValue.HasText() && SqlBuilder.IsString(column.SqlDbType) && !defaultValue.Contains("'")) + if (defaultValue.HasText() && column.DbType.IsString() && !defaultValue.Contains("'")) defaultValue = "'" + defaultValue + "'"; if (string.IsNullOrEmpty(defaultValue)) @@ -667,10 +666,10 @@ private static Dictionary ApplyIndexAutoReplacements(DiffTabl var nIx = newOnly.FirstOrDefault(n => { var newIx = dictionary[n]; - if (oldIx.IsPrimary && newIx is PrimaryClusteredIndex) + if (oldIx.IsPrimary && newIx is PrimaryKeyIndex) return true; - if (oldIx.IsPrimary || newIx is PrimaryClusteredIndex) + if (oldIx.IsPrimary || newIx is PrimaryKeyIndex) return false; if (oldIx.IsUnique != (newIx is UniqueTableIndex)) @@ -731,156 +730,9 @@ private static Dictionary ApplyIndexAutoReplacements(DiffTabl public static Func? IgnoreTable = null; - public static Dictionary DefaultGetDatabaseDescription(List databases) - { - List allTables = new List(); - - foreach (var db in databases) - { - SafeConsole.WriteColor(ConsoleColor.Cyan, '.'); - - using (Administrator.OverrideDatabaseInSysViews(db)) - { - var databaseName = db == null ? Connector.Current.DatabaseName() : db.Name; - - var sysDb = Database.View().Single(a => a.name == databaseName); - - var con = Connector.Current; - - var tables = - (from s in Database.View() - from t in s.Tables().Where(t => !t.ExtendedProperties().Any(a => a.name == "microsoft_database_tools_support")) //IntelliSense bug - select new DiffTable - { - Name = new ObjectName(new SchemaName(db, s.name), t.name), - - TemporalType = !con.SupportsTemporalTables ? SysTableTemporalType.None: t.temporal_type, - - Period = !con.SupportsTemporalTables ? null : - (from p in t.Periods() - join sc in t.Columns() on p.start_column_id equals sc.column_id - join ec in t.Columns() on p.end_column_id equals ec.column_id -#pragma warning disable CS0472 - select (int?)p.object_id == null ? null : new DiffPeriod -#pragma warning restore CS0472 - { - StartColumnName = sc.name, - EndColumnName = ec.name, - }).SingleOrDefaultEx(), - - TemporalTableName = !con.SupportsTemporalTables || t.history_table_id == null ? null : - Database.View() - .Where(ht => ht.object_id == t.history_table_id) - .Select(ht => new ObjectName(new SchemaName(db, ht.Schema().name), ht.name)) - .SingleOrDefault(), - - PrimaryKeyName = (from k in t.KeyConstraints() - where k.type == "PK" - select k.name == null ? null : new ObjectName(new SchemaName(db, k.Schema().name), k.name)) - .SingleOrDefaultEx(), - - Columns = (from c in t.Columns() - join userType in Database.View().DefaultIfEmpty() on c.user_type_id equals userType.user_type_id - join sysType in Database.View().DefaultIfEmpty() on c.system_type_id equals sysType.user_type_id - join ctr in Database.View().DefaultIfEmpty() on c.default_object_id equals ctr.object_id - select new DiffColumn - { - Name = c.name, - SqlDbType = sysType == null ? SqlDbType.Udt : ToSqlDbType(sysType.name), - UserTypeName = sysType == null ? userType.name : null, - Nullable = c.is_nullable, - Collation = c.collation_name == sysDb.collation_name ? null : c.collation_name, - Length = c.max_length, - Precision = c.precision, - Scale = c.scale, - Identity = c.is_identity, - GeneratedAlwaysType = con.SupportsTemporalTables ? c.generated_always_type : GeneratedAlwaysType.None, - DefaultConstraint = ctr.name == null ? null : new DiffDefaultConstraint - { - Name = ctr.name, - Definition = ctr.definition - }, - PrimaryKey = t.Indices().Any(i => i.is_primary_key && i.IndexColumns().Any(ic => ic.column_id == c.column_id)), - }).ToDictionaryEx(a => a.Name, "columns"), - - MultiForeignKeys = (from fk in t.ForeignKeys() - join rt in Database.View() on fk.referenced_object_id equals rt.object_id - select new DiffForeignKey - { - Name = new ObjectName(new SchemaName(db, fk.Schema().name), fk.name), - IsDisabled = fk.is_disabled, - TargetTable = new ObjectName(new SchemaName(db, rt.Schema().name), rt.name), - Columns = fk.ForeignKeyColumns().Select(fkc => new DiffForeignKeyColumn - { - Parent = t.Columns().Single(c => c.column_id == fkc.parent_column_id).name, - Referenced = rt.Columns().Single(c => c.column_id == fkc.referenced_column_id).name - }).ToList(), - }).ToList(), - - SimpleIndices = (from i in t.Indices() - where /*!i.is_primary_key && */i.type != 0 /*heap indexes*/ - select new DiffIndex - { - IsUnique = i.is_unique, - IsPrimary = i.is_primary_key, - IndexName = i.name, - FilterDefinition = i.filter_definition, - Type = (DiffIndexType)i.type, - Columns = (from ic in i.IndexColumns() - join c in t.Columns() on ic.column_id equals c.column_id - orderby ic.index_column_id - select new DiffIndexColumn { ColumnName = c.name, IsIncluded = ic.is_included_column }).ToList() - }).ToList(), - - ViewIndices = (from v in Database.View() - where v.name.StartsWith("VIX_" + t.name + "_") - from i in v.Indices() - select new DiffIndex - { - IsUnique = i.is_unique, - ViewName = v.name, - IndexName = i.name, - Columns = (from ic in i.IndexColumns() - join c in v.Columns() on ic.column_id equals c.column_id - orderby ic.index_column_id - select new DiffIndexColumn { ColumnName = c.name, IsIncluded = ic.is_included_column }).ToList() - - }).ToList(), - - Stats = (from st in t.Stats() - where st.user_created - select new DiffStats - { - StatsName = st.name, - Columns = (from ic in st.StatsColumns() - join c in t.Columns() on ic.column_id equals c.column_id - select c.name).ToList() - }).ToList(), - - }).ToList(); - - if (IgnoreTable != null) - tables.RemoveAll(IgnoreTable); - - tables.ForEach(t => t.ForeignKeysToColumns()); - - allTables.AddRange(tables); - } - } - - var database = allTables.ToDictionary(t => t.Name.ToString()); - - return database; - } - + - public static SqlDbType ToSqlDbType(string str) - { - if (str == "numeric") - return SqlDbType.Decimal; - return str.ToEnum(true); - } static SqlPreCommand? SynchronizeEnumsScript(Replacements replacements) @@ -998,10 +850,11 @@ private static Entity Clone(Entity current) public static SqlPreCommand? SnapshotIsolation(Replacements replacements) { - if (replacements.SchemaOnly) + if (replacements.SchemaOnly || Schema.Current.Settings.IsPostgres) return null; var list = Schema.Current.DatabaseNames().Select(a => a?.ToString()).ToList(); + var sqlBuilder = Connector.Current.SqlBuilder; if (list.Contains(null)) { @@ -1016,8 +869,8 @@ private static Entity Clone(Entity current) var cmd = replacements.WithReplacedDatabaseName().Using(_ => results.Select((a, i) => SqlPreCommand.Combine(Spacing.Simple, !a.snapshot_isolation_state || !a.is_read_committed_snapshot_on ? DisconnectUsers(a.name!/*CSBUG*/, "SPID" + i) : null, - !a.snapshot_isolation_state ? SqlBuilder.SetSnapshotIsolation(a.name!/*CSBUG*/, true) : null, - !a.is_read_committed_snapshot_on ? SqlBuilder.MakeSnapshotIsolationDefault(a.name!/*CSBUG*/, true) : null)).Combine(Spacing.Double)); + !a.snapshot_isolation_state ? sqlBuilder.SetSnapshotIsolation(a.name!/*CSBUG*/, true) : null, + !a.is_read_committed_snapshot_on ? sqlBuilder.MakeSnapshotIsolationDefault(a.name!/*CSBUG*/, true) : null)).Combine(Spacing.Double)); if (cmd == null) return null; @@ -1092,6 +945,24 @@ public override string ToString() { return Name.ToString(); } + + internal void FixSqlColumnLengthSqlServer() + { + foreach (var c in Columns.Values.Where(c => c.Length != -1)) + { + var sqlDbType = c.DbType.SqlServer; + if (sqlDbType == SqlDbType.NChar || sqlDbType == SqlDbType.NText || sqlDbType == SqlDbType.NVarChar) + c.Length /= 2; + } + } + } + + + public enum SysTableTemporalType + { + None = 0, + HistoryTable = 1, + SystemVersionTemporalTable = 2 } public class DiffStats @@ -1113,7 +984,7 @@ public class DiffIndex public bool IsPrimary; public string IndexName; public string ViewName; - public string FilterDefinition; + public string? FilterDefinition; public DiffIndexType? Type; public List Columns; @@ -1131,7 +1002,7 @@ internal bool IndexEquals(DiffTable dif, Maps.TableIndex mix) if (this.ColumnsChanged(dif, mix)) return false; - if (this.IsPrimary != mix is PrimaryClusteredIndex) + if (this.IsPrimary != mix is PrimaryKeyIndex) return false; if (this.Type != GetIndexType(mix)) @@ -1145,8 +1016,8 @@ internal bool IndexEquals(DiffTable dif, Maps.TableIndex mix) if (mix is UniqueTableIndex && ((UniqueTableIndex)mix).ViewName != null) return null; - if (mix is PrimaryClusteredIndex) - return DiffIndexType.Clustered; + if (mix is PrimaryKeyIndex) + return Schema.Current.Settings.IsPostgres ? DiffIndexType.NonClustered : DiffIndexType.Clustered; return DiffIndexType.NonClustered; } @@ -1203,14 +1074,14 @@ public enum GeneratedAlwaysType public class DiffDefaultConstraint { - public string Name; + public string? Name; public string Definition; } public class DiffColumn { public string Name; - public SqlDbType SqlDbType; + public AbstractDbType DbType; public string? UserTypeName; public bool Nullable; public string? Collation; @@ -1228,26 +1099,20 @@ public class DiffColumn public bool ColumnEquals(IColumn other, bool ignorePrimaryKey, bool ignoreIdentity, bool ignoreGenerateAlways) { - var result = - SqlDbType == other.SqlDbType + var result = DbType.Equals(other.DbType) && Collation == other.Collation && StringComparer.InvariantCultureIgnoreCase.Equals(UserTypeName, other.UserDefinedTypeName) && Nullable == (other.Nullable.ToBool()) - && (other.Size == null || other.Size.Value == Precision || other.Size.Value == Length / BytesPerChar(other.SqlDbType) || other.Size.Value == int.MaxValue && Length == -1) + && (other.Size == null || other.Size.Value == Precision || other.Size.Value == Length || other.Size.Value == int.MaxValue && Length == -1) && (other.Scale == null || other.Scale.Value == Scale) && (ignoreIdentity || Identity == other.Identity) && (ignorePrimaryKey || PrimaryKey == other.PrimaryKey) && (ignoreGenerateAlways || GeneratedAlwaysType == other.GetGeneratedAlwaysType()); - return result; - } - - public static int BytesPerChar(System.Data.SqlDbType sqlDbType) - { - if (sqlDbType == System.Data.SqlDbType.NChar || sqlDbType == System.Data.SqlDbType.NText || sqlDbType == System.Data.SqlDbType.NVarChar) - return 2; + if (!result) + return false; - return 1; + return result; } public bool DefaultEquals(IColumn other) @@ -1286,7 +1151,7 @@ public DiffColumn Clone() Nullable = Nullable, Precision = Precision, Scale = Scale, - SqlDbType = SqlDbType, + DbType = DbType, UserTypeName = UserTypeName, }; } @@ -1297,14 +1162,27 @@ public override string ToString() } internal bool CompatibleTypes(IColumn tabCol) + { + if (Schema.Current.Settings.IsPostgres) + return CompatibleTypes_Postgres(this.DbType.PostgreSql, tabCol.DbType.PostgreSql); + else + return CompatibleTypes_SqlServer(this.DbType.SqlServer, tabCol.DbType.SqlServer); + } + + private bool CompatibleTypes_Postgres(NpgsqlDbType fromType, NpgsqlDbType toType) + { + return true; + } + + private bool CompatibleTypes_SqlServer(SqlDbType fromType, SqlDbType toType) { //https://docs.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql - switch (this.SqlDbType) + switch (fromType) { //BLACKLIST!! case SqlDbType.Binary: case SqlDbType.VarBinary: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Float: case SqlDbType.Real: @@ -1321,11 +1199,11 @@ internal bool CompatibleTypes(IColumn tabCol) case SqlDbType.NChar: case SqlDbType.NVarChar: - return tabCol.SqlDbType != SqlDbType.Image; + return fromType != SqlDbType.Image; case SqlDbType.DateTime: case SqlDbType.SmallDateTime: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.UniqueIdentifier: case SqlDbType.Image: @@ -1339,18 +1217,18 @@ internal bool CompatibleTypes(IColumn tabCol) } case SqlDbType.Date: - if (tabCol.SqlDbType == SqlDbType.Time) + if (fromType == SqlDbType.Time) return false; goto case SqlDbType.DateTime2; case SqlDbType.Time: - if (tabCol.SqlDbType == SqlDbType.Date) + if (fromType == SqlDbType.Date) return false; goto case SqlDbType.DateTime2; case SqlDbType.DateTimeOffset: case SqlDbType.DateTime2: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Decimal: case SqlDbType.Float: @@ -1383,7 +1261,7 @@ internal bool CompatibleTypes(IColumn tabCol) case SqlDbType.Money: case SqlDbType.SmallMoney: case SqlDbType.Bit: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Date: case SqlDbType.Time: @@ -1401,7 +1279,7 @@ internal bool CompatibleTypes(IColumn tabCol) } case SqlDbType.Timestamp: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.NChar: case SqlDbType.NVarChar: @@ -1420,7 +1298,7 @@ internal bool CompatibleTypes(IColumn tabCol) return true; } case SqlDbType.Variant: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Timestamp: case SqlDbType.Image: @@ -1435,7 +1313,7 @@ internal bool CompatibleTypes(IColumn tabCol) //WHITELIST!! case SqlDbType.UniqueIdentifier: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Binary: case SqlDbType.VarBinary: @@ -1450,7 +1328,7 @@ internal bool CompatibleTypes(IColumn tabCol) return false; } case SqlDbType.Image: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Binary: case SqlDbType.Image: @@ -1462,7 +1340,7 @@ internal bool CompatibleTypes(IColumn tabCol) } case SqlDbType.NText: case SqlDbType.Text: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Char: case SqlDbType.VarChar: @@ -1477,7 +1355,7 @@ internal bool CompatibleTypes(IColumn tabCol) } case SqlDbType.Xml: case SqlDbType.Udt: - switch (tabCol.SqlDbType) + switch (fromType) { case SqlDbType.Binary: case SqlDbType.VarBinary: diff --git a/Signum.Engine/Engine/Scripts/versioning_function.sql b/Signum.Engine/Engine/Scripts/versioning_function.sql new file mode 100644 index 0000000000..be2f4c5b00 --- /dev/null +++ b/Signum.Engine/Engine/Scripts/versioning_function.sql @@ -0,0 +1,179 @@ +CREATE OR REPLACE FUNCTION versioning() +RETURNS TRIGGER AS $$ +DECLARE + sys_period text; + history_table text; + manipulate jsonb; + commonColumns text[]; + time_stamp_to_use timestamptz := current_timestamp; + range_lower timestamptz; + transaction_info txid_snapshot; + existing_range tstzrange; + holder record; + holder2 record; + pg_version integer; +BEGIN + -- version 0.2.0 + + IF TG_WHEN != 'BEFORE' OR TG_LEVEL != 'ROW' THEN + RAISE TRIGGER_PROTOCOL_VIOLATED USING + MESSAGE = 'function "versioning" must be fired BEFORE ROW'; + END IF; + + IF TG_OP != 'INSERT' AND TG_OP != 'UPDATE' AND TG_OP != 'DELETE' THEN + RAISE TRIGGER_PROTOCOL_VIOLATED USING + MESSAGE = 'function "versioning" must be fired for INSERT or UPDATE or DELETE'; + END IF; + + IF TG_NARGS != 3 THEN + RAISE INVALID_PARAMETER_VALUE USING + MESSAGE = 'wrong number of parameters for function "versioning"', + HINT = 'expected 3 parameters but got ' || TG_NARGS; + END IF; + + sys_period := TG_ARGV[0]; + history_table := TG_ARGV[1]; + + -- check if sys_period exists on original table + SELECT atttypid, attndims INTO holder FROM pg_attribute WHERE attrelid = TG_RELID AND attname = sys_period AND NOT attisdropped; + IF NOT FOUND THEN + RAISE 'column "%" of relation "%" does not exist', sys_period, TG_TABLE_NAME USING + ERRCODE = 'undefined_column'; + END IF; + IF holder.atttypid != to_regtype('tstzrange') THEN + IF holder.attndims > 0 THEN + RAISE 'system period column "%" of relation "%" is not a range but an array', sys_period, TG_TABLE_NAME USING + ERRCODE = 'datatype_mismatch'; + END IF; + + SELECT rngsubtype INTO holder2 FROM pg_range WHERE rngtypid = holder.atttypid; + IF FOUND THEN + RAISE 'system period column "%" of relation "%" is not a range of timestamp with timezone but of type %', sys_period, TG_TABLE_NAME, format_type(holder2.rngsubtype, null) USING + ERRCODE = 'datatype_mismatch'; + END IF; + + RAISE 'system period column "%" of relation "%" is not a range but type %', sys_period, TG_TABLE_NAME, format_type(holder.atttypid, null) USING + ERRCODE = 'datatype_mismatch'; + END IF; + + IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN + -- Ignore rows already modified in this transaction + transaction_info := txid_current_snapshot(); + IF OLD.xmin::text >= (txid_snapshot_xmin(transaction_info) % (2^32)::bigint)::text + AND OLD.xmin::text <= (txid_snapshot_xmax(transaction_info) % (2^32)::bigint)::text THEN + IF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + + RETURN NEW; + END IF; + + SELECT current_setting('server_version_num')::integer + INTO pg_version; + + -- to support postgres < 9.6 + IF pg_version < 90600 THEN + -- check if history table exits + IF to_regclass(history_table::cstring) IS NULL THEN + RAISE 'relation "%" does not exist', history_table; + END IF; + ELSE + IF to_regclass(history_table) IS NULL THEN + RAISE 'relation "%" does not exist', history_table; + END IF; + END IF; + + -- check if history table has sys_period + IF NOT EXISTS(SELECT * FROM pg_attribute WHERE attrelid = history_table::regclass AND attname = sys_period AND NOT attisdropped) THEN + RAISE 'history relation "%" does not contain system period column "%"', history_table, sys_period USING + HINT = 'history relation must contain system period column with the same name and data type as the versioned one'; + END IF; + + EXECUTE format('SELECT $1.%I', sys_period) USING OLD INTO existing_range; + + IF existing_range IS NULL THEN + RAISE 'system period column "%" of relation "%" must not be null', sys_period, TG_TABLE_NAME USING + ERRCODE = 'null_value_not_allowed'; + END IF; + + IF isempty(existing_range) OR NOT upper_inf(existing_range) THEN + RAISE 'system period column "%" of relation "%" contains invalid value', sys_period, TG_TABLE_NAME USING + ERRCODE = 'data_exception', + DETAIL = 'valid ranges must be non-empty and unbounded on the high side'; + END IF; + + IF TG_ARGV[2] = 'true' THEN + -- mitigate update conflicts + range_lower := lower(existing_range); + IF range_lower >= time_stamp_to_use THEN + time_stamp_to_use := range_lower + interval '1 microseconds'; + END IF; + END IF; + + WITH history AS + (SELECT attname, atttypid + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 + AND NOT attisdropped), + main AS + (SELECT attname, atttypid + FROM pg_attribute + WHERE attrelid = TG_RELID + AND attnum > 0 + AND NOT attisdropped) + SELECT + history.attname AS history_name, + main.attname AS main_name, + history.atttypid AS history_type, + main.atttypid AS main_type + INTO holder + FROM history + INNER JOIN main + ON history.attname = main.attname + WHERE + history.atttypid != main.atttypid; + + IF FOUND THEN + RAISE 'column "%" of relation "%" is of type % but column "%" of history relation "%" is of type %', + holder.main_name, TG_TABLE_NAME, format_type(holder.main_type, null), holder.history_name, history_table, format_type(holder.history_type, null) + USING ERRCODE = 'datatype_mismatch'; + END IF; + + WITH history AS + (SELECT attname + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 + AND NOT attisdropped), + main AS + (SELECT attname + FROM pg_attribute + WHERE attrelid = TG_RELID + AND attnum > 0 + AND NOT attisdropped) + SELECT array_agg(quote_ident(history.attname)) INTO commonColumns + FROM history + INNER JOIN main + ON history.attname = main.attname + AND history.attname != sys_period; + + EXECUTE ('INSERT INTO ' || history_table || '(' || + array_to_string(commonColumns , ',') || + ',' || + quote_ident(sys_period) || + ') VALUES ($1.' || + array_to_string(commonColumns, ',$1.') || + ',tstzrange($2, $3, ''[)''))') + USING OLD, range_lower, time_stamp_to_use; + END IF; + + IF TG_OP = 'UPDATE' OR TG_OP = 'INSERT' THEN + manipulate := jsonb_set('{}'::jsonb, ('{' || sys_period || '}')::text[], to_jsonb(tstzrange(time_stamp_to_use, null, '[)'))); + + RETURN jsonb_populate_record(NEW, manipulate); + END IF; + + RETURN OLD; +END; +$$ LANGUAGE plpgsql; diff --git a/Signum.Engine/Engine/Scripts/versioning_function_nochecks.sql b/Signum.Engine/Engine/Scripts/versioning_function_nochecks.sql new file mode 100644 index 0000000000..6c3d473ec8 --- /dev/null +++ b/Signum.Engine/Engine/Scripts/versioning_function_nochecks.sql @@ -0,0 +1,76 @@ +CREATE OR REPLACE FUNCTION versioning() +RETURNS TRIGGER AS $$ +DECLARE + sys_period text; + history_table text; + manipulate jsonb; + commonColumns text[]; + time_stamp_to_use timestamptz := current_timestamp; + range_lower timestamptz; + transaction_info txid_snapshot; + existing_range tstzrange; +BEGIN + -- version 0.0.1 + + sys_period := TG_ARGV[0]; + history_table := TG_ARGV[1]; + + IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN + -- Ignore rows already modified in this transaction + transaction_info := txid_current_snapshot(); + IF OLD.xmin::text >= (txid_snapshot_xmin(transaction_info) % (2^32)::bigint)::text + AND OLD.xmin::text <= (txid_snapshot_xmax(transaction_info) % (2^32)::bigint)::text THEN + IF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + + RETURN NEW; + END IF; + + EXECUTE format('SELECT $1.%I', sys_period) USING OLD INTO existing_range; + + IF TG_ARGV[2] = 'true' THEN + -- mitigate update conflicts + range_lower := lower(existing_range); + IF range_lower >= time_stamp_to_use THEN + time_stamp_to_use := range_lower + interval '1 microseconds'; + END IF; + END IF; + + WITH history AS + (SELECT attname + FROM pg_attribute + WHERE attrelid = history_table::regclass + AND attnum > 0 + AND NOT attisdropped), + main AS + (SELECT attname + FROM pg_attribute + WHERE attrelid = TG_RELID + AND attnum > 0 + AND NOT attisdropped) + SELECT array_agg(quote_ident(history.attname)) INTO commonColumns + FROM history + INNER JOIN main + ON history.attname = main.attname + AND history.attname != sys_period; + + EXECUTE ('INSERT INTO ' || history_table || '(' || + array_to_string(commonColumns , ',') || + ',' || + quote_ident(sys_period) || + ') VALUES ($1.' || + array_to_string(commonColumns, ',$1.') || + ',tstzrange($2, $3, ''[)''))') + USING OLD, range_lower, time_stamp_to_use; + END IF; + + IF TG_OP = 'UPDATE' OR TG_OP = 'INSERT' THEN + manipulate := jsonb_set('{}'::jsonb, ('{' || sys_period || '}')::text[], to_jsonb(tstzrange(time_stamp_to_use, null, '[)'))); + + RETURN jsonb_populate_record(NEW, manipulate); + END IF; + + RETURN OLD; +END; +$$ LANGUAGE plpgsql; diff --git a/Signum.Engine/Engine/SqlBuilder.cs b/Signum.Engine/Engine/SqlBuilder.cs index 964f90ea97..3696511851 100644 --- a/Signum.Engine/Engine/SqlBuilder.cs +++ b/Signum.Engine/Engine/SqlBuilder.cs @@ -4,12 +4,24 @@ using System.Data; using Signum.Utilities; using Signum.Engine.Maps; +using Signum.Entities.Reflection; namespace Signum.Engine { - public static class SqlBuilder + public class SqlBuilder { - public static List SystemSchemas = new List() + Connector connector; + bool isPostgres; + + public bool IsPostgres => isPostgres; + + internal SqlBuilder(Connector connector) + { + this.connector = connector; + this.isPostgres = connector.Schema.Settings.IsPostgres; + } + + public List SystemSchemas = new List() { "dbo", "guest", @@ -27,26 +39,43 @@ public static class SqlBuilder }; #region Create Tables - public static SqlPreCommandSimple CreateTableSql(ITable t, ObjectName? tableName = null, bool avoidSystemVersioning = false) + public SqlPreCommandSimple CreateTableSql(ITable t, ObjectName? tableName = null, bool avoidSystemVersioning = false) { - var primaryKeyConstraint = t.PrimaryKey == null || t.SystemVersioned != null && tableName != null && t.SystemVersioned.TableName.Equals(tableName) ? - null : "CONSTRAINT {0} PRIMARY KEY CLUSTERED ({1} ASC)".FormatWith(PrimaryClusteredIndex.GetPrimaryKeyName(t.Name), t.PrimaryKey.Name.SqlEscape()); + var primaryKeyConstraint = t.PrimaryKey == null || t.SystemVersioned != null && tableName != null && t.SystemVersioned.TableName.Equals(tableName) ? null : + isPostgres ? + "CONSTRAINT {0} PRIMARY KEY ({1})".FormatWith(PrimaryKeyIndex.GetPrimaryKeyName(t.Name).SqlEscape(isPostgres), t.PrimaryKey.Name.SqlEscape(isPostgres)) : + "CONSTRAINT {0} PRIMARY KEY CLUSTERED ({1} ASC)".FormatWith(PrimaryKeyIndex.GetPrimaryKeyName(t.Name).SqlEscape(isPostgres), t.PrimaryKey.Name.SqlEscape(isPostgres)); - var systemPeriod = t.SystemVersioned == null || avoidSystemVersioning ? null : Period(t.SystemVersioned); + var systemPeriod = t.SystemVersioned == null || IsPostgres || avoidSystemVersioning ? null : Period(t.SystemVersioned); - var columns = t.Columns.Values.Select(c => SqlBuilder.ColumnLine(c, GetDefaultConstaint(t, c), isChange: false, forHistoryTable: avoidSystemVersioning)) + var columns = t.Columns.Values.Select(c => this.ColumnLine(c, GetDefaultConstaint(t, c), isChange: false, forHistoryTable: avoidSystemVersioning)) .And(primaryKeyConstraint) .And(systemPeriod) .NotNull() .ToString(",\r\n"); - var systemVersioning = t.SystemVersioned == null || avoidSystemVersioning ? null : + var systemVersioning = t.SystemVersioned == null || avoidSystemVersioning || IsPostgres ? null : $"\r\nWITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {t.SystemVersioned.TableName.OnDatabase(null)}))"; - return new SqlPreCommandSimple($"CREATE TABLE {tableName ?? t.Name}(\r\n{columns}\r\n)" + systemVersioning); + var result = new SqlPreCommandSimple($"CREATE {(IsPostgres && t.Name.IsTemporal ? "TEMPORARY " : "")}TABLE {tableName ?? t.Name}(\r\n{columns}\r\n)" + systemVersioning + ";"); + + if (!(IsPostgres && t.SystemVersioned != null)) + return result; + + return new[] + { + result, + new SqlPreCommandSimple($"CREATE TABLE {t.SystemVersioned.TableName}(LIKE {t.Name});"), + new SqlPreCommandSimple(@$"CREATE TRIGGER versioning_trigger +BEFORE INSERT OR UPDATE OR DELETE ON {t.Name} +FOR EACH ROW EXECUTE PROCEDURE versioning( + 'sys_period', '{t.SystemVersioned.TableName}', true +);") + }.Combine(Spacing.Simple)!; + } - public static SqlPreCommand DropTable(DiffTable diffTable) + public SqlPreCommand DropTable(DiffTable diffTable) { if (diffTable.TemporalTableName == null) return DropTable(diffTable.Name); @@ -57,17 +86,22 @@ public static SqlPreCommand DropTable(DiffTable diffTable) )!; } - public static SqlPreCommandSimple DropTable(ObjectName tableName) + public SqlPreCommandSimple DropTable(ObjectName tableName) + { + return new SqlPreCommandSimple("DROP TABLE {0};".FormatWith(tableName)); + } + + public SqlPreCommandSimple DropView(ObjectName viewName) { - return new SqlPreCommandSimple("DROP TABLE {0}".FormatWith(tableName)); + return new SqlPreCommandSimple("DROP VIEW {0};".FormatWith(viewName)); } - public static SqlPreCommandSimple DropView(ObjectName viewName) + public SqlPreCommandSimple CreateExtensionIfNotExist(string extensionName) { - return new SqlPreCommandSimple("DROP VIEW {0}".FormatWith(viewName)); + return new SqlPreCommandSimple($"CREATE EXTENSION IF NOT EXISTS \"{ extensionName }\";"); } - static SqlPreCommand DropViewIndex(ObjectName viewName, string index) + SqlPreCommand DropViewIndex(ObjectName viewName, string index) { return new[]{ DropIndex(viewName, index), @@ -75,99 +109,52 @@ static SqlPreCommand DropViewIndex(ObjectName viewName, string index) }.Combine(Spacing.Simple)!; } - public static SqlPreCommand AlterTableAddPeriod(ITable table) + public SqlPreCommand AlterTableAddPeriod(ITable table) { - return new SqlPreCommandSimple($"ALTER TABLE {table.Name} ADD {Period(table.SystemVersioned!)}"); + return new SqlPreCommandSimple($"ALTER TABLE {table.Name} ADD {Period(table.SystemVersioned!)};"); } - static string Period(SystemVersionedInfo sv) { + string? Period(SystemVersionedInfo sv) { if (!Connector.Current.SupportsTemporalTables) throw new InvalidOperationException($"The current connector '{Connector.Current}' does not support Temporal Tables"); - return $"PERIOD FOR SYSTEM_TIME ({sv.StartColumnName.SqlEscape()}, {sv.EndColumnName.SqlEscape()})"; - } - - public static SqlPreCommand AlterTableDropPeriod(ITable table) - { - return new SqlPreCommandSimple($"ALTER TABLE {table.Name} DROP PERIOD FOR SYSTEM_TIME"); + return $"PERIOD FOR SYSTEM_TIME ({sv.StartColumnName!.SqlEscape(isPostgres)}, {sv.EndColumnName!.SqlEscape(isPostgres)})"; } - public static SqlPreCommand AlterTableEnableSystemVersioning(ITable table) + public SqlPreCommand AlterTableDropPeriod(ITable table) { - return new SqlPreCommandSimple($"ALTER TABLE {table.Name} SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {table.SystemVersioned!.TableName.OnDatabase(null)}))"); + return new SqlPreCommandSimple($"ALTER TABLE {table.Name} DROP PERIOD FOR SYSTEM_TIME;"); } - public static SqlPreCommandSimple AlterTableDisableSystemVersioning(ObjectName tableName) + public SqlPreCommand AlterTableEnableSystemVersioning(ITable table) { - return new SqlPreCommandSimple($"ALTER TABLE {tableName} SET (SYSTEM_VERSIONING = OFF)"); + return new SqlPreCommandSimple($"ALTER TABLE {table.Name} SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {table.SystemVersioned!.TableName.OnDatabase(null)}));"); } - public static SqlPreCommand AlterTableDropColumn(ITable table, string columnName) + public SqlPreCommandSimple AlterTableDisableSystemVersioning(ObjectName tableName) { - return new SqlPreCommandSimple("ALTER TABLE {0} DROP COLUMN {1}".FormatWith(table.Name, columnName.SqlEscape())); + return new SqlPreCommandSimple($"ALTER TABLE {tableName} SET (SYSTEM_VERSIONING = OFF);"); } - public static SqlPreCommand AlterTableAddColumn(ITable table, IColumn column, SqlBuilder.DefaultConstraint? tempDefault = null) + public SqlPreCommand AlterTableDropColumn(ITable table, string columnName) { - return new SqlPreCommandSimple("ALTER TABLE {0} ADD {1}".FormatWith(table.Name, ColumnLine(column, tempDefault ?? GetDefaultConstaint(table, column), isChange: false))); + return new SqlPreCommandSimple("ALTER TABLE {0} DROP COLUMN {1};".FormatWith(table.Name, columnName.SqlEscape(isPostgres))); } - public static SqlPreCommand AlterTableAddOldColumn(ITable table, DiffColumn column) + public SqlPreCommand AlterTableAddColumn(ITable table, IColumn column, SqlBuilder.DefaultConstraint? tempDefault = null) { - return new SqlPreCommandSimple("ALTER TABLE {0} ADD {1}".FormatWith(table.Name, CreateOldColumn(column))); + return new SqlPreCommandSimple("ALTER TABLE {0} ADD {1};".FormatWith(table.Name, ColumnLine(column, tempDefault ?? GetDefaultConstaint(table, column), isChange: false))); } - public static bool IsNumber(SqlDbType sqlDbType) + public SqlPreCommand AlterTableAddOldColumn(ITable table, DiffColumn column) { - switch (sqlDbType) - { - case SqlDbType.BigInt: - case SqlDbType.Float: - case SqlDbType.Decimal: - case SqlDbType.Int: - case SqlDbType.Bit: - case SqlDbType.Money: - case SqlDbType.Real: - case SqlDbType.TinyInt: - case SqlDbType.SmallInt: - case SqlDbType.SmallMoney: - return true; - } - - return false; + return new SqlPreCommandSimple("ALTER TABLE {0} ADD {1};".FormatWith(table.Name, CreateOldColumn(column))); } - public static bool IsString(SqlDbType sqlDbType) + public SqlPreCommand AlterTableAlterColumn(ITable table, IColumn column, string? defaultConstraintName = null, ObjectName? forceTableName = null) { - switch (sqlDbType) - { - case SqlDbType.NText: - case SqlDbType.NVarChar: - case SqlDbType.Text: - case SqlDbType.VarChar: - return true; - } - - return false; - } - - public static bool IsDate(SqlDbType sqlDbType) - { - switch (sqlDbType) - { - case SqlDbType.DateTime: - case SqlDbType.DateTime2: - case SqlDbType.DateTimeOffset: - return true; - } - - return false; - } - - public static SqlPreCommand AlterTableAlterColumn(ITable table, IColumn column, string? defaultConstraintName = null, ObjectName? forceTableName = null) - { - var alterColumn = new SqlPreCommandSimple("ALTER TABLE {0} ALTER COLUMN {1}".FormatWith(forceTableName ?? table.Name, ColumnLine(column, null, isChange: true))); + var alterColumn = new SqlPreCommandSimple("ALTER TABLE {0} ALTER COLUMN {1};".FormatWith(forceTableName ?? table.Name, CreateColumn(column, null, isChange: true))); if (column.Default == null) return alterColumn; @@ -181,12 +168,12 @@ public static SqlPreCommand AlterTableAlterColumn(ITable table, IColumn column, )!; } - public static DefaultConstraint? GetDefaultConstaint(ITable t, IColumn c) + public DefaultConstraint? GetDefaultConstaint(ITable t, IColumn c) { if (c.Default == null) return null; - return new DefaultConstraint(c.Name, $"DF_{t.Name.Name}_{c.Name}", Quote(c.SqlDbType, c.Default)); + return new DefaultConstraint(c.Name, $"DF_{t.Name.Name}_{c.Name}", Quote(c.DbType, c.Default)); } public class DefaultConstraint @@ -203,7 +190,7 @@ public DefaultConstraint(string columnName, string name, string quotedDefinition } } - public static string CreateOldColumn(DiffColumn c) + public string CreateOldColumn(DiffColumn c) { string fullType = GetColumnType(c); @@ -214,7 +201,7 @@ public static string CreateOldColumn(DiffColumn c) var defaultConstraint = c.DefaultConstraint!= null ? $"CONSTRAINT {c.DefaultConstraint.Name} DEFAULT " + c.DefaultConstraint.Definition : null; return $" ".Combine( - c.Name.SqlEscape(), + c.Name.SqlEscape(isPostgres), fullType, c.Identity ? "IDENTITY " : null, generatedAlways, @@ -224,20 +211,20 @@ public static string CreateOldColumn(DiffColumn c) ); } - public static string ColumnLine(IColumn c, DefaultConstraint? constraint, bool isChange, bool forHistoryTable = false) + public string ColumnLine(IColumn c, DefaultConstraint? constraint, bool isChange, bool forHistoryTable = false) { string fullType = GetColumnType(c); - var generatedAlways = c is SystemVersionedInfo.Column svc && !forHistoryTable ? + var generatedAlways = c is SystemVersionedInfo.SqlServerPeriodColumn svc && !forHistoryTable ? $"GENERATED ALWAYS AS ROW {(svc.SystemVersionColumnType == SystemVersionedInfo.ColumnType.Start ? "START" : "END")} HIDDEN" : null; var defaultConstraint = constraint != null ? $"CONSTRAINT {constraint.Name} DEFAULT " + constraint.QuotedDefinition : null; return $" ".Combine( - c.Name.SqlEscape(), + c.Name.SqlEscape(isPostgres), fullType, - c.Identity && !isChange && !forHistoryTable ? "IDENTITY " : null, + c.Identity && !isChange && !forHistoryTable ? (isPostgres? "GENERATED ALWAYS AS IDENTITY": "IDENTITY") : null, generatedAlways, c.Collation != null ? ("COLLATE " + c.Collation) : null, c.Nullable.ToBool() ? "NULL" : "NOT NULL", @@ -245,31 +232,31 @@ public static string ColumnLine(IColumn c, DefaultConstraint? constraint, bool i ); } - public static string GetColumnType(IColumn c) + public string GetColumnType(IColumn c) { - return (c.SqlDbType == SqlDbType.Udt ? c.UserDefinedTypeName : c.SqlDbType.ToString().ToUpper()) + GetSizeScale(c.Size, c.Scale); + return c.UserDefinedTypeName ?? (c.DbType.ToString(IsPostgres) + GetSizeScale(c.Size, c.Scale)); } - public static string GetColumnType(DiffColumn c) + public string GetColumnType(DiffColumn c) { - return (c.SqlDbType == SqlDbType.Udt ? c.UserTypeName! : c.SqlDbType.ToString().ToUpper()) /*+ GetSizeScale(Math.Max(c.Length, c.Precision), c.Scale)*/; + return c.UserTypeName ?? c.DbType.ToString(IsPostgres) /*+ GetSizeScale(Math.Max(c.Length, c.Precision), c.Scale)*/; } - public static string Quote(SqlDbType type, string @default) + public string Quote(AbstractDbType type, string @default) { - if (IsString(type) && !(@default.StartsWith("'") && @default.StartsWith("'"))) + if (type.IsString() && !(@default.StartsWith("'") && @default.StartsWith("'"))) return "'" + @default + "'"; return @default; } - public static string GetSizeScale(int? size, int? scale) + public string GetSizeScale(int? size, int? scale) { if (size == null) return ""; if (size == int.MaxValue) - return "(MAX)"; + return IsPostgres ? "" : "(MAX)"; if (scale == null) return "({0})".FormatWith(size); @@ -277,56 +264,56 @@ public static string GetSizeScale(int? size, int? scale) return "({0},{1})".FormatWith(size, scale); } - public static SqlPreCommand? AlterTableForeignKeys(ITable t) + public SqlPreCommand? AlterTableForeignKeys(ITable t) { return t.Columns.Values.Select(c => - (c.ReferenceTable == null || c.AvoidForeignKey) ? null : SqlBuilder.AlterTableAddConstraintForeignKey(t, c.Name, c.ReferenceTable)) + (c.ReferenceTable == null || c.AvoidForeignKey) ? null : this.AlterTableAddConstraintForeignKey(t, c.Name, c.ReferenceTable)) .Combine(Spacing.Simple); } - public static SqlPreCommand DropIndex(ObjectName tableName, DiffIndex index) + public SqlPreCommand DropIndex(ObjectName tableName, DiffIndex index) { if (index.IsPrimary) - return AlterTableDropConstraint(tableName, new ObjectName(tableName.Schema, index.IndexName)); + return AlterTableDropConstraint(tableName, new ObjectName(tableName.Schema, index.IndexName, isPostgres)); if (index.ViewName == null) return DropIndex(tableName, index.IndexName); else - return DropViewIndex(new ObjectName(tableName.Schema, index.ViewName), index.IndexName); + return DropViewIndex(new ObjectName(tableName.Schema, index.ViewName, isPostgres), index.IndexName); } - public static SqlPreCommand DropIndex(ObjectName objectName, string indexName) + public SqlPreCommand DropIndex(ObjectName objectName, string indexName) { if (objectName.Schema.Database == null) - return new SqlPreCommandSimple("DROP INDEX {0} ON {1}".FormatWith(indexName.SqlEscape(), objectName)); + return new SqlPreCommandSimple("DROP INDEX {0} ON {1};".FormatWith(indexName.SqlEscape(isPostgres), objectName)); else - return new SqlPreCommandSimple("EXEC {0}.dbo.sp_executesql N'DROP INDEX {1} ON {2}'" - .FormatWith(objectName.Schema.Database.ToString().SqlEscape(), indexName.SqlEscape(), objectName.OnDatabase(null).ToString())); + return new SqlPreCommandSimple("EXEC {0}.dbo.sp_executesql N'DROP INDEX {1} ON {2}';" + .FormatWith(objectName.Schema.Database.ToString().SqlEscape(isPostgres), indexName.SqlEscape(isPostgres), objectName.OnDatabase(null).ToString())); } - public static SqlPreCommand CreateIndex(TableIndex index, Replacements? checkUnique) + public SqlPreCommand CreateIndex(TableIndex index, Replacements? checkUnique) { - if (index is PrimaryClusteredIndex) + if (index is PrimaryKeyIndex) { - var columns = index.Columns.ToString(c => c.Name.SqlEscape(), ", "); + var columns = index.Columns.ToString(c => c.Name.SqlEscape(isPostgres), ", "); - return new SqlPreCommandSimple($"ALTER TABLE {index.Table.Name} ADD CONSTRAINT {index.IndexName} PRIMARY KEY CLUSTERED({columns})"); + return new SqlPreCommandSimple($"ALTER TABLE {index.Table.Name} ADD CONSTRAINT {index.IndexName.SqlEscape(isPostgres)} PRIMARY KEY CLUSTERED({columns});"); } if (index is UniqueTableIndex uIndex) { if (uIndex.ViewName != null) { - ObjectName viewName = new ObjectName(uIndex.Table.Name.Schema, uIndex.ViewName); + ObjectName viewName = new ObjectName(uIndex.Table.Name.Schema, uIndex.ViewName, isPostgres); - var columns = index.Columns.ToString(c => c.Name.SqlEscape(), ", "); + var columns = index.Columns.ToString(c => c.Name.SqlEscape(isPostgres), ", "); - SqlPreCommandSimple viewSql = new SqlPreCommandSimple($"CREATE VIEW {viewName} WITH SCHEMABINDING AS SELECT {columns} FROM {uIndex.Table.Name.ToString()} WHERE {uIndex.Where}") + SqlPreCommandSimple viewSql = new SqlPreCommandSimple($"CREATE VIEW {viewName} WITH SCHEMABINDING AS SELECT {columns} FROM {uIndex.Table.Name} WHERE {uIndex.Where};") { GoBefore = true, GoAfter = true }; - SqlPreCommandSimple indexSql = new SqlPreCommandSimple($"CREATE UNIQUE CLUSTERED INDEX {uIndex.IndexName} ON {viewName}({columns})"); + SqlPreCommandSimple indexSql = new SqlPreCommandSimple($"CREATE UNIQUE CLUSTERED INDEX {uIndex.IndexName.SqlEscape(isPostgres)} ON {viewName}({columns});"); return SqlPreCommand.Combine(Spacing.Simple, checkUnique!=null ? RemoveDuplicatesIfNecessary(uIndex, checkUnique) : null, @@ -337,7 +324,7 @@ public static SqlPreCommand CreateIndex(TableIndex index, Replacements? checkUni { return SqlPreCommand.Combine(Spacing.Double, checkUnique != null ? RemoveDuplicatesIfNecessary(uIndex, checkUnique) : null, - CreateIndexBasic(index, false))!; + CreateIndexBasic(index, forHistoryTable: false))!; } } else @@ -346,7 +333,7 @@ public static SqlPreCommand CreateIndex(TableIndex index, Replacements? checkUni } } - public static int DuplicateCount(UniqueTableIndex uniqueIndex, Replacements rep) + public int DuplicateCount(UniqueTableIndex uniqueIndex, Replacements rep) { var primaryKey = uniqueIndex.Table.Columns.Values.Where(a => a.PrimaryKey).Only(); @@ -357,22 +344,22 @@ public static int DuplicateCount(UniqueTableIndex uniqueIndex, Replacements rep) var columnReplacement = rep.TryGetC(Replacements.KeyColumnsForTable(uniqueIndex.Table.Name.ToString()))?.Inverse() ?? new Dictionary(); - var oldColumns = uniqueIndex.Columns.ToString(c => (columnReplacement.TryGetC(c.Name) ?? c.Name).SqlEscape(), ", "); + var oldColumns = uniqueIndex.Columns.ToString(c => (columnReplacement.TryGetC(c.Name) ?? c.Name).SqlEscape(isPostgres), ", "); var oldPrimaryKey = columnReplacement.TryGetC(primaryKey.Name) ?? primaryKey.Name; - return (int)Executor.ExecuteScalar( + return Convert.ToInt32(Executor.ExecuteScalar( $@"SELECT Count(*) FROM {oldTableName} -WHERE {oldPrimaryKey} NOT IN +WHERE {oldPrimaryKey.SqlEscape(IsPostgres)} NOT IN ( - SELECT MIN({oldPrimaryKey}) + SELECT MIN({oldPrimaryKey.SqlEscape(IsPostgres)}) FROM {oldTableName} {(!uniqueIndex.Where.HasText() ? "" : "WHERE " + uniqueIndex.Where.Replace(columnReplacement))} GROUP BY {oldColumns} -){(!uniqueIndex.Where.HasText() ? "" : "AND " + uniqueIndex.Where.Replace(columnReplacement))}")!; +){(!uniqueIndex.Where.HasText() ? "" : "AND " + uniqueIndex.Where.Replace(columnReplacement))};")!); } - public static SqlPreCommand? RemoveDuplicatesIfNecessary(UniqueTableIndex uniqueIndex, Replacements rep) + public SqlPreCommand? RemoveDuplicatesIfNecessary(UniqueTableIndex uniqueIndex, Replacements rep) { try { @@ -387,7 +374,7 @@ GROUP BY {oldColumns} if (count == 0) return null; - var columns = uniqueIndex.Columns.ToString(c => c.Name.SqlEscape(), ", "); + var columns = uniqueIndex.Columns.ToString(c => c.Name.SqlEscape(isPostgres), ", "); if (rep.Interactive) { @@ -403,12 +390,12 @@ GROUP BY {oldColumns} } catch (Exception) { - return new SqlPreCommandSimple($"-- Impossible to determine duplicates in new index {uniqueIndex.IndexName}"); + return new SqlPreCommandSimple($"-- Impossible to determine duplicates in new index {uniqueIndex.IndexName.SqlEscape(isPostgres)}"); } } - private static SqlPreCommand RemoveDuplicates(UniqueTableIndex uniqueIndex, IColumn primaryKey, string columns, bool commentedOut) + private SqlPreCommand RemoveDuplicates(UniqueTableIndex uniqueIndex, IColumn primaryKey, string columns, bool commentedOut) { return new SqlPreCommandSimple($@"DELETE {uniqueIndex.Table.Name} WHERE {primaryKey.Name} NOT IN @@ -417,80 +404,100 @@ SELECT MIN({primaryKey.Name}) FROM {uniqueIndex.Table.Name} {(string.IsNullOrWhiteSpace(uniqueIndex.Where) ? "" : "WHERE " + uniqueIndex.Where)} GROUP BY {columns} -){(string.IsNullOrWhiteSpace(uniqueIndex.Where) ? "" : " AND " + uniqueIndex.Where)}".Let(txt => commentedOut ? txt.Indent(2, '-') : txt)); +){(string.IsNullOrWhiteSpace(uniqueIndex.Where) ? "" : " AND " + uniqueIndex.Where)};".Let(txt => commentedOut ? txt.Indent(2, '-') : txt)); } - public static SqlPreCommand CreateIndexBasic(Maps.TableIndex index, bool forHistoryTable) + public SqlPreCommand CreateIndexBasic(Maps.TableIndex index, bool forHistoryTable) { var indexType = index is UniqueTableIndex ? "UNIQUE INDEX" : "INDEX"; - var columns = index.Columns.ToString(c => c.Name.SqlEscape(), ", "); - var include = index.IncludeColumns.HasItems() ? $" INCLUDE ({index.IncludeColumns.ToString(c => c.Name.SqlEscape(), ", ")})" : null; + var columns = index.Columns.ToString(c => c.Name.SqlEscape(isPostgres), ", "); + var include = index.IncludeColumns.HasItems() ? $" INCLUDE ({index.IncludeColumns.ToString(c => c.Name.SqlEscape(isPostgres), ", ")})" : null; var where = index.Where.HasText() ? $" WHERE {index.Where}" : ""; var tableName = forHistoryTable ? index.Table.SystemVersioned!.TableName : index.Table.Name; - return new SqlPreCommandSimple($"CREATE {indexType} {index.IndexName} ON {tableName}({columns}){include}{where}"); + return new SqlPreCommandSimple($"CREATE {indexType} {index.GetIndexName(tableName).SqlEscape(isPostgres)} ON {tableName}({columns}){include}{where};"); } - internal static SqlPreCommand UpdateTrim(ITable tab, IColumn tabCol) + internal SqlPreCommand UpdateTrim(ITable tab, IColumn tabCol) { - return new SqlPreCommandSimple("UPDATE {0} SET {1} = RTRIM({1})".FormatWith(tab.Name, tabCol.Name));; + return new SqlPreCommandSimple("UPDATE {0} SET {1} = RTRIM({1});".FormatWith(tab.Name, tabCol.Name));; } - public static SqlPreCommand AlterTableDropConstraint(ObjectName tableName, ObjectName constraintName) => - AlterTableDropConstraint(tableName, constraintName.Name); + public SqlPreCommand AlterTableDropConstraint(ObjectName tableName, ObjectName foreignKeyName) => + AlterTableDropConstraint(tableName, foreignKeyName.Name); - public static SqlPreCommand AlterTableDropConstraint(ObjectName tableName, string constraintName) + public SqlPreCommand AlterTableDropConstraint(ObjectName tableName, string constraintName) { - return new SqlPreCommandSimple("ALTER TABLE {0} DROP CONSTRAINT {1}".FormatWith( + return new SqlPreCommandSimple("ALTER TABLE {0} DROP CONSTRAINT {1};".FormatWith( tableName, - constraintName.SqlEscape())); + constraintName.SqlEscape(isPostgres))); + } + + public SqlPreCommand AlterTableDropDefaultConstaint(ObjectName tableName, DiffColumn column) + { + if (isPostgres) + return AlterTableAlterColumnDropDefault(tableName, column.Name); + else + return AlterTableDropConstraint(tableName, column.DefaultConstraint!.Name!); } - public static SqlPreCommandSimple AlterTableAddDefaultConstraint(ObjectName tableName, DefaultConstraint constraint) + public SqlPreCommand AlterTableAlterColumnDropDefault(ObjectName tableName, string columnName) { - return new SqlPreCommandSimple($"ALTER TABLE {tableName} ADD CONSTRAINT {constraint.Name} DEFAULT {constraint.QuotedDefinition} FOR {constraint.ColumnName}"); + return new SqlPreCommandSimple("ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT;".FormatWith( + tableName, + columnName.SqlEscape(isPostgres))); } - public static SqlPreCommand? AlterTableAddConstraintForeignKey(ITable table, string fieldName, ITable foreignTable) + public SqlPreCommandSimple AlterTableAddDefaultConstraint(ObjectName tableName, DefaultConstraint constraint) + { + return new SqlPreCommandSimple($"ALTER TABLE {tableName} ADD CONSTRAINT {constraint.Name} DEFAULT {constraint.QuotedDefinition} FOR {constraint.ColumnName};"); + } + + public SqlPreCommand? AlterTableAddConstraintForeignKey(ITable table, string fieldName, ITable foreignTable) { return AlterTableAddConstraintForeignKey(table.Name, fieldName, foreignTable.Name, foreignTable.PrimaryKey.Name); } - public static SqlPreCommand? AlterTableAddConstraintForeignKey(ObjectName parentTable, string parentColumn, ObjectName targetTable, string targetPrimaryKey) + public SqlPreCommand? AlterTableAddConstraintForeignKey(ObjectName parentTable, string parentColumn, ObjectName targetTable, string targetPrimaryKey) { if (!object.Equals(parentTable.Schema.Database, targetTable.Schema.Database)) return null; - return new SqlPreCommandSimple("ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3}({4})".FormatWith( + return new SqlPreCommandSimple("ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3}({4});".FormatWith( parentTable, - ForeignKeyName(parentTable.Name, parentColumn), - parentColumn.SqlEscape(), + ForeignKeyName(parentTable.Name, parentColumn).SqlEscape(isPostgres), + parentColumn.SqlEscape(isPostgres), targetTable, - targetPrimaryKey.SqlEscape())); + targetPrimaryKey.SqlEscape(isPostgres))); } - public static string ForeignKeyName(string table, string fieldName) + public string ForeignKeyName(string table, string fieldName) { - return "FK_{0}_{1}".FormatWith(table, fieldName).SqlEscape(); + var result = "FK_{0}_{1}".FormatWith(table, fieldName); + + return StringHashEncoder.ChopHash(result, this.connector.MaxNameLength); } - public static SqlPreCommand RenameForeignKey(ObjectName foreignKeyName, string newName) + public SqlPreCommand RenameForeignKey(ObjectName tn, ObjectName foreignKeyName, string newName) { + if (IsPostgres) + return new SqlPreCommandSimple($"ALTER TABLE {tn} RENAME CONSTRAINT {foreignKeyName.Name.SqlEscape(IsPostgres)} TO {newName.SqlEscape(IsPostgres)};"); + return SP_RENAME(foreignKeyName.Schema.Database, foreignKeyName.OnDatabase(null).ToString(), newName, "OBJECT"); } - public static SqlPreCommandSimple SP_RENAME(DatabaseName? database, string oldName, string newName, string? objectType) + public SqlPreCommandSimple SP_RENAME(DatabaseName? database, string oldName, string newName, string? objectType) { - return new SqlPreCommandSimple("EXEC {0}SP_RENAME '{1}' , '{2}'{3}".FormatWith( - database == null ? null: (new SchemaName(database, "dbo").ToString() + "."), + return new SqlPreCommandSimple("EXEC {0}SP_RENAME '{1}' , '{2}'{3};".FormatWith( + database == null ? null: (SchemaName.Default(isPostgres).ToString() + "."), oldName, newName, objectType == null ? null : ", '{0}'".FormatWith(objectType) )); } - public static SqlPreCommand RenameOrChangeSchema(ObjectName oldTableName, ObjectName newTableName) + public SqlPreCommand RenameOrChangeSchema(ObjectName oldTableName, ObjectName newTableName) { if (!object.Equals(oldTableName.Schema.Database, newTableName.Schema.Database)) throw new InvalidOperationException("Different database"); @@ -505,7 +512,7 @@ public static SqlPreCommand RenameOrChangeSchema(ObjectName oldTableName, Object oldNewSchema.Equals(newTableName) ? null : RenameTable(oldNewSchema, newTableName.Name))!; } - public static SqlPreCommand RenameOrMove(DiffTable oldTable, ITable newTable, ObjectName newTableName) + public SqlPreCommand RenameOrMove(DiffTable oldTable, ITable newTable, ObjectName newTableName) { if (object.Equals(oldTable.Name.Schema.Database, newTableName.Schema.Database)) return RenameOrChangeSchema(oldTable.Name, newTableName); @@ -516,112 +523,124 @@ public static SqlPreCommand RenameOrMove(DiffTable oldTable, ITable newTable, Ob DropTable(oldTable))!; } - public static SqlPreCommand MoveRows(ObjectName oldTable, ObjectName newTable, IEnumerable columnNames, bool avoidIdentityInsert = false) + public SqlPreCommand MoveRows(ObjectName oldTable, ObjectName newTable, IEnumerable columnNames, bool avoidIdentityInsert = false) { SqlPreCommandSimple command = new SqlPreCommandSimple( @"INSERT INTO {0} ({2}) SELECT {3} -FROM {1} as [table]".FormatWith( +FROM {1} as [table];".FormatWith( newTable, oldTable, - columnNames.ToString(a => a.SqlEscape(), ", "), - columnNames.ToString(a => "[table]." + a.SqlEscape(), ", "))); + columnNames.ToString(a => a.SqlEscape(isPostgres), ", "), + columnNames.ToString(a => "[table]." + a.SqlEscape(isPostgres), ", "))); if (avoidIdentityInsert) return command; return SqlPreCommand.Combine(Spacing.Simple, - new SqlPreCommandSimple("SET IDENTITY_INSERT {0} ON".FormatWith(newTable)) { GoBefore = true }, + new SqlPreCommandSimple("SET IDENTITY_INSERT {0} ON;".FormatWith(newTable)) { GoBefore = true }, command, - new SqlPreCommandSimple("SET IDENTITY_INSERT {0} OFF".FormatWith(newTable)) { GoAfter = true })!; + new SqlPreCommandSimple("SET IDENTITY_INSERT {0} OFF;".FormatWith(newTable)) { GoAfter = true })!; } - public static SqlPreCommand RenameTable(ObjectName oldName, string newName) + public SqlPreCommand RenameTable(ObjectName oldName, string newName) { + if (IsPostgres) + return new SqlPreCommandSimple($"ALTER TABLE {oldName} RENAME TO {newName.SqlEscape(IsPostgres)};"); + return SP_RENAME(oldName.Schema.Database, oldName.OnDatabase(null).ToString(), newName, null); } - public static SqlPreCommandSimple AlterSchema(ObjectName oldName, SchemaName schemaName) + public SqlPreCommandSimple AlterSchema(ObjectName oldName, SchemaName schemaName) { - return new SqlPreCommandSimple("ALTER SCHEMA {0} TRANSFER {1};".FormatWith(schemaName.Name.SqlEscape(), oldName)); + if (IsPostgres) + return new SqlPreCommandSimple($"ALTER TABLE {oldName} SET SCHEMA {schemaName.Name.SqlEscape(IsPostgres)};"); + + return new SqlPreCommandSimple("ALTER SCHEMA {0} TRANSFER {1};".FormatWith(schemaName.Name.SqlEscape(isPostgres), oldName)); } - public static SqlPreCommand RenameColumn(ObjectName tableName, string oldName, string newName) + public SqlPreCommand RenameColumn(ObjectName tableName, string oldName, string newName) { + if (IsPostgres) + return new SqlPreCommandSimple($"ALTER TABLE {tableName} RENAME COLUMN {oldName.SqlEscape(IsPostgres)} TO {newName.SqlEscape(IsPostgres)};"); + return SP_RENAME(tableName.Schema.Database, tableName.OnDatabase(null) + "." + oldName, newName, "COLUMN"); } - public static SqlPreCommand RenameIndex(ObjectName tableName, string oldName, string newName) + public SqlPreCommand RenameIndex(ObjectName tableName, string oldName, string newName) { + if (IsPostgres) + return new SqlPreCommandSimple($"ALTER INDEX {oldName.SqlEscape(IsPostgres)} RENAME TO {newName.SqlEscape(IsPostgres)};"); + return SP_RENAME(tableName.Schema.Database, tableName.OnDatabase(null) + "." + oldName, newName, "INDEX"); } #endregion - public static SqlPreCommandSimple SetIdentityInsert(ObjectName tableName, bool value) + public SqlPreCommandSimple SetIdentityInsert(ObjectName tableName, bool value) { return new SqlPreCommandSimple("SET IDENTITY_INSERT {0} {1}".FormatWith( tableName, value ? "ON" : "OFF")); } - public static SqlPreCommandSimple SetSingleUser(string databaseName) + public SqlPreCommandSimple SetSingleUser(string databaseName) { return new SqlPreCommandSimple("ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;".FormatWith(databaseName)); } - public static SqlPreCommandSimple SetMultiUser(string databaseName) + public SqlPreCommandSimple SetMultiUser(string databaseName) { return new SqlPreCommandSimple("ALTER DATABASE {0} SET MULTI_USER;".FormatWith(databaseName)); } - public static SqlPreCommandSimple SetSnapshotIsolation(string databaseName, bool value) + public SqlPreCommandSimple SetSnapshotIsolation(string databaseName, bool value) { - return new SqlPreCommandSimple("ALTER DATABASE {0} SET ALLOW_SNAPSHOT_ISOLATION {1}".FormatWith(databaseName, value ? "ON" : "OFF")); + return new SqlPreCommandSimple("ALTER DATABASE {0} SET ALLOW_SNAPSHOT_ISOLATION {1};".FormatWith(databaseName, value ? "ON" : "OFF")); } - public static SqlPreCommandSimple MakeSnapshotIsolationDefault(string databaseName, bool value) + public SqlPreCommandSimple MakeSnapshotIsolationDefault(string databaseName, bool value) { - return new SqlPreCommandSimple("ALTER DATABASE {0} SET READ_COMMITTED_SNAPSHOT {1}".FormatWith(databaseName, value ? "ON" : "OFF")); + return new SqlPreCommandSimple("ALTER DATABASE {0} SET READ_COMMITTED_SNAPSHOT {1};".FormatWith(databaseName, value ? "ON" : "OFF")); } - public static SqlPreCommandSimple SelectRowCount() + public SqlPreCommandSimple SelectRowCount() { return new SqlPreCommandSimple("select @@rowcount;"); } - public static SqlPreCommand CreateSchema(SchemaName schemaName) + public SqlPreCommand CreateSchema(SchemaName schemaName) { if (schemaName.Database == null) - return new SqlPreCommandSimple("CREATE SCHEMA {0}".FormatWith(schemaName)) { GoAfter = true, GoBefore = true }; + return new SqlPreCommandSimple("CREATE SCHEMA {0};".FormatWith(schemaName)) { GoAfter = true, GoBefore = true }; else - return new SqlPreCommandSimple($"EXEC('use {schemaName.Database}; EXEC sp_executesql N''CREATE SCHEMA {schemaName.Name}'' ')"); + return new SqlPreCommandSimple($"EXEC('use {schemaName.Database}; EXEC sp_executesql N''CREATE SCHEMA {schemaName.Name}'' ');"); } - public static SqlPreCommand DropSchema(SchemaName schemaName) + public SqlPreCommand DropSchema(SchemaName schemaName) { - return new SqlPreCommandSimple("DROP SCHEMA {0}".FormatWith(schemaName)) { GoAfter = true, GoBefore = true }; + return new SqlPreCommandSimple("DROP SCHEMA {0};".FormatWith(schemaName)) { GoAfter = true, GoBefore = true }; } - public static SqlPreCommandSimple DisableForeignKey(ObjectName tableName, string foreignKey) + public SqlPreCommandSimple DisableForeignKey(ObjectName tableName, string foreignKey) { - return new SqlPreCommandSimple("ALTER TABLE {0} NOCHECK CONSTRAINT {1}".FormatWith(tableName, foreignKey)); + return new SqlPreCommandSimple("ALTER TABLE {0} NOCHECK CONSTRAINT {1};".FormatWith(tableName, foreignKey)); } - public static SqlPreCommandSimple EnableForeignKey(ObjectName tableName, string foreignKey) + public SqlPreCommandSimple EnableForeignKey(ObjectName tableName, string foreignKey) { - return new SqlPreCommandSimple("ALTER TABLE {0} WITH CHECK CHECK CONSTRAINT {1}".FormatWith(tableName, foreignKey)); + return new SqlPreCommandSimple("ALTER TABLE {0} WITH CHECK CHECK CONSTRAINT {1};".FormatWith(tableName, foreignKey)); } - public static SqlPreCommandSimple DisableIndex(ObjectName tableName, string indexName) + public SqlPreCommandSimple DisableIndex(ObjectName tableName, string indexName) { - return new SqlPreCommandSimple("ALTER INDEX [{0}] ON {1} DISABLE".FormatWith(indexName, tableName)); + return new SqlPreCommandSimple("ALTER INDEX [{0}] ON {1} DISABLE;".FormatWith(indexName, tableName)); } - public static SqlPreCommandSimple RebuildIndex(ObjectName tableName, string indexName) + public SqlPreCommandSimple RebuildIndex(ObjectName tableName, string indexName) { - return new SqlPreCommandSimple("ALTER INDEX [{0}] ON {1} REBUILD".FormatWith(indexName, tableName)); + return new SqlPreCommandSimple("ALTER INDEX [{0}] ON {1} REBUILD;".FormatWith(indexName, tableName)); } - public static SqlPreCommandSimple DropPrimaryKeyConstraint(ObjectName tableName) + public SqlPreCommandSimple DropPrimaryKeyConstraint(ObjectName tableName) { DatabaseName? db = tableName.Schema.Database; @@ -643,16 +662,14 @@ EXEC DB.dbo.sp_executesql @sql" return new SqlPreCommandSimple(command); } - - internal static SqlPreCommand? DropStatistics(string tn, List list) + internal SqlPreCommand? DropStatistics(string tn, List list) { if (list.IsEmpty()) return null; - return new SqlPreCommandSimple("DROP STATISTICS " + list.ToString(s => tn.SqlEscape() + "." + s.StatsName.SqlEscape(), ",\r\n")); + return new SqlPreCommandSimple("DROP STATISTICS " + list.ToString(s => tn.SqlEscape(isPostgres) + "." + s.StatsName.SqlEscape(isPostgres), ",\r\n") + ";"); } - - public static SqlPreCommand TruncateTable(ObjectName tableName) => new SqlPreCommandSimple($"TRUNCATE TABLE {tableName}"); + public SqlPreCommand TruncateTable(ObjectName tableName) => new SqlPreCommandSimple($"TRUNCATE TABLE {tableName};"); } } diff --git a/Signum.Engine/Engine/SqlPreCommand.cs b/Signum.Engine/Engine/SqlPreCommand.cs index 1e9514d904..d209f7d9d1 100644 --- a/Signum.Engine/Engine/SqlPreCommand.cs +++ b/Signum.Engine/Engine/SqlPreCommand.cs @@ -11,6 +11,7 @@ using System.Data.Common; using System.Globalization; using Signum.Engine.Maps; +using Npgsql; namespace Signum.Engine { @@ -83,27 +84,112 @@ public static SqlPreCommand PlainSqlCommand(this SqlPreCommand command) .Combine(Spacing.Simple)!; } - public static bool AvoidOpenOpenSqlFileRetry = true; - public static void OpenSqlFileRetry(this SqlPreCommand command) { SafeConsole.WriteLineColor(ConsoleColor.Yellow, "There are changes!"); - string file = command.OpenSqlFile(); - if (!AvoidOpenOpenSqlFileRetry && SafeConsole.Ask("Open again?")) - Process.Start(file); + var fileName = "Sync {0:dd-MM-yyyy hh_mm_ss}.sql".FormatWith(DateTime.Now); + + Save(command, fileName); + SafeConsole.WriteLineColor(ConsoleColor.DarkYellow, command.PlainSql()); + + Console.WriteLine("Script saved in: " + Path.Combine(Directory.GetCurrentDirectory(), fileName)); + var answer = SafeConsole.AskRetry("Open or run?", "open", "run", "exit"); + + if(answer == "open") + { + Thread.Sleep(1000); + Open(fileName); + if (SafeConsole.Ask("run now?")) + ExecuteRetry(fileName); + } + else if(answer == "run") + { + ExecuteRetry(fileName); + } } - public static string OpenSqlFile(this SqlPreCommand command) + static void ExecuteRetry(string fileName) { - return OpenSqlFile(command, "Sync {0:dd-MM-yyyy hh_mm_ss}.sql".FormatWith(DateTime.Now)); + retry: + try + { + var script = File.ReadAllText(fileName); + ExecuteScript("script", script); + } + catch (ExecuteSqlScriptException) + { + Console.WriteLine("The current script is in saved in: " + Path.Combine(Directory.GetCurrentDirectory(), fileName)); + if (SafeConsole.Ask("retry?")) + goto retry; + + } } - public static string OpenSqlFile(this SqlPreCommand command, string fileName) + public static int Timeout = 20 * 60; + + public static void ExecuteScript(string title, string script) { - Save(command, fileName); + using (Connector.CommandTimeoutScope(Timeout)) + { + var regex = new Regex(@" *(GO|USE \w+|USE \[[^\]]+\]) *(\r?\n|$)", RegexOptions.IgnoreCase); + + var parts = regex.Split(script); + + var realParts = parts.Where(a => !string.IsNullOrWhiteSpace(a) && !regex.IsMatch(a)).ToArray(); + + int pos = 0; + + try + { + for (pos = 0; pos < realParts.Length; pos++) + { + SafeConsole.WaitExecute("Executing {0} [{1}/{2}]".FormatWith(title, pos + 1, realParts.Length), () => Executor.ExecuteNonQuery(realParts[pos])); + } + } + catch (Exception ex) + { + var sqlE = ex as SqlException ?? ex.InnerException as SqlException; + var pgE = ex as PostgresException ?? ex.InnerException as PostgresException; + if (sqlE == null && pgE == null) + throw; + + Console.WriteLine(); + Console.WriteLine(); + + var list = script.Lines(); + + var lineNumer = (pgE?.Line?.ToInt() ?? sqlE!.LineNumber - 1) + pos + parts.Take(parts.IndexOf(realParts[pos])).Sum(a => a.Lines().Length); - Thread.Sleep(1000); + SafeConsole.WriteLineColor(ConsoleColor.Red, "ERROR:"); + var min = Math.Max(0, lineNumer - 20); + var max = Math.Min(list.Length - 1, lineNumer + 20); + + if (min > 0) + Console.WriteLine("..."); + + for (int i = min; i <= max; i++) + { + Console.Write(i + ": "); + SafeConsole.WriteLineColor(i == lineNumer ? ConsoleColor.Red : ConsoleColor.DarkRed, list[i]); + } + + if (max < list.Length - 1) + Console.WriteLine("..."); + + Console.WriteLine(); + SafeConsole.WriteLineColor(ConsoleColor.DarkRed, ex.GetType().Name + " (Number {0}): ".FormatWith(pgE?.SqlState ?? sqlE?.Number.ToString())); + SafeConsole.WriteLineColor(ConsoleColor.Red, ex.Message); + + Console.WriteLine(); + + throw new ExecuteSqlScriptException(ex.Message, ex); + } + } + } + + private static void Open(string fileName) + { new Process { StartInfo = new ProcessStartInfo(Path.Combine(Directory.GetCurrentDirectory(), fileName)) @@ -111,10 +197,9 @@ public static string OpenSqlFile(this SqlPreCommand command, string fileName) UseShellExecute = true } }.Start(); - - return fileName; } + public static void Save(this SqlPreCommand command, string fileName) { string content = command.PlainSql(); @@ -123,6 +208,18 @@ public static void Save(this SqlPreCommand command, string fileName) } } + + [Serializable] + public class ExecuteSqlScriptException : Exception + { + public ExecuteSqlScriptException() { } + public ExecuteSqlScriptException(string message) : base(message) { } + public ExecuteSqlScriptException(string message, Exception inner) : base(message, inner) { } + protected ExecuteSqlScriptException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + public class SqlPreCommandSimple : SqlPreCommand { public override bool GoBefore { get; set; } @@ -159,7 +256,7 @@ protected internal override int NumParameters static readonly Regex regex = new Regex(@"@[_\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nl}][_\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nl}\p{Nd}]*"); - internal static string Encode(object value) + internal static string Encode(object? value) { if (value == null || value == DBNull.Value) return "NULL"; @@ -167,17 +264,32 @@ internal static string Encode(object value) if (value is string s) return "\'" + s.Replace("'", "''") + "'"; + if (value is char c) + return "\'" + c.ToString().Replace("'", "''") + "'"; + if (value is Guid g) return "\'" + g.ToString() + "'"; if (value is DateTime dt) - return "convert(datetime, '{0}', 126)".FormatWith(dt.ToString("yyyy-MM-ddThh:mm:ss.fff", CultureInfo.InvariantCulture)); + { + return Schema.Current.Settings.IsPostgres ? + "'{0}'".FormatWith(dt.ToString("yyyy-MM-dd hh:mm:ss.fff", CultureInfo.InvariantCulture)) : + "convert(datetime, '{0}', 126)".FormatWith(dt.ToString("yyyy-MM-ddThh:mm:ss.fff", CultureInfo.InvariantCulture)); + } if (value is TimeSpan ts) - return "convert(time, '{0:g}')".FormatWith(ts.ToString("g", CultureInfo.InvariantCulture)); + { + var str = ts.ToString("g", CultureInfo.InvariantCulture); + return Schema.Current.Settings.IsPostgres ? str : "convert(time, '{0}')".FormatWith(str); + } if (value is bool b) + { + if (Schema.Current.Settings.IsPostgres) + return b.ToString(); + return (b ? 1 : 0).ToString(); + } if (Schema.Current.Settings.UdtSqlName.TryGetValue(value.GetType(), out var name)) return "CAST('{0}' AS {1})".FormatWith(value, name); @@ -188,6 +300,9 @@ internal static string Encode(object value) if (value is byte[] bytes) return "0x" + BitConverter.ToString(bytes).Replace("-", ""); + if (value is IFormattable f) + return f.ToString(null, CultureInfo.InvariantCulture); + return value.ToString()!; } @@ -206,8 +321,9 @@ protected internal override void PlainSql(StringBuilder sb) public string sp_executesql() { var pars = this.Parameters.EmptyIfNull(); + var sqlBuilder = Connector.Current.SqlBuilder; - var parameterVars = pars.ToString(p => $"{p.ParameterName} {((SqlParameter)p).SqlDbType.ToString()}{SqlBuilder.GetSizeScale(p.Size.DefaultToNull(), p.Scale.DefaultToNull())}", ", "); + var parameterVars = pars.ToString(p => $"{p.ParameterName} {(p is SqlParameter sp ? sp.SqlDbType.ToString() : ((NpgsqlParameter)p).NpgsqlDbType.ToString())}{sqlBuilder.GetSizeScale(p.Size.DefaultToNull(), p.Scale.DefaultToNull())}", ", "); var parameterValues = pars.ToString(p => Encode(p.Value), ","); return $"EXEC sp_executesql N'{this.Sql}', N'{parameterVars}', {parameterValues}"; diff --git a/Signum.Engine/Engine/SqlUtils.cs b/Signum.Engine/Engine/SqlUtils.cs index 337e8d1781..f964cf2cbe 100644 --- a/Signum.Engine/Engine/SqlUtils.cs +++ b/Signum.Engine/Engine/SqlUtils.cs @@ -9,7 +9,7 @@ namespace Signum.Engine { public static class SqlUtils { - static HashSet Keywords = + static HashSet KeywordsSqlServer = @"ADD ALL ALTER @@ -210,16 +210,658 @@ public static class SqlUtils WORK WRITETEXT".Lines().Select(a => a.Trim().ToUpperInvariant()).ToHashSet(); - public static string SqlEscape(this string ident) + static HashSet KeywordsPostgres = +@"A +ABORT +ABS +ABSOLUTE +ACCESS +ACTION +ADA +ADD +ADMIN +AFTER +AGGREGATE +ALIAS +ALL +ALLOCATE +ALSO +ALTER +ALWAYS +ANALYSE +ANALYZE +AND +ANY +ARE +ARRAY +AS +ASC +ASENSITIVE +ASSERTION +ASSIGNMENT +ASYMMETRIC +AT +ATOMIC +ATTRIBUTE +ATTRIBUTES +AUTHORIZATION +AVG +BACKWARD +BEFORE +BEGIN +BERNOULLI +BETWEEN +BIGINT +BINARY +BIT +BITVAR +BIT_LENGTH +BLOB +BOOLEAN +BOTH +BREADTH +BY +C +CACHE +CALL +CALLED +CARDINALITY +CASCADE +CASCADED +CASE +CAST +CATALOG +CATALOG_NAME +CEIL +CEILING +CHAIN +CHAR +CHARACTER +CHARACTERISTICS +CHARACTERS +CHARACTER_LENGTH +CHARACTER_SET_CATALOG +CHARACTER_SET_NAME +CHARACTER_SET_SCHEMA +CHAR_LENGTH +CHECK +CHECKED +CHECKPOINT +CLASS +CLASS_ORIGIN +CLOB +CLOSE +CLUSTER +COALESCE +COBOL +COLLATE +COLLATION +COLLATION_CATALOG +COLLATION_NAME +COLLATION_SCHEMA +COLLECT +COLUMN +COLUMN_NAME +COMMAND_FUNCTION +COMMAND_FUNCTION_CODE +COMMENT +COMMIT +COMMITTED +COMPLETION +CONDITION +CONDITION_NUMBER +CONNECT +CONNECTION +CONNECTION_NAME +CONSTRAINT +CONSTRAINTS +CONSTRAINT_CATALOG +CONSTRAINT_NAME +CONSTRAINT_SCHEMA +CONSTRUCTOR +CONTAINS +CONTINUE +CONVERSION +CONVERT +COPY +CORR +CORRESPONDING +COUNT +COVAR_POP +COVAR_SAMP +CREATE +CREATEDB +CREATEROLE +CREATEUSER +CROSS +CSV +CUBE +CUME_DIST +CURRENT +CURRENT_DATE +CURRENT_DEFAULT_TRANSFORM_GROUP +CURRENT_PATH +CURRENT_ROLE +CURRENT_TIME +CURRENT_TIMESTAMP +CURRENT_TRANSFORM_GROUP_FOR_TYPE +CURRENT_USER +CURSOR +CURSOR_NAME +CYCLE +DATA +DATABASE +DATE +DATETIME_INTERVAL_CODE +DATETIME_INTERVAL_PRECISION +DAY +DEALLOCATE +DEC +DECIMAL +DECLARE +DEFAULT +DEFAULTS +DEFERRABLE +DEFERRED +DEFINED +DEFINER +DEGREE +DELETE +DELIMITER +DELIMITERS +DENSE_RANK +DEPTH +DEREF +DERIVED +DESC +DESCRIBE +DESCRIPTOR +DESTROY +DESTRUCTOR +DETERMINISTIC +DIAGNOSTICS +DICTIONARY +DISABLE +DISCONNECT +DISPATCH +DISTINCT +DO +DOMAIN +DOUBLE +DROP +DYNAMIC +DYNAMIC_FUNCTION +DYNAMIC_FUNCTION_CODE +EACH +ELEMENT +ELSE +ENABLE +ENCODING +ENCRYPTED +END +END-EXEC +EQUALS +ESCAPE +EVERY +EXCEPT +EXCEPTION +EXCLUDE +EXCLUDING +EXCLUSIVE +EXEC +EXECUTE +EXISTING +EXISTS +EXP +EXPLAIN +EXTERNAL +EXTRACT +FALSE +FETCH +FILTER +FINAL +FIRST +FLOAT +FLOOR +FOLLOWING +FOR +FORCE +FOREIGN +FORTRAN +FORWARD +FOUND +FREE +FREEZE +FROM +FULL +FUNCTION +FUSION +G +GENERAL +GENERATED +GET +GLOBAL +GO +GOTO +GRANT +GRANTED +GREATEST +GROUP +GROUPING +HANDLER +HAVING +HEADER +HIERARCHY +HOLD +HOST +HOUR +IDENTITY +IGNORE +ILIKE +IMMEDIATE +IMMUTABLE +IMPLEMENTATION +IMPLICIT +IN +INCLUDING +INCREMENT +INDEX +INDICATOR +INFIX +INHERIT +INHERITS +INITIALIZE +INITIALLY +INNER +INOUT +INPUT +INSENSITIVE +INSERT +INSTANCE +INSTANTIABLE +INSTEAD +INT +INTEGER +INTERSECT +INTERSECTION +INTERVAL +INTO +INVOKER +IS +ISNULL +ISOLATION +ITERATE +JOIN +K +KEY +KEY_MEMBER +KEY_TYPE +LANCOMPILER +LANGUAGE +LARGE +LAST +LATERAL +LEADING +LEAST +LEFT +LENGTH +LESS +LEVEL +LIKE +LIMIT +LISTEN +LN +LOAD +LOCAL +LOCALTIME +LOCALTIMESTAMP +LOCATION +LOCATOR +LOCK +LOGIN +LOWER +M +MAP +MATCH +MATCHED +MAX +MAXVALUE +MEMBER +MERGE +MESSAGE_LENGTH +MESSAGE_OCTET_LENGTH +MESSAGE_TEXT +METHOD +MIN +MINUTE +MINVALUE +MOD +MODE +MODIFIES +MODIFY +MODULE +MONTH +MORE +MOVE +MULTISET +MUMPS +NAME +NAMES +NATIONAL +NATURAL +NCHAR +NCLOB +NESTING +NEW +NEXT +NO +NOCREATEDB +NOCREATEROLE +NOCREATEUSER +NOINHERIT +NOLOGIN +NONE +NORMALIZE +NORMALIZED +NOSUPERUSER +NOT +NOTHING +NOTIFY +NOTNULL +NOWAIT +NULL +NULLABLE +NULLIF +NULLS +NUMBER +NUMERIC +OBJECT +OCTETS +OCTET_LENGTH +OF +OFF +OFFSET +OIDS +OLD +ON +ONLY +OPEN +OPERATION +OPERATOR +OPTION +OPTIONS +OR +ORDER +ORDERING +ORDINALITY +OTHERS +OUT +OUTER +OUTPUT +OVER +OVERLAPS +OVERLAY +OVERRIDING +OWNER +PAD +PARAMETER +PARAMETERS +PARAMETER_MODE +PARAMETER_NAME +PARAMETER_ORDINAL_POSITION +PARAMETER_SPECIFIC_CATALOG +PARAMETER_SPECIFIC_NAME +PARAMETER_SPECIFIC_SCHEMA +PARTIAL +PARTITION +PASCAL +PASSWORD +PATH +PERCENTILE_CONT +PERCENTILE_DISC +PERCENT_RANK +PLACING +PLI +POSITION +POSTFIX +POWER +PRECEDING +PRECISION +PREFIX +PREORDER +PREPARE +PREPARED +PRESERVE +PRIMARY +PRIOR +PRIVILEGES +PROCEDURAL +PROCEDURE +PUBLIC +QUOTE +RANGE +RANK +READ +READS +REAL +RECHECK +RECURSIVE +REF +REFERENCES +REFERENCING +REGR_AVGX +REGR_AVGY +REGR_COUNT +REGR_INTERCEPT +REGR_R2 +REGR_SLOPE +REGR_SXX +REGR_SXY +REGR_SYY +REINDEX +RELATIVE +RELEASE +RENAME +REPEATABLE +REPLACE +RESET +RESTART +RESTRICT +RESULT +RETURN +RETURNED_CARDINALITY +RETURNED_LENGTH +RETURNED_OCTET_LENGTH +RETURNED_SQLSTATE +RETURNS +REVOKE +RIGHT +ROLE +ROLLBACK +ROLLUP +ROUTINE +ROUTINE_CATALOG +ROUTINE_NAME +ROUTINE_SCHEMA +ROW +ROWS +ROW_COUNT +ROW_NUMBER +RULE +SAVEPOINT +SCALE +SCHEMA +SCHEMA_NAME +SCOPE +SCOPE_CATALOG +SCOPE_NAME +SCOPE_SCHEMA +SCROLL +SEARCH +SECOND +SECTION +SECURITY +SELECT +SELF +SENSITIVE +SEQUENCE +SERIALIZABLE +SERVER_NAME +SESSION +SESSION_USER +SET +SETOF +SETS +SHARE +SHOW +SIMILAR +SIMPLE +SIZE +SMALLINT +SOME +SOURCE +SPACE +SPECIFIC +SPECIFICTYPE +SPECIFIC_NAME +SQL +SQLCODE +SQLERROR +SQLEXCEPTION +SQLSTATE +SQLWARNING +SQRT +STABLE +START +STATE +STATEMENT +STATIC +STATISTICS +STDDEV_POP +STDDEV_SAMP +STDIN +STDOUT +STORAGE +STRICT +STRUCTURE +STYLE +SUBCLASS_ORIGIN +SUBLIST +SUBMULTISET +SUBSTRING +SUM +SUPERUSER +SYMMETRIC +SYSID +SYSTEM +SYSTEM_USER +TABLE +TABLESAMPLE +TABLESPACE +TABLE_NAME +TEMP +TEMPLATE +TEMPORARY +TERMINATE +THAN +THEN +TIES +TIME +TIMESTAMP +TIMEZONE_HOUR +TIMEZONE_MINUTE +TO +TOAST +TOP_LEVEL_COUNT +TRAILING +TRANSACTION +TRANSACTIONS_COMMITTED +TRANSACTIONS_ROLLED_BACK +TRANSACTION_ACTIVE +TRANSFORM +TRANSFORMS +TRANSLATE +TRANSLATION +TREAT +TRIGGER +TRIGGER_CATALOG +TRIGGER_NAME +TRIGGER_SCHEMA +TRIM +TRUE +TRUNCATE +TRUSTED +TYPE +UESCAPE +UNBOUNDED +UNCOMMITTED +UNDER +UNENCRYPTED +UNION +UNIQUE +UNKNOWN +UNLISTEN +UNNAMED +UNNEST +UNTIL +UPDATE +UPPER +USAGE +USER +USER_DEFINED_TYPE_CATALOG +USER_DEFINED_TYPE_CODE +USER_DEFINED_TYPE_NAME +USER_DEFINED_TYPE_SCHEMA +USING +VACUUM +VALID +VALIDATOR +VALUE +VALUES +VARCHAR +VARIABLE +VARYING +VAR_POP +VAR_SAMP +VERBOSE +VIEW +VOLATILE +WHEN +WHENEVER +WHERE +WIDTH_BUCKET +WINDOW +WITH +WITHIN +WITHOUT +WORK +WRITE +YEAR +ZONE".Lines().Select(a => a.Trim().ToUpperInvariant()).ToHashSet(); + + + public static string SqlEscape(this string ident, bool isPostgres) { - if (Keywords.Contains(ident.ToUpperInvariant()) || Regex.IsMatch(ident, @"-\d|[áéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ/\\. -]")) - return "[" + ident + "]"; + if (isPostgres) + { + if (ident.ToLowerInvariant() != ident || KeywordsSqlServer.Contains(ident.ToUpperInvariant()) || Regex.IsMatch(ident, @"-\d|[áéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ/\\. -]")) + return "\"" + ident + "\""; + + return ident; + } + else + { + if (KeywordsSqlServer.Contains(ident.ToUpperInvariant()) || Regex.IsMatch(ident, @"-\d|[áéíóúàèìòùÁÉÍÓÚÀÈÌÒÙ/\\. -]")) + return "[" + ident + "]"; - return ident; + return ident; + } } public static SqlPreCommand? RemoveDuplicatedIndices() { + var isPostgres = Schema.Current.Settings.IsPostgres; + var sqlBuilder = Connector.Current.SqlBuilder; var plainData = (from s in Database.View() from t in s.Tables() from ix in t.Indices() @@ -228,7 +870,7 @@ from c in t.Columns() where ic.column_id == c.column_id select new { - table = new ObjectName(new SchemaName(null, s.name), t.name), + table = new ObjectName(new SchemaName(null, s.name, isPostgres), t.name, isPostgres), index = ix.name, ix.is_unique, column = c.name, @@ -251,7 +893,7 @@ from c in t.Columns() var best = gr.OrderByDescending(a => a.is_unique).ThenByDescending(a => a.index!/*CSBUG*/.StartsWith("IX")).ThenByDescending(a => a.index).First(); return gr.Where(g => g != best) - .Select(g => SqlBuilder.DropIndex(t.Key!, g.index!)) + .Select(g => sqlBuilder.DropIndex(t.Key!, g.index!)) .PreAnd(new SqlPreCommandSimple("-- DUPLICATIONS OF {0}".FormatWith(best.index))).Combine(Spacing.Simple); }) ).Combine(Spacing.Double); diff --git a/Signum.Engine/Engine/Synchronizer.cs b/Signum.Engine/Engine/Synchronizer.cs index 11612ff50a..da46ce4dd5 100644 --- a/Signum.Engine/Engine/Synchronizer.cs +++ b/Signum.Engine/Engine/Synchronizer.cs @@ -122,8 +122,15 @@ public static void SynchronizeReplacing( where N : class where K : notnull { - return newDictionary.OuterJoinDictionaryCC(oldDictionary, (key, newVal, oldVal) => + HashSet set = new HashSet(); + set.UnionWith(newDictionary.Keys); + set.UnionWith(oldDictionary.Keys); + + return set.Select(key => { + var newVal = newDictionary.TryGetC(key); + var oldVal = oldDictionary.TryGetC(key); + if (newVal == null) return removeOld == null ? null : removeOld(key, oldVal!); @@ -131,7 +138,7 @@ public static void SynchronizeReplacing( return createNew == null ? null : createNew(key, newVal); return mergeBoth == null ? null : mergeBoth(key, newVal, oldVal); - }).Values.Combine(spacing); + }).Combine(spacing); } @@ -157,7 +164,7 @@ public static void SynchronizeReplacing( return SynchronizeScript(spacing, newDictionary, repOldDictionary, createNew, removeOld, mergeBoth); } - public static IDisposable? RenameTable(Table table, Replacements replacements) + public static IDisposable? UseOldTableName(Table table, Replacements replacements) { string? fullName = replacements.TryGetC(Replacements.KeyTablesInverse)?.TryGetC(table.Name.ToString()); if (fullName == null) @@ -165,7 +172,7 @@ public static void SynchronizeReplacing( ObjectName realName = table.Name; - table.Name = ObjectName.Parse(fullName); + table.Name = ObjectName.Parse(fullName, Schema.Current.Settings.IsPostgres); return new Disposable(() => table.Name = realName); } diff --git a/Signum.Engine/Engine/Sys.Tables.cs b/Signum.Engine/Engine/SysTables.cs similarity index 95% rename from Signum.Engine/Engine/Sys.Tables.cs rename to Signum.Engine/Engine/SysTables.cs index d1166e5b23..436f573c0e 100644 --- a/Signum.Engine/Engine/Sys.Tables.cs +++ b/Signum.Engine/Engine/SysTables.cs @@ -75,13 +75,6 @@ public IQueryable Tables() => As.Expression(() => Database.View().Where(t => t.schema_id == this.schema_id)); } - public enum SysTableTemporalType - { - None = 0, - HistoryTable = 1, - SystemVersionTemporalTable = 2 - } - [TableName("sys.periods")] public class SysPeriods : IView { diff --git a/Signum.Engine/Engine/SysTablesSchema.cs b/Signum.Engine/Engine/SysTablesSchema.cs new file mode 100644 index 0000000000..2a98d2732d --- /dev/null +++ b/Signum.Engine/Engine/SysTablesSchema.cs @@ -0,0 +1,183 @@ +using Signum.Engine.Maps; +using Signum.Engine.SchemaInfoTables; +using Signum.Utilities; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; + +namespace Signum.Engine.Engine +{ + public static class SysTablesSchema + { + public static Dictionary GetDatabaseDescription(List databases) + { + List allTables = new List(); + + var isPostgres = false; + + foreach (var db in databases) + { + SafeConsole.WriteColor(ConsoleColor.Cyan, '.'); + + using (Administrator.OverrideDatabaseInSysViews(db)) + { + var databaseName = db == null ? Connector.Current.DatabaseName() : db.Name; + + var sysDb = Database.View().Single(a => a.name == databaseName); + + var con = Connector.Current; + + var tables = + (from s in Database.View() + from t in s.Tables().Where(t => !t.ExtendedProperties().Any(a => a.name == "microsoft_database_tools_support")) //IntelliSense bug + select new DiffTable + { + Name = new ObjectName(new SchemaName(db, s.name, isPostgres), t.name, isPostgres), + + TemporalType = !con.SupportsTemporalTables ? SysTableTemporalType.None : t.temporal_type, + + Period = !con.SupportsTemporalTables ? null : + (from p in t.Periods() + join sc in t.Columns() on p.start_column_id equals sc.column_id + join ec in t.Columns() on p.end_column_id equals ec.column_id + select new DiffPeriod + { + StartColumnName = sc.name, + EndColumnName = ec.name, + }).SingleOrDefaultEx(), + + TemporalTableName = !con.SupportsTemporalTables || t.history_table_id == null ? null : + Database.View() + .Where(ht => ht.object_id == t.history_table_id) + .Select(ht => new ObjectName(new SchemaName(db, ht.Schema().name, isPostgres), ht.name, isPostgres)) + .SingleOrDefault(), + + PrimaryKeyName = (from k in t.KeyConstraints() + where k.type == "PK" + select k.name == null ? null : new ObjectName(new SchemaName(db, k.Schema().name, isPostgres), k.name, isPostgres)) + .SingleOrDefaultEx(), + + Columns = (from c in t.Columns() + join userType in Database.View().DefaultIfEmpty() on c.user_type_id equals userType.user_type_id + join sysType in Database.View().DefaultIfEmpty() on c.system_type_id equals sysType.user_type_id + join ctr in Database.View().DefaultIfEmpty() on c.default_object_id equals ctr.object_id + select new DiffColumn + { + Name = c.name, + DbType = new AbstractDbType(sysType == null ? SqlDbType.Udt : ToSqlDbType(sysType.name)), + UserTypeName = sysType == null ? userType.name : null, + Nullable = c.is_nullable, + Collation = c.collation_name == sysDb.collation_name ? null : c.collation_name, + Length = c.max_length, + Precision = c.precision, + Scale = c.scale, + Identity = c.is_identity, + GeneratedAlwaysType = con.SupportsTemporalTables ? c.generated_always_type : GeneratedAlwaysType.None, + DefaultConstraint = ctr.name == null ? null : new DiffDefaultConstraint + { + Name = ctr.name, + Definition = ctr.definition + }, + PrimaryKey = t.Indices().Any(i => i.is_primary_key && i.IndexColumns().Any(ic => ic.column_id == c.column_id)), + }).ToDictionaryEx(a => a.Name, "columns"), + + MultiForeignKeys = (from fk in t.ForeignKeys() + join rt in Database.View() on fk.referenced_object_id equals rt.object_id + select new DiffForeignKey + { + Name = new ObjectName(new SchemaName(db, fk.Schema().name, isPostgres), fk.name, isPostgres), + IsDisabled = fk.is_disabled, + TargetTable = new ObjectName(new SchemaName(db, rt.Schema().name, isPostgres), rt.name, isPostgres), + Columns = fk.ForeignKeyColumns().Select(fkc => new DiffForeignKeyColumn + { + Parent = t.Columns().Single(c => c.column_id == fkc.parent_column_id).name, + Referenced = rt.Columns().Single(c => c.column_id == fkc.referenced_column_id).name + }).ToList(), + }).ToList(), + + SimpleIndices = (from i in t.Indices() + where /*!i.is_primary_key && */i.type != 0 /*heap indexes*/ + select new DiffIndex + { + IsUnique = i.is_unique, + IsPrimary = i.is_primary_key, + IndexName = i.name, + FilterDefinition = i.filter_definition, + Type = (DiffIndexType)i.type, + Columns = (from ic in i.IndexColumns() + join c in t.Columns() on ic.column_id equals c.column_id + orderby ic.index_column_id + select new DiffIndexColumn { ColumnName = c.name, IsIncluded = ic.is_included_column }).ToList() + }).ToList(), + + ViewIndices = (from v in Database.View() + where v.name.StartsWith("VIX_" + t.name + "_") + from i in v.Indices() + select new DiffIndex + { + IsUnique = i.is_unique, + ViewName = v.name, + IndexName = i.name, + Columns = (from ic in i.IndexColumns() + join c in v.Columns() on ic.column_id equals c.column_id + orderby ic.index_column_id + select new DiffIndexColumn { ColumnName = c.name, IsIncluded = ic.is_included_column }).ToList() + + }).ToList(), + + Stats = (from st in t.Stats() + where st.user_created + select new DiffStats + { + StatsName = st.name, + Columns = (from ic in st.StatsColumns() + join c in t.Columns() on ic.column_id equals c.column_id + select c.name).ToList() + }).ToList(), + + }).ToList(); + + if (SchemaSynchronizer.IgnoreTable != null) + tables.RemoveAll(SchemaSynchronizer.IgnoreTable); + + tables.ForEach(t => t.FixSqlColumnLengthSqlServer()); + tables.ForEach(t => t.ForeignKeysToColumns()); + + allTables.AddRange(tables); + } + } + + var database = allTables.ToDictionary(t => t.Name.ToString()); + + return database; + } + + public static SqlDbType ToSqlDbType(string str) + { + if (str == "numeric") + return SqlDbType.Decimal; + + return str.ToEnum(true); + } + + + public static HashSet GetSchemaNames(List list) + { + var sqlBuilder = Connector.Current.SqlBuilder; + var isPostgres = false; + HashSet result = new HashSet(); + foreach (var db in list) + { + using (Administrator.OverrideDatabaseInSysViews(db)) + { + var schemaNames = Database.View().Select(s => s.name).ToList().Except(sqlBuilder.SystemSchemas); + + result.AddRange(schemaNames.Select(sn => new SchemaName(db, sn, isPostgres)).Where(a => !SchemaSynchronizer.IgnoreSchema(a))); + } + } + return result; + } + } +} diff --git a/Signum.Engine/Linq/AliasGenerator.cs b/Signum.Engine/Linq/AliasGenerator.cs index 16dfe4b07b..f3d52d3852 100644 --- a/Signum.Engine/Linq/AliasGenerator.cs +++ b/Signum.Engine/Linq/AliasGenerator.cs @@ -9,6 +9,7 @@ namespace Signum.Engine.Linq public class AliasGenerator { readonly HashSet usedAliases = new HashSet(); + public bool isPostgres = Schema.Current.Settings.IsPostgres; int selectAliasCount = 0; public Alias NextSelectAlias() @@ -19,26 +20,26 @@ public Alias NextSelectAlias() public Alias GetUniqueAlias(string baseAlias) { if (usedAliases.Add(baseAlias)) - return new Alias(baseAlias); + return new Alias(baseAlias, isPostgres); for (int i = 1; ; i++) { string alias = baseAlias + i; if (usedAliases.Add(alias)) - return new Alias(alias); + return new Alias(alias, isPostgres); } } - public Alias Table(ObjectName name) + public Alias Table(ObjectName objectName) { - return new Alias(name); + return new Alias(objectName); } public Alias Raw(string name) { - return new Alias(name); + return new Alias(name, isPostgres); } public Alias NextTableAlias(string tableName) @@ -47,7 +48,7 @@ public Alias NextTableAlias(string tableName) tableName.Any(a => a == '_') ? new string(tableName.SplitNoEmpty('_' ).Select(s => s[0]).ToArray()) : null; if (!abv.HasText()) - abv = tableName.TryStart(3)!; + abv = tableName.TryStart(3).ToLower(); else abv = abv.ToLower(); @@ -66,14 +67,14 @@ public Alias CloneAlias(Alias alias) public class Alias: IEquatable { - public static readonly Alias Unknown = new Alias("Unknown"); - + public readonly bool isPostgres; public readonly string? Name; //Mutually exclusive public readonly ObjectName? ObjectName; //Mutually exclusive - internal Alias(string name) + internal Alias(string name, bool isPostgres) { this.Name = name; + this.isPostgres = isPostgres; } internal Alias(ObjectName objectName) @@ -98,7 +99,7 @@ public override int GetHashCode() public override string ToString() { - return Name?.SqlEscape() ?? ObjectName!.ToString(); + return Name?.SqlEscape(isPostgres) ?? ObjectName!.ToString(); } } } diff --git a/Signum.Engine/Linq/DbExpressions.Signum.cs b/Signum.Engine/Linq/DbExpressions.Signum.cs index c4c59ef8a4..4dcebe4f50 100644 --- a/Signum.Engine/Linq/DbExpressions.Signum.cs +++ b/Signum.Engine/Linq/DbExpressions.Signum.cs @@ -22,7 +22,7 @@ internal class EntityExpression : DbExpression public readonly Table Table; public readonly PrimaryKeyExpression ExternalId; - public readonly NewExpression? ExternalPeriod; + public readonly IntervalExpression? ExternalPeriod; //Optional public readonly Alias? TableAlias; @@ -31,15 +31,15 @@ internal class EntityExpression : DbExpression public readonly bool AvoidExpandOnRetrieving; - public readonly NewExpression? TablePeriod; + public readonly IntervalExpression? TablePeriod; public EntityExpression(Type type, PrimaryKeyExpression externalId, - NewExpression? externalPeriod, + IntervalExpression? externalPeriod, Alias? tableAlias, IEnumerable? bindings, - IEnumerable? mixins, - NewExpression? tablePeriod, bool avoidExpandOnRetrieving) + IEnumerable? mixins, + IntervalExpression? tablePeriod, bool avoidExpandOnRetrieving) : base(DbExpressionType.Entity, type) { if (type == null) @@ -254,10 +254,10 @@ internal class ImplementedByAllExpression : DbExpression { public readonly Expression Id; public readonly TypeImplementedByAllExpression TypeId; - public readonly NewExpression? ExternalPeriod; + public readonly IntervalExpression? ExternalPeriod; - public ImplementedByAllExpression(Type type, Expression id, TypeImplementedByAllExpression typeId, NewExpression? externalPeriod) + public ImplementedByAllExpression(Type type, Expression id, TypeImplementedByAllExpression typeId, IntervalExpression? externalPeriod) : base(DbExpressionType.ImplementedByAll, type) { if (id == null) @@ -430,9 +430,9 @@ internal class MListExpression : DbExpression { public readonly PrimaryKeyExpression BackID; // not readonly public readonly TableMList TableMList; - public readonly NewExpression? ExternalPeriod; + public readonly IntervalExpression? ExternalPeriod; - public MListExpression(Type type, PrimaryKeyExpression backID, NewExpression? externalPeriod, TableMList tr) + public MListExpression(Type type, PrimaryKeyExpression backID, IntervalExpression? externalPeriod, TableMList tr) : base(DbExpressionType.MList, type) { this.BackID = backID; @@ -454,10 +454,10 @@ protected override Expression Accept(DbExpressionVisitor visitor) internal class AdditionalFieldExpression : DbExpression { public readonly PrimaryKeyExpression BackID; // not readonly - public readonly NewExpression? ExternalPeriod; + public readonly IntervalExpression? ExternalPeriod; public readonly PropertyRoute Route; - public AdditionalFieldExpression(Type type, PrimaryKeyExpression backID, NewExpression? externalPeriod, PropertyRoute route) + public AdditionalFieldExpression(Type type, PrimaryKeyExpression backID, IntervalExpression? externalPeriod, PropertyRoute route) : base(DbExpressionType.AdditionalField, type) { this.BackID = backID; @@ -511,9 +511,9 @@ internal class MListElementExpression : DbExpression public readonly Alias Alias; - public readonly NewExpression? TablePeriod; + public readonly IntervalExpression? TablePeriod; - public MListElementExpression(PrimaryKeyExpression rowId, EntityExpression parent, Expression? order, Expression element, NewExpression? systemPeriod, TableMList table, Alias alias) + public MListElementExpression(PrimaryKeyExpression rowId, EntityExpression parent, Expression? order, Expression element, IntervalExpression? systemPeriod, TableMList table, Alias alias) : base(DbExpressionType.MListElement, typeof(MListElement<,>).MakeGenericType(parent.Type, element.Type)) { this.RowId = rowId; diff --git a/Signum.Engine/Linq/DbExpressions.Sql.cs b/Signum.Engine/Linq/DbExpressions.Sql.cs index bf358d2966..33065fc1b9 100644 --- a/Signum.Engine/Linq/DbExpressions.Sql.cs +++ b/Signum.Engine/Linq/DbExpressions.Sql.cs @@ -3,6 +3,7 @@ using Signum.Entities; using Signum.Entities.DynamicQuery; using Signum.Utilities; +using Signum.Utilities.DataStructures; using Signum.Utilities.ExpressionTrees; using Signum.Utilities.Reflection; using System; @@ -36,7 +37,7 @@ internal enum DbExpressionType SqlTableValuedFunction, SqlConstant, SqlVariable, - SqlEnum, + SqlLiteral, SqlCast, Case, RowNumber, @@ -50,7 +51,6 @@ internal enum DbExpressionType Delete, InsertSelect, CommandAggregate, - SelectRowCount, Entity = 2000, EmbeddedInit, MixinInit, @@ -68,6 +68,7 @@ internal enum DbExpressionType PrimaryKey, PrimaryKeyString, ToDayOfWeek, + Interval, } @@ -133,20 +134,25 @@ public SourceWithAliasExpression(DbExpressionType nodeType, Alias alias) internal class SqlTableValuedFunctionExpression : SourceWithAliasExpression { - public readonly Table Table; + public readonly Table? ViewTable; + public readonly Type? SingleColumnType; public readonly ReadOnlyCollection Arguments; - public readonly string SqlFunction; + public readonly ObjectName SqlFunction; public override Alias[] KnownAliases { get { return new[] { Alias }; } } - public SqlTableValuedFunctionExpression(string sqlFunction, Table table, Alias alias, IEnumerable arguments) + public SqlTableValuedFunctionExpression(ObjectName sqlFunction, Table? viewTable, Type? singleColumnType, Alias alias, IEnumerable arguments) : base(DbExpressionType.SqlTableValuedFunction, alias) { + if ((viewTable == null) == (singleColumnType == null)) + throw new ArgumentException("Either viewTable or singleColumn should be set"); + this.SqlFunction = sqlFunction; - this.Table = table; + this.ViewTable = viewTable; + this.SingleColumnType = singleColumnType; this.Arguments = arguments.ToReadOnly(); } @@ -159,12 +165,20 @@ public override string ToString() internal ColumnExpression GetIdExpression() { - var expression = ((ITablePrivate)Table).GetPrimaryOrder(Alias); + if (ViewTable != null) + { - if (expression == null) - throw new InvalidOperationException("Impossible to determine Primary Key for {0}".FormatWith(Table.Name)); + var expression = ((ITablePrivate)ViewTable).GetPrimaryOrder(Alias); - return expression; + if (expression == null) + throw new InvalidOperationException("Impossible to determine Primary Key for {0}".FormatWith(ViewTable.Name)); + + return expression; + } + else + { + return new ColumnExpression(this.SingleColumnType!, Alias, null); + } } protected override Expression Accept(DbExpressionVisitor visitor) @@ -198,7 +212,7 @@ internal TableExpression(Alias alias, ITable table, SystemTime? systemTime, stri public override string ToString() { - var st = SystemTime != null && SystemTime is SystemTime.HistoryTable ? "FOR SYSTEM_TIME " + SystemTime.ToString() : null; + var st = SystemTime != null && !(SystemTime is SystemTime.HistoryTable) ? " FOR SYSTEM_TIME " + SystemTime.ToString() : null; return $"{Name}{st} as {Alias}"; } @@ -222,16 +236,16 @@ protected override Expression Accept(DbExpressionVisitor visitor) internal class ColumnExpression : DbExpression, IEquatable { public readonly Alias Alias; - public readonly string Name; + public readonly string? Name; - internal ColumnExpression(Type type, Alias alias, string name) + internal ColumnExpression(Type type, Alias alias, string? name) : base(DbExpressionType.Column, type) { if (type.UnNullify() == typeof(PrimaryKey)) throw new ArgumentException("type should not be PrimaryKey"); this.Alias = alias ?? throw new ArgumentNullException(nameof(alias)); - this.Name = name ?? throw new ArgumentNullException(nameof(name)); + this.Name = name ?? (Schema.Current.Settings.IsPostgres ? (string?)null : throw new ArgumentNullException(nameof(name))); } public override string ToString() @@ -247,7 +261,7 @@ public bool Equals(ColumnExpression other) public override int GetHashCode() { - return Alias.GetHashCode() ^ Name.GetHashCode(); + return Alias.GetHashCode() ^ (Name?.GetHashCode() ?? -1); } protected override Expression Accept(DbExpressionVisitor visitor) @@ -286,33 +300,54 @@ internal enum AggregateSqlFunction StdDev, StdDevP, Count, + CountDistinct, Min, Max, Sum, + + string_agg, + } + + static class AggregateSqlFunctionExtensions + { + public static bool OrderMatters(this AggregateSqlFunction aggregateFunction) + { + switch (aggregateFunction) + { + case AggregateSqlFunction.Average: + case AggregateSqlFunction.StdDev: + case AggregateSqlFunction.StdDevP: + case AggregateSqlFunction.Count: + case AggregateSqlFunction.CountDistinct: + case AggregateSqlFunction.Min: + case AggregateSqlFunction.Max: + case AggregateSqlFunction.Sum: + return false; + case AggregateSqlFunction.string_agg: + return true; + default: + throw new UnexpectedValueException(aggregateFunction); + } + } } internal class AggregateExpression : DbExpression { - public readonly Expression Expression; - public readonly bool Distinct; public readonly AggregateSqlFunction AggregateFunction; - public AggregateExpression(Type type, Expression expression, AggregateSqlFunction aggregateFunction, bool distinct) + public readonly ReadOnlyCollection Arguments; + public AggregateExpression(Type type, AggregateSqlFunction aggregateFunction, IEnumerable arguments) : base(DbExpressionType.Aggregate, type) { - if (aggregateFunction != AggregateSqlFunction.Count && expression == null ) - throw new ArgumentNullException(nameof(expression)); - - if (distinct && (aggregateFunction != AggregateSqlFunction.Count || expression == null)) - throw new ArgumentException("Distinct only allowed for Count with expression"); - - this.Distinct = distinct; - this.Expression = expression; + if (arguments == null) + throw new ArgumentNullException(nameof(arguments)); + this.AggregateFunction = aggregateFunction; + this.Arguments = arguments.ToReadOnly(); } public override string ToString() { - return $"{AggregateFunction}({(Distinct ? "Distinct " : "")}{Expression?.ToString() ?? "*"})"; + return $"{AggregateFunction}({(AggregateFunction == AggregateSqlFunction.CountDistinct ? "Distinct " : "")}{Arguments.ToString(", ") ?? "*"})"; } protected override Expression Accept(DbExpressionVisitor visitor) @@ -421,7 +456,7 @@ internal SelectRoles SelectRoles if (GroupBy.Count > 0) roles |= SelectRoles.GroupBy; - else if (Columns.Any(cd => AggregateFinder.HasAggregates(cd.Expression))) + else if (AggregateFinder.GetAggregates(Columns) != null) roles |= SelectRoles.Aggregate; if (OrderBy.Count > 0) @@ -440,7 +475,7 @@ internal SelectRoles SelectRoles } } - public bool IsAllAggregates => Columns.Any() && Columns.All(a => a.Expression is AggregateExpression); + public bool IsAllAggregates => Columns.Any() && Columns.All(a => a.Expression is AggregateExpression ag && !ag.AggregateFunction.OrderMatters()); public override string ToString() { @@ -541,8 +576,19 @@ internal SetOperatorExpression(SetOperator @operator, SourceWithAliasExpression : base(DbExpressionType.SetOperator, alias) { this.Operator = @operator; - this.Left = left ?? throw new ArgumentNullException(nameof(left)); - this.Right = right ?? throw new ArgumentNullException(nameof(right)); + this.Left = Validate(left, nameof(left)); + this.Right = Validate(right, nameof(right)); + } + + static SourceWithAliasExpression Validate(SourceWithAliasExpression exp, string name) + { + if (exp == null) + throw new ArgumentNullException(name); + + if (exp is TableExpression || exp is SqlTableValuedFunctionExpression) + throw new ArgumentException($"{name} should not be a {exp.GetType().Name}"); + + return exp; } public override string ToString() @@ -602,11 +648,33 @@ internal enum SqlFunction COALESCE, CONVERT, - ISNULL, STUFF, COLLATE, } + internal enum PostgresFunction + { + strpos, + starts_with, + length, + EXTRACT, + trunc, + substr, + repeat, + date_trunc, + age, + tstzrange, + } + + public static class PostgressOperator + { + public static string Overlap = "&&"; + public static string Contains = "@>"; + + public static string[] All = new[] { Overlap, Contains }; + + } + internal enum SqlEnums { year, @@ -614,22 +682,23 @@ internal enum SqlEnums quarter, day, week, - weekday, + weekday, //Sql Server + dow, //Postgres hour, minute, second, millisecond, - dayofyear, - iso_week + dayofyear, //SQL Server + doy, //Postgres + epoch } - - - internal class SqlEnumExpression : DbExpression + internal class SqlLiteralExpression : DbExpression { - public readonly SqlEnums Value; - public SqlEnumExpression(SqlEnums value) - : base(DbExpressionType.SqlEnum, typeof(object)) + public readonly string Value; + public SqlLiteralExpression(SqlEnums value) : this(typeof(object), value.ToString()) { } + public SqlLiteralExpression(Type type, string value) + : base(DbExpressionType.SqlLiteral, type) { this.Value = value; } @@ -641,7 +710,7 @@ public override string ToString() protected override Expression Accept(DbExpressionVisitor visitor) { - return visitor.VisitSqlEnum(this); + return visitor.VisitSqlLiteral(this); } } @@ -666,10 +735,18 @@ protected override Expression Accept(DbExpressionVisitor visitor) return visitor.VisitToDayOfWeek(this); } - public static ResetLazy> DateFirst = new ResetLazy>(() => Tuple.Create((byte)Executor.ExecuteScalar("SELECT @@DATEFIRST")!)); + internal static MethodInfo miToDayOfWeekPostgres = ReflectionTools.GetMethodInfo(() => ToDayOfWeekPostgres(1)); + public static DayOfWeek? ToDayOfWeekPostgres(int? postgressWeekDay) + { + if (postgressWeekDay == null) + return null; + + return (DayOfWeek)(postgressWeekDay); + } + - internal static MethodInfo miToDayOfWeek = ReflectionTools.GetMethodInfo(() => ToDayOfWeek(1, 1)); - public static DayOfWeek? ToDayOfWeek(int? sqlServerWeekDay, byte dateFirst) + internal static MethodInfo miToDayOfWeekSql = ReflectionTools.GetMethodInfo(() => ToDayOfWeekSql(1, 1)); + public static DayOfWeek? ToDayOfWeekSql(int? sqlServerWeekDay, byte dateFirst) { if (sqlServerWeekDay == null) return null; @@ -685,7 +762,7 @@ public static int ToSqlWeekDay(DayOfWeek dayOfWeek, byte dateFirst /*keep parame internal class SqlCastExpression : DbExpression { - public readonly SqlDbType SqlDbType; + public readonly AbstractDbType DbType; public readonly Expression Expression; public SqlCastExpression(Type type, Expression expression) @@ -693,16 +770,16 @@ public SqlCastExpression(Type type, Expression expression) { } - public SqlCastExpression(Type type, Expression expression, SqlDbType sqlDbType) + public SqlCastExpression(Type type, Expression expression, AbstractDbType dbType) : base(DbExpressionType.SqlCast, type) { this.Expression = expression; - this.SqlDbType = sqlDbType; + this.DbType = dbType; } public override string ToString() { - return "Cast({0} as {1})".FormatWith(Expression.ToString(), SqlDbType.ToString().ToUpper()); + return "Cast({0} as {1})".FormatWith(Expression.ToString(), DbType.ToString(Schema.Current.Settings.IsPostgres)); } protected override Expression Accept(DbExpressionVisitor visitor) @@ -899,6 +976,75 @@ protected override Expression Accept(DbExpressionVisitor visitor) } } + internal class IntervalExpression : DbExpression + { + public readonly Expression? Min; + public readonly Expression? Max; + public readonly Expression? PostgresRange; + public readonly bool AsUtc; + + public IntervalExpression(Type type, Expression? min, Expression? max, Expression? postgresRange, bool asUtc) + :base(DbExpressionType.Interval, type) + + { + this.Min = min ?? (postgresRange == null ? throw new ArgumentException(nameof(min)) : (Expression?)null); + this.Max = max ?? (postgresRange == null ? throw new ArgumentException(nameof(max)) : (Expression?)null); + this.PostgresRange = postgresRange ?? ((min == null || max == null) ? throw new ArgumentException(nameof(min)) : (Expression?)null); + this.AsUtc = asUtc; + } + + public override string ToString() + { + var type = this.Type.GetGenericArguments()[0].TypeName(); + + if (PostgresRange != null) + return $"new Interval<{type}>({this.PostgresRange})"; + else + return $"new Interval<{type}>({this.Min}, {this.Max})"; + } + + protected override Expression Accept(DbExpressionVisitor visitor) + { + return visitor.VisitInterval(this); + } + } + + public static class SystemTimeExpressions + { + static MethodInfo miOverlaps = ReflectionTools.GetMethodInfo((Interval pair) => pair.Overlaps(new Interval())); + internal static Expression? Overlaps(this IntervalExpression? interval1, IntervalExpression? interval2) + { + if (interval1 == null) + return null; + + if (interval2 == null) + return null; + + if(interval1.PostgresRange != null) + { + return new SqlFunctionExpression(typeof(bool), null, "&&", new Expression[] { interval1.PostgresRange!, interval2.PostgresRange! }); + } + + var min1 = interval1.Min; + var max1 = interval1.Max; + var min2 = interval2.Min; + var max2 = interval2.Max; + + return Expression.And( + Expression.GreaterThan(max1, min2), + Expression.GreaterThan(max2, min1) + ); + } + + public static Expression And(this Expression expression, Expression? other) + { + if (other == null) + return expression; + + return Expression.And(expression, other); + } + } + internal class LikeExpression : DbExpression { public readonly Expression Expression; @@ -1193,22 +1339,25 @@ internal class DeleteExpression : CommandExpression public readonly SourceWithAliasExpression Source; public readonly Expression? Where; + public readonly bool ReturnRowCount; - public DeleteExpression(ITable table, bool useHistoryTable, SourceWithAliasExpression source, Expression? where) + public DeleteExpression(ITable table, bool useHistoryTable, SourceWithAliasExpression source, Expression? where, bool returnRowCount) : base(DbExpressionType.Delete) { this.Table = table; this.UseHistoryTable = useHistoryTable; this.Source = source; this.Where = where; + this.ReturnRowCount = returnRowCount; } public override string ToString() { - return "DELETE {0}\r\nFROM {1}\r\n{2}".FormatWith( + return "DELETE FROM {0}\r\nFROM {1}\r\n{2}".FormatWith( Table.Name, Source.ToString(), - Where?.Let(w => "WHERE " + w.ToString())); + Where?.Let(w => "WHERE " + w.ToString())) + + (ReturnRowCount ? "\r\nSELECT @@rowcount" : ""); } protected override Expression Accept(DbExpressionVisitor visitor) @@ -1226,8 +1375,9 @@ internal class UpdateExpression : CommandExpression public readonly ReadOnlyCollection Assigments; public readonly SourceWithAliasExpression Source; public readonly Expression Where; + public readonly bool ReturnRowCount; - public UpdateExpression(ITable table, bool useHistoryTable, SourceWithAliasExpression source, Expression where, IEnumerable assigments) + public UpdateExpression(ITable table, bool useHistoryTable, SourceWithAliasExpression source, Expression where, IEnumerable assigments, bool returnRowCount) : base(DbExpressionType.Update) { this.Table = table; @@ -1235,6 +1385,7 @@ public UpdateExpression(ITable table, bool useHistoryTable, SourceWithAliasExpre this.Assigments = assigments.ToReadOnly(); this.Source = source; this.Where = where; + this.ReturnRowCount = returnRowCount; } public override string ToString() @@ -1259,14 +1410,16 @@ internal class InsertSelectExpression : CommandExpression public ObjectName Name { get { return UseHistoryTable ? Table.SystemVersioned!.TableName : Table.Name; } } public readonly ReadOnlyCollection Assigments; public readonly SourceWithAliasExpression Source; + public readonly bool ReturnRowCount; - public InsertSelectExpression(ITable table, bool useHistoryTable, SourceWithAliasExpression source, IEnumerable assigments) + public InsertSelectExpression(ITable table, bool useHistoryTable, SourceWithAliasExpression source, IEnumerable assigments, bool returnRowCount) : base(DbExpressionType.InsertSelect) { this.Table = table; this.UseHistoryTable = useHistoryTable; this.Assigments = assigments.ToReadOnly(); this.Source = source; + this.ReturnRowCount = returnRowCount; } public override string ToString() @@ -1321,22 +1474,4 @@ protected override Expression Accept(DbExpressionVisitor visitor) return visitor.VisitCommandAggregate(this); } } - - internal class SelectRowCountExpression : CommandExpression - { - public SelectRowCountExpression() - : base(DbExpressionType.SelectRowCount) - { - } - - public override string ToString() - { - return "SELECT @@rowcount"; - } - - protected override Expression Accept(DbExpressionVisitor visitor) - { - return visitor.VisitSelectRowCount(this); - } - } } diff --git a/Signum.Engine/Linq/DbQueryProvider.cs b/Signum.Engine/Linq/DbQueryProvider.cs index 5f50200e3a..0ecd8fc6be 100644 --- a/Signum.Engine/Linq/DbQueryProvider.cs +++ b/Signum.Engine/Linq/DbQueryProvider.cs @@ -83,10 +83,15 @@ internal R Translate(Expression expression, Func continu internal static Expression Optimize(Expression binded, QueryBinder binder, AliasGenerator aliasGenerator, HeavyProfiler.Tracer? log) { + var isPostgres = Schema.Current.Settings.IsPostgres; + + log.Switch("Aggregate"); - Expression rewrited = AggregateRewriter.Rewrite(binded); + Expression rewriten = AggregateRewriter.Rewrite(binded); + log.Switch("DupHistory"); + Expression dupHistory = DuplicateHistory.Rewrite(rewriten, aliasGenerator); log.Switch("EntityCompleter"); - Expression completed = EntityCompleter.Complete(rewrited, binder); + Expression completed = EntityCompleter.Complete(dupHistory, binder); log.Switch("AliasReplacer"); Expression replaced = AliasProjectionReplacer.Replace(completed, aliasGenerator); log.Switch("OrderBy"); @@ -98,7 +103,7 @@ internal static Expression Optimize(Expression binded, QueryBinder binder, Alias log.Switch("Redundant"); Expression subqueryCleaned = RedundantSubqueryRemover.Remove(columnCleaned); log.Switch("Condition"); - Expression rewriteConditions = ConditionsRewriter.Rewrite(subqueryCleaned); + Expression rewriteConditions = isPostgres ? ConditionsRewriterPostgres.Rewrite(subqueryCleaned) : ConditionsRewriter.Rewrite(subqueryCleaned); log.Switch("Scalar"); Expression scalar = ScalarSubqueryRewriter.Rewrite(rewriteConditions); return scalar; diff --git a/Signum.Engine/Linq/ExpressionVisitor/AggregateFinder.cs b/Signum.Engine/Linq/ExpressionVisitor/AggregateFinder.cs index c07411711d..d63658dea8 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/AggregateFinder.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/AggregateFinder.cs @@ -1,4 +1,7 @@ -using System.Linq.Expressions; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq.Expressions; namespace Signum.Engine.Linq { @@ -7,21 +10,29 @@ namespace Signum.Engine.Linq /// internal class AggregateFinder : DbExpressionVisitor { - bool hasAggregates = false; + List? aggregates; private AggregateFinder() { } - public static bool HasAggregates(Expression source) + protected internal override Expression VisitAggregate(AggregateExpression aggregate) + { + if (aggregates == null) + aggregates = new List(); + + aggregates.Add(aggregate); + return base.VisitAggregate(aggregate); + } + + public static List? GetAggregates(ReadOnlyCollection columns) { AggregateFinder ap = new AggregateFinder(); - ap.Visit(source); - return ap.hasAggregates; + Visit(columns, ap.VisitColumnDeclaration); + return ap.aggregates; } - protected internal override Expression VisitAggregate(AggregateExpression aggregate) + protected internal override Expression VisitScalar(ScalarExpression scalar) { - hasAggregates = true; - return base.VisitAggregate(aggregate); + return scalar; } } } diff --git a/Signum.Engine/Linq/ExpressionVisitor/ChildProjectionFlattener.cs b/Signum.Engine/Linq/ExpressionVisitor/ChildProjectionFlattener.cs index 5da721b4f5..b0ddc9f843 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/ChildProjectionFlattener.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/ChildProjectionFlattener.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -130,7 +131,7 @@ protected internal override Expression VisitProjection(ProjectionExpression proj currentSource = old; - Expression key = TupleReflection.TupleChainConstructor(columnsSMExternal.Select(cd => cd.GetReference(aliasSM).Nullify())); + Expression key = TupleReflection.TupleChainConstructor(columnsSMExternal.Select(cd => MakeEquatable(cd.GetReference(aliasSM)))); Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(key.Type, projector.Type); ConstructorInfo ciKVP = kvpType.GetConstructor(new[] { key.Type, projector.Type })!; Type projType = proj.UniqueFunction == null ? typeof(IEnumerable<>).MakeGenericType(kvpType) : kvpType; @@ -139,11 +140,19 @@ protected internal override Expression VisitProjection(ProjectionExpression proj Expression.New(ciKVP, key, projector), proj.UniqueFunction, projType); return new ChildProjectionExpression(childProj, - TupleReflection.TupleChainConstructor(columns.Select(a => a.Nullify())), inMList != null, inMList ?? proj.Type, new LookupToken()); + TupleReflection.TupleChainConstructor(columns.Select(a => MakeEquatable(a))), inMList != null, inMList ?? proj.Type, new LookupToken()); } } } + public Expression MakeEquatable(Expression expression) + { + if (expression.Type.IsArray) + return Expression.New(typeof(ArrayBox<>).MakeGenericType(expression.Type.ElementType()!).GetConstructors().SingleEx(), expression); + + return expression.Nullify(); + } + private SelectExpression WithoutOrder(SelectExpression sel) { if (sel.Top != null || (sel.OrderBy.Count == 0)) @@ -329,4 +338,27 @@ protected internal override Expression VisitColumn(ColumnExpression column) } } + + class ArrayBox : IEquatable> + { + readonly int hashCode; + public readonly T[]? Array; + + public ArrayBox(T[]? array) + { + this.Array = array; + this.hashCode = 0; + if(array != null) + { + foreach (var item in array) + { + this.hashCode = (this.hashCode << 1) ^ (item == null ? 0 : item.GetHashCode()); + } + } + } + + public override int GetHashCode() => hashCode; + public override bool Equals(object? obj) => obj is ArrayBox a && Equals(a); + public bool Equals([AllowNull] ArrayBox other) => Enumerable.SequenceEqual(Array, other.Array); + } } diff --git a/Signum.Engine/Linq/ExpressionVisitor/ColumnProjector.cs b/Signum.Engine/Linq/ExpressionVisitor/ColumnProjector.cs index 14c18a23f3..81117d3084 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/ColumnProjector.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/ColumnProjector.cs @@ -129,7 +129,7 @@ public override Expression Visit(Expression expression) return mapped; } - mapped = request.AddIndependentColumn(column.Type, column.Name, implementation, column); + mapped = request.AddIndependentColumn(column.Type, column.Name!, implementation, column); this.map[column] = mapped; return mapped; } @@ -188,7 +188,7 @@ public string GetNextColumnName() public ColumnDeclaration MapColumn(ColumnExpression ce) { - string columnName = GetUniqueColumnName(ce.Name); + string columnName = GetUniqueColumnName(ce.Name!); var result = new ColumnDeclaration(columnName, ce); columns.Add(result.Name, result); return result; diff --git a/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriter.cs b/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriter.cs index 393558a883..a5ea242a17 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriter.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriter.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using Signum.Utilities.ExpressionTrees; using System.Data.SqlTypes; +using System.Linq; namespace Signum.Engine.Linq { @@ -159,7 +160,7 @@ protected internal override Expression VisitSqlCast(SqlCastExpression castExpr) { var expression = MakeSqlValue(Visit(castExpr.Expression)); if (expression != castExpr.Expression) - return new SqlCastExpression(castExpr.Type, expression, castExpr.SqlDbType); + return new SqlCastExpression(castExpr.Type, expression, castExpr.DbType); return castExpr; } @@ -282,7 +283,7 @@ protected internal override Expression VisitSqlTableValuedFunction(SqlTableValue { ReadOnlyCollection args = Visit(sqlFunction.Arguments, a => MakeSqlValue(Visit(a))); if (args != sqlFunction.Arguments) - return new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.Table, sqlFunction.Alias, args); + return new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.ViewTable, sqlFunction.SingleColumnType, sqlFunction.Alias, args); return sqlFunction; } @@ -307,9 +308,9 @@ protected internal override When VisitWhen(When when) protected internal override Expression VisitAggregate(AggregateExpression aggregate) { - Expression source = MakeSqlValue(Visit(aggregate.Expression)); - if (source != aggregate.Expression) - return new AggregateExpression(aggregate.Type, source, aggregate.AggregateFunction, aggregate.Distinct); + var arguments = Visit(aggregate.Arguments).Select(a => MakeSqlValue(a)).ToReadOnly(); + if (arguments != aggregate.Arguments) + return new AggregateExpression(aggregate.Type, aggregate.AggregateFunction, arguments); return aggregate; } @@ -374,7 +375,7 @@ protected internal override Expression VisitUpdate(UpdateExpression update) return c; }); if (source != update.Source || where != update.Where || assigments != update.Assigments) - return new UpdateExpression(update.Table, update.UseHistoryTable, (SelectExpression)source, where, assigments); + return new UpdateExpression(update.Table, update.UseHistoryTable, (SelectExpression)source, where, assigments, update.ReturnRowCount); return update; } diff --git a/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriterPostgres.cs b/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriterPostgres.cs new file mode 100644 index 0000000000..1c9ad09a0b --- /dev/null +++ b/Signum.Engine/Linq/ExpressionVisitor/ConditionsRewriterPostgres.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using Signum.Utilities; +using System.Collections.ObjectModel; +using Signum.Utilities.ExpressionTrees; +using System.Data.SqlTypes; + +namespace Signum.Engine.Linq +{ + internal class ConditionsRewriterPostgres: DbExpressionVisitor + { + public static Expression Rewrite(Expression expression) + { + return new ConditionsRewriterPostgres().Visit(expression); + } + + protected internal override Expression VisitSqlCast(SqlCastExpression castExpr) + { + var expression = Visit(castExpr.Expression); + + if(expression.Type.UnNullify() == typeof(bool) && castExpr.Type.UnNullify() != typeof(int)) + return new SqlCastExpression(castExpr.Type, new SqlCastExpression(typeof(int), expression), castExpr.DbType); + + if (expression != castExpr.Expression) + return new SqlCastExpression(castExpr.Type, expression, castExpr.DbType); + + return castExpr; + } + } +} diff --git a/Signum.Engine/Linq/ExpressionVisitor/DbExpressionComparer.cs b/Signum.Engine/Linq/ExpressionVisitor/DbExpressionComparer.cs index 2c3c802fd4..c0d51c191f 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/DbExpressionComparer.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/DbExpressionComparer.cs @@ -114,8 +114,6 @@ private bool ComparePrivate(Expression? a, Expression? b) return CompareInsertSelect((InsertSelectExpression)a, (InsertSelectExpression)b); case DbExpressionType.CommandAggregate: return CompareCommandAggregate((CommandAggregateExpression)a, (CommandAggregateExpression)b); - case DbExpressionType.SelectRowCount: - return CompareSelectRowCount((SelectRowCountExpression)a, (SelectRowCountExpression)b); case DbExpressionType.Entity: return CompareEntityInit((EntityExpression)a, (EntityExpression)b); case DbExpressionType.EmbeddedInit: @@ -305,7 +303,7 @@ private bool CompareChildProjection(ChildProjectionExpression a, ChildProjection protected virtual bool CompareAggregate(AggregateExpression a, AggregateExpression b) { - return a.AggregateFunction == b.AggregateFunction && Compare(a.Expression, b.Expression); + return a.AggregateFunction == b.AggregateFunction && CompareList(a.Arguments, b.Arguments, Compare); } protected virtual bool CompareAggregateSubquery(AggregateRequestsExpression a, AggregateRequestsExpression b) @@ -316,7 +314,7 @@ protected virtual bool CompareAggregateSubquery(AggregateRequestsExpression a, A protected virtual bool CompareSqlCast(SqlCastExpression a, SqlCastExpression b) { - return a.SqlDbType == b.SqlDbType + return a.DbType.Equals(b.DbType) && Compare(a.Expression, b.Expression); } @@ -330,7 +328,7 @@ protected virtual bool CompareSqlFunction(SqlFunctionExpression a, SqlFunctionEx private bool CompareTableValuedSqlFunction(SqlTableValuedFunctionExpression a, SqlTableValuedFunctionExpression b) { - return a.Table == b.Table + return a.ViewTable == b.ViewTable && CompareAlias(a.Alias, b.Alias) && CompareList(a.Arguments, b.Arguments, Compare); } @@ -423,7 +421,8 @@ protected virtual bool CompareDelete(DeleteExpression a, DeleteExpression b) return a.Table == b.Table && a.UseHistoryTable == b.UseHistoryTable && Compare(a.Source, b.Source) - && Compare(a.Where, b.Where); + && Compare(a.Where, b.Where) + && a.ReturnRowCount == b.ReturnRowCount; } protected virtual bool CompareUpdate(UpdateExpression a, UpdateExpression b) @@ -432,7 +431,8 @@ protected virtual bool CompareUpdate(UpdateExpression a, UpdateExpression b) && a.UseHistoryTable == b.UseHistoryTable && CompareList(a.Assigments, b.Assigments, CompareAssigment) && Compare(a.Source, b.Source) - && Compare(a.Where, b.Where); + && Compare(a.Where, b.Where) + && a.ReturnRowCount == b.ReturnRowCount; } protected virtual bool CompareInsertSelect(InsertSelectExpression a, InsertSelectExpression b) @@ -440,7 +440,8 @@ protected virtual bool CompareInsertSelect(InsertSelectExpression a, InsertSelec return a.Table == b.Table && a.UseHistoryTable == b.UseHistoryTable && CompareList(a.Assigments, b.Assigments, CompareAssigment) - && Compare(a.Source, b.Source); + && Compare(a.Source, b.Source) + && a.ReturnRowCount == b.ReturnRowCount; } protected virtual bool CompareAssigment(ColumnAssignment a, ColumnAssignment b) @@ -453,11 +454,6 @@ protected virtual bool CompareCommandAggregate(CommandAggregateExpression a, Com return CompareList(a.Commands, b.Commands, Compare); } - protected virtual bool CompareSelectRowCount(SelectRowCountExpression a, SelectRowCountExpression b) - { - return true; - } - protected virtual bool CompareEntityInit(EntityExpression a, EntityExpression b) { return a.Table == b.Table diff --git a/Signum.Engine/Linq/ExpressionVisitor/DbExpressionNominator.cs b/Signum.Engine/Linq/ExpressionVisitor/DbExpressionNominator.cs index ece5e0b70b..ae525e57f3 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/DbExpressionNominator.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/DbExpressionNominator.cs @@ -1,4 +1,5 @@ using Microsoft.SqlServer.Server; +using NpgsqlTypes; using Signum.Engine.Maps; using Signum.Entities; using Signum.Utilities; @@ -53,7 +54,12 @@ bool Has(Expression expression) return this.candidates.Contains(expression); } - private DbExpressionNominator() { } + + bool isPostgres; + private DbExpressionNominator() + { + this.isPostgres = Schema.Current.Settings.IsPostgres; + } static internal HashSet Nominate(Expression? expression, out Expression newExpression, bool isGroupKey = false) { @@ -105,7 +111,6 @@ private bool IsExcluded(Expression exp) case DbExpressionType.Update: case DbExpressionType.Delete: case DbExpressionType.CommandAggregate: - case DbExpressionType.SelectRowCount: return true; } return false; @@ -253,7 +258,9 @@ protected override Expression VisitConstant(ConstantExpression c) { if (ut == typeof(DayOfWeek)) { - var dayNumber = c.Value == null ? (int?)null : ToDayOfWeekExpression.ToSqlWeekDay((DayOfWeek)c.Value, ToDayOfWeekExpression.DateFirst.Value.Item1); + var dayNumber = c.Value == null ? (int?)null : + isPostgres ? (int)(DayOfWeek)c.Value : + ToDayOfWeekExpression.ToSqlWeekDay((DayOfWeek)c.Value, ((SqlConnector)Connector.Current).DateFirst); return new ToDayOfWeekExpression(Add(Expression.Constant(dayNumber, typeof(int?)))); } @@ -287,7 +294,7 @@ protected internal override Expression VisitSqlTableValuedFunction(SqlTableValue { ReadOnlyCollection args = Visit(sqlFunction.Arguments, a => Visit(a)!); if (args != sqlFunction.Arguments) - sqlFunction = new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.Table, sqlFunction.Alias, args); ; + sqlFunction = new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.ViewTable, sqlFunction.SingleColumnType, sqlFunction.Alias, args); ; if (args.All(Has)) return Add(sqlFunction); @@ -299,7 +306,7 @@ protected internal override Expression VisitSqlCast(SqlCastExpression castExpr) { var expression = Visit(castExpr.Expression); if (expression != castExpr.Expression) - castExpr = new SqlCastExpression(castExpr.Type, expression!, castExpr.SqlDbType); + castExpr = new SqlCastExpression(castExpr.Type, expression!, castExpr.DbType); return Add(castExpr); } @@ -385,9 +392,14 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo new SqlConstantExpression(culture.Name) })); } + protected Expression? TrySqlFunction(Expression? obj, PostgresFunction postgresFunction, Type type, params Expression[] expression) + { + return TrySqlFunction(obj, postgresFunction.ToString(), type, expression); + } + protected Expression? TrySqlFunction(Expression? obj, SqlFunction sqlFunction, Type type, params Expression[] expression) { - return TrySqlFunction(obj, sqlFunction.ToString(), type, expression); + return TrySqlFunction(obj, isPostgres? sqlFunction.ToString().ToLower() : sqlFunction.ToString(), type, expression); } protected Expression? TrySqlFunction(Expression? obj, string sqlFunction, Type type, params Expression[] expression) @@ -413,10 +425,10 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo return null; } - return Add(new SqlFunctionExpression(type, newObj, sqlFunction.ToString(), newExpressions)); + return Add(new SqlFunctionExpression(type, newObj, sqlFunction, newExpressions)); } - private SqlFunctionExpression? TrySqlDifference(SqlEnums sqlEnums, Type type, Expression expression) + private Expression? TrySqlDifference(SqlEnums sqlEnums, Type type, Expression expression) { if (innerProjection) return null; @@ -432,7 +444,7 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo return null; } - private SqlFunctionExpression? TrySqlDifference(SqlEnums sqlEnums, Type type, Expression leftSide, Expression rightSide) + private Expression? TrySqlDifference(SqlEnums sqlEnums, Type type, Expression leftSide, Expression rightSide) { Expression left = Visit(leftSide); if (!Has(left.RemoveNullify())) @@ -442,10 +454,40 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo if (!Has(right.RemoveNullify())) return null; - SqlFunctionExpression result = new SqlFunctionExpression(type, null, SqlFunction.DATEDIFF.ToString(), new Expression[]{ - new SqlEnumExpression(sqlEnums), right, left}); + if (isPostgres) + { + var secondsDouble = new SqlFunctionExpression(typeof(double), null, PostgresFunction.EXTRACT.ToString(), new Expression[] + { + new SqlLiteralExpression(SqlEnums.epoch), + Expression.Subtract(left, right), + }); - return Add(result); + if (sqlEnums == SqlEnums.second) + return Add(secondsDouble); + + + if (sqlEnums == SqlEnums.millisecond) + return Add(Expression.Multiply(secondsDouble, new SqlConstantExpression(1000.0))); + + double scale = sqlEnums switch + { + SqlEnums.minute => 60, + SqlEnums.hour => 60 * 60, + SqlEnums.day => 60 * 60 * 24, + _ => throw new UnexpectedValueException(sqlEnums), + }; + + return Add(Expression.Divide(secondsDouble, new SqlConstantExpression(scale))); + } + else + { + return Add(new SqlFunctionExpression(type, null, SqlFunction.DATEDIFF.ToString(), new Expression[] + { + new SqlLiteralExpression(sqlEnums), + right, + left + })); + } } private Expression? TrySqlDate(Expression expression) @@ -454,18 +496,27 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo if (innerProjection || !Has(expr)) return null; - if (Connector.Current.AllowsConvertToDate) - return Add(new SqlFunctionExpression(typeof(DateTime), null, SqlFunction.CONVERT.ToString(), new[] + if (isPostgres) + { + return Add(new SqlCastExpression(typeof(DateTime), expr, new AbstractDbType(NpgsqlDbType.Date))); + } + else + { + if (Connector.Current.AllowsConvertToDate) { - new SqlConstantExpression(SqlDbType.Date), - expr, - new SqlConstantExpression(101) - })); + return Add(new SqlFunctionExpression(typeof(DateTime), null, SqlFunction.CONVERT.ToString(), new[] + { + new SqlConstantExpression(SqlDbType.Date.ToString()), + expr, + new SqlConstantExpression(101) + })); + } - return Add(new SqlCastExpression(typeof(DateTime), - new SqlFunctionExpression(typeof(double), null, SqlFunction.FLOOR.ToString(), - new[] { new SqlCastExpression(typeof(double), expr) } - ))); + return Add(new SqlCastExpression(typeof(DateTime), + new SqlFunctionExpression(typeof(double), null, SqlFunction.FLOOR.ToString(), + new[] { new SqlCastExpression(typeof(double), expr) } + ))); + } } @@ -475,10 +526,13 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo if (innerProjection || !Has(expr)) return null; + if (isPostgres) + return Add(new SqlCastExpression(typeof(TimeSpan), expression)); + if (Connector.Current.AllowsConvertToTime) return Add(new SqlFunctionExpression(typeof(TimeSpan), null, SqlFunction.CONVERT.ToString(), new[] { - new SqlConstantExpression(SqlDbType.Time), + new SqlConstantExpression(isPostgres ? NpgsqlDbType.Time.ToString() : SqlDbType.Time.ToString()), expr, })); @@ -491,42 +545,49 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo if (innerProjection || !Has(expr)) return null; - var number = TrySqlFunction(null, SqlFunction.DATEPART, typeof(int?), new SqlEnumExpression(SqlEnums.weekday), expr)!; + var number = TrySqlFunction(null, getDatePart(), typeof(int?), new SqlLiteralExpression(isPostgres ? SqlEnums.dow : SqlEnums.weekday), expr)!; Add(number); return new ToDayOfWeekExpression(number).TryConvert(typeof(DayOfWeek)); } - + private Expression? TrySqlStartOf(Expression expression, SqlEnums part) { Expression expr = Visit(expression); if (innerProjection || !Has(expr)) return null; - Expression result = - TrySqlFunction(null, SqlFunction.DATEADD, expr.Type, new SqlEnumExpression(part), - TrySqlFunction(null, SqlFunction.DATEDIFF, typeof(int), new SqlEnumExpression(part), new SqlConstantExpression(0), expr)!, - new SqlConstantExpression(0))!; - - return Add(result); - } - - private Expression? TrySqlSecondsStart(Expression expression) - { - Expression expr = Visit(expression); - if (innerProjection || !Has(expr)) - return null; + if (isPostgres) + { + Expression? result = + TrySqlFunction(null, PostgresFunction.date_trunc, expr.Type, + new SqlConstantExpression(part.ToString()), expr); + return Add(result); + } + else + { + if (part == SqlEnums.second) + { + Expression result = + TrySqlFunction(null, SqlFunction.DATEADD, expr.Type, new SqlLiteralExpression(SqlEnums.millisecond), + Expression.Negate(TrySqlFunction(null, SqlFunction.DATEPART, typeof(int), new SqlLiteralExpression(SqlEnums.millisecond), expr)), expr)!; - Expression result = - TrySqlFunction(null, SqlFunction.DATEADD, expr.Type, new SqlEnumExpression(SqlEnums.millisecond), - Expression.Negate(TrySqlFunction(null, SqlFunction.DATEPART, typeof(int), new SqlEnumExpression(SqlEnums.millisecond), expr)), expr)!; + return Add(result); + } + else + { + Expression result = + TrySqlFunction(null, SqlFunction.DATEADD, expr.Type, new SqlLiteralExpression(part), + TrySqlFunction(null, SqlFunction.DATEDIFF, typeof(int), new SqlLiteralExpression(part), new SqlConstantExpression(0), expr)!, + new SqlConstantExpression(0))!; - return Add(result); + return Add(result); + } + } } - private Expression? TryAddSubtractDateTimeTimeSpan(Expression date, Expression time, bool add) { Expression exprDate = Visit(date); @@ -534,8 +595,8 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo if (innerProjection || !Has(exprDate) || !Has(exprTime)) return null; - var castDate = new SqlCastExpression(typeof(DateTime), exprDate, SqlDbType.DateTime); //Just in case is a Date - var castTime = new SqlCastExpression(typeof(TimeSpan), exprTime, SqlDbType.DateTime); //Just in case is a Date + var castDate = new SqlCastExpression(typeof(DateTime), exprDate, new AbstractDbType(SqlDbType.DateTime, NpgsqlDbType.Timestamp)); //Just in case is a Date + var castTime = new SqlCastExpression(typeof(TimeSpan), exprTime, new AbstractDbType(SqlDbType.Time, NpgsqlDbType.Time)); //Just in case is a Date var result = add ? Expression.Add(castDate, castTime) : Expression.Subtract(castDate, castTime); @@ -543,22 +604,43 @@ protected Expression GetFormatToString(MethodCallExpression m, string? defaultFo return Add(result); } - private Expression? TryDatePartTo(SqlEnumExpression datePart, Expression start, Expression end) + private Expression? TryDatePartTo(SqlEnums unit, Expression start, Expression end) { Expression exprStart = Visit(start); Expression exprEnd = Visit(end); if (innerProjection || !Has(exprStart) || !Has(exprEnd)) return null; - var diff = new SqlFunctionExpression(typeof(int), null, SqlFunction.DATEDIFF.ToString(), - new[] { datePart, exprStart, exprEnd }); + if (isPostgres) + { + var age = new SqlFunctionExpression(typeof(DateSpan), null, PostgresFunction.age.ToString(), new[] { exprStart, exprEnd }); - var add = new SqlFunctionExpression(typeof(DateTime), null, SqlFunction.DATEADD.ToString(), - new[] { datePart, diff, exprStart }); + SqlFunctionExpression Extract( SqlEnums part, Expression period) + { + return new SqlFunctionExpression(typeof(int), null, PostgresFunction.EXTRACT.ToString(), new[] { new SqlLiteralExpression(part), period }); + } - return Add(new CaseExpression(new[]{ + if (unit == SqlEnums.month) + return Add(Expression.Add(Extract(SqlEnums.year, age), Expression.Multiply(Extract(SqlEnums.month, age), new SqlConstantExpression(12, typeof(int))))); + else if (unit == SqlEnums.year) + return Add(Extract(SqlEnums.year, age)); + else + throw new UnexpectedValueException(unit); + } + else + { + SqlLiteralExpression datePart = new SqlLiteralExpression(unit); + + var diff = new SqlFunctionExpression(typeof(int), null, SqlFunction.DATEDIFF.ToString(), + new[] { datePart, exprStart, exprEnd }); + + var add = new SqlFunctionExpression(typeof(DateTime), null, SqlFunction.DATEADD.ToString(), + new[] { datePart, diff, exprStart }); + + return Add(new CaseExpression(new[]{ new When(Expression.GreaterThan(add, exprEnd), Expression.Subtract(diff, Expression.Constant(1)))}, - diff)); + diff)); + } } @@ -783,7 +865,7 @@ private Expression NullToStringEmpty(Expression exp) return exp; } - return new SqlFunctionExpression(typeof(string), null, SqlFunction.ISNULL.ToString(), new[] { exp, new SqlConstantExpression("") }); + return new SqlFunctionExpression(typeof(string), null, SqlFunction.COALESCE.ToString(), new[] { exp, new SqlConstantExpression("") }); } private static bool AlwaysHasValue(Expression exp) @@ -1075,7 +1157,7 @@ protected internal override Expression VisitIsNull(IsNullExpression isNull) return isNull; } - protected internal override Expression VisitSqlEnum(SqlEnumExpression sqlEnum) + protected internal override Expression VisitSqlLiteral(SqlLiteralExpression sqlEnum) { if (!innerProjection) return Add(sqlEnum); @@ -1124,7 +1206,9 @@ protected internal override Expression VisitLike(LikeExpression like) if (Has(newSubExpression) && Has(newExpression)) { - SqlFunctionExpression result = new SqlFunctionExpression(typeof(int), null, SqlFunction.CHARINDEX.ToString(), new[] { newExpression, newSubExpression }); + SqlFunctionExpression result = isPostgres ? + new SqlFunctionExpression(typeof(int), null, PostgresFunction.strpos.ToString(), new[] { newExpression, newSubExpression }): + new SqlFunctionExpression(typeof(int), null, SqlFunction.CHARINDEX.ToString(), new[] { newSubExpression, newExpression }); Add(result); @@ -1158,20 +1242,27 @@ protected override Expression VisitMember(MemberExpression m) return base.VisitMember(m); } + string getDatePart() + { + return isPostgres ? PostgresFunction.EXTRACT.ToString() : SqlFunction.DATEPART.ToString(); + } + public Expression? HardCodedMembers(MemberExpression m) { + + switch (m.Member.DeclaringType!.TypeName() + "." + m.Member.Name) { - case "string.Length": return TrySqlFunction(null, SqlFunction.LEN, m.Type, m.Expression); + case "string.Length": return TrySqlFunction(null, isPostgres ? PostgresFunction.length.ToString() : SqlFunction.LEN.ToString(), m.Type, m.Expression); case "Math.PI": return TrySqlFunction(null, SqlFunction.PI, m.Type); - case "DateTime.Year": return TrySqlFunction(null, SqlFunction.YEAR, m.Type, m.Expression); - case "DateTime.Month": return TrySqlFunction(null, SqlFunction.MONTH, m.Type, m.Expression); - case "DateTime.Day": return TrySqlFunction(null, SqlFunction.DAY, m.Type, m.Expression); - case "DateTime.DayOfYear": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.dayofyear), m.Expression); - case "DateTime.Hour": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.hour), m.Expression); - case "DateTime.Minute": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.minute), m.Expression); - case "DateTime.Second": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.second), m.Expression); - case "DateTime.Millisecond": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.millisecond), m.Expression); + case "DateTime.Year": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.year), m.Expression); + case "DateTime.Month": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.month), m.Expression); + case "DateTime.Day": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.day), m.Expression); + case "DateTime.DayOfYear": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(isPostgres? SqlEnums.doy: SqlEnums.dayofyear), m.Expression); + case "DateTime.Hour": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.hour), m.Expression); + case "DateTime.Minute": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.minute), m.Expression); + case "DateTime.Second": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.second), m.Expression); + case "DateTime.Millisecond": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.millisecond), m.Expression); case "DateTime.Date": return TrySqlDate(m.Expression); case "DateTime.TimeOfDay": return TrySqlTime(m.Expression); case "DateTime.DayOfWeek": return TrySqlDayOftheWeek(m.Expression); @@ -1184,11 +1275,11 @@ protected override Expression VisitMember(MemberExpression m) return Add(new SqlCastExpression(typeof(int?), TrySqlFunction(null, SqlFunction.FLOOR, typeof(double?), diff)!)); } - case "TimeSpan.Hours": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.hour), m.Expression); - case "TimeSpan.Minutes": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.minute), m.Expression); - case "TimeSpan.Seconds": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.second), m.Expression); - case "TimeSpan.Milliseconds": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.millisecond), m.Expression); - + case "TimeSpan.Hours": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.hour), m.Expression); + case "TimeSpan.Minutes": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.minute), m.Expression); + case "TimeSpan.Seconds": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.second), m.Expression); + case "TimeSpan.Milliseconds": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.millisecond), m.Expression); + case "TimeSpan.TotalDays": return TrySqlDifference(SqlEnums.day, m.Type, m.Expression); case "TimeSpan.TotalHours": return TrySqlDifference(SqlEnums.hour, m.Type, m.Expression); case "TimeSpan.TotalMilliseconds": return TrySqlDifference(SqlEnums.millisecond, m.Type, m.Expression); @@ -1220,12 +1311,18 @@ protected override Expression VisitMember(MemberExpression m) SqlMethodAttribute? sma = m.Method.GetCustomAttribute(); if (sma != null) { - if (m.Method.IsExtensionMethod()) - using (ForceFullNominate()) - return TrySqlFunction(m.Arguments[0], m.Method.Name, m.Type, m.Arguments.Skip(1).ToArray()); - using (ForceFullNominate()) - return TrySqlFunction(m.Object, sma.Name ?? m.Method.Name, m.Type, m.Arguments.ToArray()); + { + if (m.Method.IsExtensionMethod()) + using (ForceFullNominate()) + return TrySqlFunction(m.Arguments[0], m.Method.Name, m.Type, m.Arguments.Skip(1).ToArray()); + + if (m.Object != null) + using (ForceFullNominate()) + return TrySqlFunction(m.Object, sma.Name ?? m.Method.Name, m.Type, m.Arguments.ToArray()); + + return TrySqlFunction(m.Object, ObjectName.Parse(sma.Name ?? m.Method.Name, isPostgres).ToString(), m.Type, m.Arguments.ToArray()); + } } return base.VisitMethodCall(m); @@ -1236,6 +1333,25 @@ protected override Expression VisitMember(MemberExpression m) return Connector.Current.SupportsFormat ? GetFormatToString(m, defaultFormat) : TrySqlToString(m); } + private Expression? TryDateAdd(Type returnType, Expression date, Expression value, SqlEnums unit) + { + if (this.isPostgres) + { + Expression d = Visit(date); + if (!Has(d)) + return null; + + Expression v = Visit(value); + if (!Has(v)) + return null; + + return Add(Expression.Add(date, Expression.Multiply(value, new SqlLiteralExpression(typeof(TimeSpan), $"INTERVAL '1 {unit}'")))); + } + + + return TrySqlFunction(null, SqlFunction.DATEADD, returnType, new SqlLiteralExpression(unit), value, date); + } + private Expression? HardCodedMethods(MethodCallExpression m) { if (m.Method.Name == "ToString" && m.Method.DeclaringType != typeof(EnumerableExtensions)) @@ -1267,9 +1383,15 @@ protected override Expression VisitMember(MemberExpression m) { Expression? startIndex = m.TryGetArgument("startIndex")?.Let(e => Expression.Add(e, new SqlConstantExpression(1))); - Expression? charIndex = startIndex != null ? + Expression? charIndex = isPostgres ? + (startIndex != null ? + throw new NotImplementedException() : + TrySqlFunction(null, PostgresFunction.strpos, m.Type, m.Object, m.GetArgument("value"))) + : + (startIndex != null ? TrySqlFunction(null, SqlFunction.CHARINDEX, m.Type, m.GetArgument("value"), m.Object, startIndex) : - TrySqlFunction(null, SqlFunction.CHARINDEX, m.Type, m.GetArgument("value"), m.Object); + TrySqlFunction(null, SqlFunction.CHARINDEX, m.Type, m.GetArgument("value"), m.Object)); + if (charIndex == null) return null; Expression result = Expression.Subtract(charIndex, new SqlConstantExpression(1)); @@ -1291,15 +1413,22 @@ protected override Expression VisitMember(MemberExpression m) case "string.Replace": return TrySqlFunction(null, SqlFunction.REPLACE, m.Type, m.Object, m.GetArgument("oldValue"), m.GetArgument("newValue")); case "string.Substring": - return TrySqlFunction(null, SqlFunction.SUBSTRING, m.Type, m.Object, Expression.Add(m.GetArgument("startIndex"), new SqlConstantExpression(1)), m.TryGetArgument("length") ?? new SqlConstantExpression(int.MaxValue)); + var start = Expression.Add(m.GetArgument("startIndex"), new SqlConstantExpression(1)); + var length = m.TryGetArgument("length"); + if(isPostgres) + return length == null ? + TrySqlFunction(null, PostgresFunction.substr, m.Type, m.Object, start) : + TrySqlFunction(null, PostgresFunction.substr, m.Type, m.Object, start, length); + else + return TrySqlFunction(null, SqlFunction.SUBSTRING, m.Type, m.Object, start, length ?? new SqlConstantExpression(int.MaxValue)); case "string.Contains": - return TryCharIndex(m.GetArgument("value"), m.Object, index => Expression.GreaterThanOrEqual(index, new SqlConstantExpression(1))); + return TryCharIndex(m.Object, m.GetArgument("value"), index => Expression.GreaterThanOrEqual(index, new SqlConstantExpression(1))); case "string.StartsWith": - return TryCharIndex(m.GetArgument("value"), m.Object, index => Expression.Equal(index, new SqlConstantExpression(1))); + return TryCharIndex(m.Object, m.GetArgument("value"), index => Expression.Equal(index, new SqlConstantExpression(1))); case "string.EndsWith": return TryCharIndex( - TrySqlFunction(null, SqlFunction.REVERSE, m.Type, m.GetArgument("value"))!, TrySqlFunction(null, SqlFunction.REVERSE, m.Type, m.Object)!, + TrySqlFunction(null, SqlFunction.REVERSE, m.Type, m.GetArgument("value"))!, index => Expression.Equal(index, new SqlConstantExpression(1))); case "string.Format": case "StringExtensions.FormatWith": @@ -1309,7 +1438,7 @@ protected override Expression VisitMember(MemberExpression m) case "StringExtensions.End": return TrySqlFunction(null, SqlFunction.RIGHT, m.Type, m.GetArgument("str"), m.GetArgument("numChars")); case "StringExtensions.Replicate": - return TrySqlFunction(null, SqlFunction.REPLICATE, m.Type, m.GetArgument("str"), m.GetArgument("times")); + return TrySqlFunction(null, isPostgres ? PostgresFunction.repeat.ToString() : SqlFunction.REPLICATE.ToString(), m.Type, m.GetArgument("str"), m.GetArgument("times")); ; case "StringExtensions.Reverse": return TrySqlFunction(null, SqlFunction.REVERSE, m.Type, m.GetArgument("str")); case "StringExtensions.Like": @@ -1328,13 +1457,13 @@ protected override Expression VisitMember(MemberExpression m) return TryAddSubtractDateTimeTimeSpan(m.Object, val, m.Method.Name == "Add"); } - case "DateTime.AddDays": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.day), m.GetArgument("value"), m.Object); - case "DateTime.AddHours": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.hour), m.GetArgument("value"), m.Object); - case "DateTime.AddMilliseconds": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.millisecond), m.GetArgument("value"), m.Object); - case "DateTime.AddMinutes": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.minute), m.GetArgument("value"), m.Object); - case "DateTime.AddMonths": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.month), m.GetArgument("months"), m.Object); - case "DateTime.AddSeconds": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.second), m.GetArgument("value"), m.Object); - case "DateTime.AddYears": return TrySqlFunction(null, SqlFunction.DATEADD, m.Type, new SqlEnumExpression(SqlEnums.year), m.GetArgument("value"), m.Object); + case "DateTime.AddYears": return TryDateAdd(m.Type, m.Object, m.GetArgument("value"), SqlEnums.year); + case "DateTime.AddMonths": return TryDateAdd(m.Type, m.Object, m.GetArgument("months"), SqlEnums.month); + case "DateTime.AddDays": return TryDateAdd(m.Type, m.Object, m.GetArgument("value"), SqlEnums.day); + case "DateTime.AddHours": return TryDateAdd(m.Type, m.Object, m.GetArgument("value"), SqlEnums.hour); + case "DateTime.AddMinutes": return TryDateAdd(m.Type, m.Object, m.GetArgument("value"), SqlEnums.minute); + case "DateTime.AddSeconds": return TryDateAdd(m.Type, m.Object, m.GetArgument("value"), SqlEnums.second); + case "DateTime.AddMilliseconds": return TryDateAdd(m.Type, m.Object, m.GetArgument("value"), SqlEnums.millisecond); case "DateTime.ToShortDateString": return GetDateTimeToStringSqlFunction(m, "d"); case "DateTime.ToShortTimeString": return GetDateTimeToStringSqlFunction(m, "t"); case "DateTime.ToLongDateString": return GetDateTimeToStringSqlFunction(m, "D"); @@ -1346,12 +1475,12 @@ protected override Expression VisitMember(MemberExpression m) case "DateTimeExtensions.WeekStart": return TrySqlStartOf(m.GetArgument("dateTime"), SqlEnums.week); case "DateTimeExtensions.HourStart": return TrySqlStartOf(m.GetArgument("dateTime"), SqlEnums.hour); case "DateTimeExtensions.MinuteStart": return TrySqlStartOf(m.GetArgument("dateTime"), SqlEnums.minute); - case "DateTimeExtensions.SecondStart": return TrySqlSecondsStart(m.GetArgument("dateTime")); - case "DateTimeExtensions.YearsTo": return TryDatePartTo(new SqlEnumExpression(SqlEnums.year), m.GetArgument("start"), m.GetArgument("end")); - case "DateTimeExtensions.MonthsTo": return TryDatePartTo(new SqlEnumExpression(SqlEnums.month), m.GetArgument("start"), m.GetArgument("end")); + case "DateTimeExtensions.SecondStart": return TrySqlStartOf(m.GetArgument("dateTime"), SqlEnums.second); + case "DateTimeExtensions.YearsTo": return TryDatePartTo(SqlEnums.year, m.GetArgument("start"), m.GetArgument("end")); + case "DateTimeExtensions.MonthsTo": return TryDatePartTo(SqlEnums.month, m.GetArgument("start"), m.GetArgument("end")); - case "DateTimeExtensions.Quarter": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.quarter), m.Arguments.Single()); - case "DateTimeExtensions.WeekNumber": return TrySqlFunction(null, SqlFunction.DATEPART, m.Type, new SqlEnumExpression(SqlEnums.week), m.Arguments.Single()); + case "DateTimeExtensions.Quarter": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.quarter), m.Arguments.Single()); + case "DateTimeExtensions.WeekNumber": return TrySqlFunction(null, getDatePart(), m.Type, new SqlLiteralExpression(SqlEnums.week), m.Arguments.Single()); case "Math.Sign": return TrySqlFunction(null, SqlFunction.SIGN, m.Type, m.GetArgument("value")); case "Math.Abs": return TrySqlFunction(null, SqlFunction.ABS, m.Type, m.GetArgument("value")); @@ -1370,10 +1499,19 @@ protected override Expression VisitMember(MemberExpression m) case "Math.Log": return m.Arguments.Count != 1 ? null : TrySqlFunction(null, SqlFunction.LOG, m.Type, m.GetArgument("d")); case "Math.Ceiling": return TrySqlFunction(null, SqlFunction.CEILING, m.Type, m.TryGetArgument("d") ?? m.GetArgument("a")); case "Math.Round": - return TrySqlFunction(null, SqlFunction.ROUND, m.Type, - m.TryGetArgument("a") ?? m.TryGetArgument("d") ?? m.GetArgument("value"), - m.TryGetArgument("decimals") ?? m.TryGetArgument("digits") ?? new SqlConstantExpression(0)); - case "Math.Truncate": return TrySqlFunction(null, SqlFunction.ROUND, m.Type, m.GetArgument("d"), new SqlConstantExpression(0), new SqlConstantExpression(1)); + + var value = m.TryGetArgument("a") ?? m.TryGetArgument("d") ?? m.GetArgument("value"); + var digits = m.TryGetArgument("decimals") ?? m.TryGetArgument("digits"); + if (digits == null) + return TrySqlFunction(null, SqlFunction.ROUND, m.Type, value); + else + return TrySqlFunction(null, SqlFunction.ROUND, m.Type, value, digits); + + case "Math.Truncate": + if(isPostgres) + return TrySqlFunction(null, PostgresFunction.trunc, m.Type, m.GetArgument("d")); + + return TrySqlFunction(null, SqlFunction.ROUND, m.Type, m.GetArgument("d"), new SqlConstantExpression(0), new SqlConstantExpression(1)); case "Math.Max": case "Math.Min": return null; /* could be translates to something like 'case when a > b then a * when a < b then b diff --git a/Signum.Engine/Linq/ExpressionVisitor/DbExpressionVisitor.cs b/Signum.Engine/Linq/ExpressionVisitor/DbExpressionVisitor.cs index 9a395c191e..0f91c8cecb 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/DbExpressionVisitor.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/DbExpressionVisitor.cs @@ -32,7 +32,7 @@ protected internal virtual Expression VisitDelete(DeleteExpression delete) var source = VisitSource(delete.Source); var where = Visit(delete.Where); if (source != delete.Source || where != delete.Where) - return new DeleteExpression(delete.Table, delete.UseHistoryTable, (SourceWithAliasExpression)source, where); + return new DeleteExpression(delete.Table, delete.UseHistoryTable, (SourceWithAliasExpression)source, where, delete.ReturnRowCount); return delete; } @@ -42,7 +42,7 @@ protected internal virtual Expression VisitUpdate(UpdateExpression update) var where = Visit(update.Where); var assigments = Visit(update.Assigments, VisitColumnAssigment); if(source != update.Source || where != update.Where || assigments != update.Assigments) - return new UpdateExpression(update.Table, update.UseHistoryTable, (SourceWithAliasExpression)source, where, assigments); + return new UpdateExpression(update.Table, update.UseHistoryTable, (SourceWithAliasExpression)source, where, assigments, update.ReturnRowCount); return update; } @@ -51,7 +51,7 @@ protected internal virtual Expression VisitInsertSelect(InsertSelectExpression i var source = VisitSource(insertSelect.Source); var assigments = Visit(insertSelect.Assigments, VisitColumnAssigment); if (source != insertSelect.Source || assigments != insertSelect.Assigments) - return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, (SourceWithAliasExpression)source, assigments); + return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, (SourceWithAliasExpression)source, assigments, insertSelect.ReturnRowCount); return insertSelect; } @@ -63,11 +63,6 @@ protected internal virtual ColumnAssignment VisitColumnAssigment(ColumnAssignmen return c; } - protected internal virtual Expression VisitSelectRowCount(SelectRowCountExpression src) - { - return src; - } - protected internal virtual Expression VisitLiteReference(LiteReferenceExpression lite) { var newRef = Visit(lite.Reference); @@ -147,7 +142,7 @@ protected internal virtual Expression VisitTypeImplementedByAll(TypeImplementedB protected internal virtual Expression VisitMList(MListExpression ml) { var newBackID = (PrimaryKeyExpression)Visit(ml.BackID); - var externalPeriod = (NewExpression)Visit(ml.ExternalPeriod); + var externalPeriod = (IntervalExpression)Visit(ml.ExternalPeriod); if (newBackID != ml.BackID || externalPeriod != ml.ExternalPeriod) return new MListExpression(ml.Type, newBackID, externalPeriod, ml.TableMList); return ml; @@ -167,7 +162,7 @@ protected internal virtual Expression VisitMListElement(MListElementExpression m var parent = (EntityExpression)Visit(mle.Parent); var order = Visit(mle.Order); var element = Visit(mle.Element); - var period = (NewExpression)Visit(mle.TablePeriod); + var period = (IntervalExpression)Visit(mle.TablePeriod); if (rowId != mle.RowId || parent != mle.Parent || order != mle.Order || element != mle.Element || period != mle.TablePeriod) return new MListElementExpression(rowId, parent, order, element, period, mle.Table, mle.Alias); return mle; @@ -176,13 +171,13 @@ protected internal virtual Expression VisitMListElement(MListElementExpression m protected internal virtual Expression VisitAdditionalField(AdditionalFieldExpression ml) { var newBackID = (PrimaryKeyExpression)Visit(ml.BackID); - var externalPeriod = (NewExpression)Visit(ml.ExternalPeriod); + var externalPeriod = (IntervalExpression)Visit(ml.ExternalPeriod); if (newBackID != ml.BackID || externalPeriod != ml.ExternalPeriod) return new AdditionalFieldExpression(ml.Type, newBackID, externalPeriod, ml.Route); return ml; } - protected internal virtual Expression VisitSqlEnum(SqlEnumExpression sqlEnum) + protected internal virtual Expression VisitSqlLiteral(SqlLiteralExpression sqlEnum) { return sqlEnum; } @@ -191,7 +186,7 @@ protected internal virtual Expression VisitSqlCast(SqlCastExpression castExpr) { var expression = Visit(castExpr.Expression); if (expression != castExpr.Expression) - return new SqlCastExpression(castExpr.Type, expression,castExpr.SqlDbType); + return new SqlCastExpression(castExpr.Type, expression,castExpr.DbType); return castExpr; } @@ -209,7 +204,7 @@ protected internal virtual Expression VisitImplementedByAll(ImplementedByAllExpr { var id = Visit(iba.Id); var typeId = (TypeImplementedByAllExpression)Visit(iba.TypeId); - var externalPeriod = (NewExpression)Visit(iba.ExternalPeriod); + var externalPeriod = (IntervalExpression)Visit(iba.ExternalPeriod); if (id != iba.Id || typeId != iba.TypeId || externalPeriod != iba.ExternalPeriod) return new ImplementedByAllExpression(iba.Type, id, typeId, externalPeriod); @@ -231,9 +226,9 @@ protected internal virtual Expression VisitEntity(EntityExpression ee) var mixins = Visit(ee.Mixins, VisitMixinEntity); var externalId = (PrimaryKeyExpression)Visit(ee.ExternalId); - var externalPeriod = (NewExpression)Visit(ee.ExternalPeriod); + var externalPeriod = (IntervalExpression)Visit(ee.ExternalPeriod); - var period = (NewExpression)Visit(ee.TablePeriod); + var period = (IntervalExpression)Visit(ee.TablePeriod); if (ee.Bindings != bindings || ee.ExternalId != externalId || ee.ExternalPeriod != externalPeriod || ee.Mixins != mixins || ee.TablePeriod != period) return new EntityExpression(ee.Type, externalId, externalPeriod, ee.TableAlias, bindings, mixins, period, ee.AvoidExpandOnRetrieving); @@ -339,9 +334,9 @@ protected internal virtual Expression VisitRowNumber(RowNumberExpression rowNumb protected internal virtual Expression VisitAggregate(AggregateExpression aggregate) { - Expression source = Visit(aggregate.Expression); - if (source != aggregate.Expression) - return new AggregateExpression(aggregate.Type, source, aggregate.AggregateFunction, aggregate.Distinct); + var expressions = Visit(aggregate.Arguments); + if (expressions != aggregate.Arguments) + return new AggregateExpression(aggregate.Type, aggregate.AggregateFunction, expressions); return aggregate; } @@ -433,7 +428,7 @@ protected internal virtual Expression VisitSqlTableValuedFunction(SqlTableValued { ReadOnlyCollection args = Visit(sqlFunction.Arguments); if (args != sqlFunction.Arguments) - return new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.Table, sqlFunction.Alias, args); + return new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.ViewTable, sqlFunction.SingleColumnType, sqlFunction.Alias, args); return sqlFunction; } @@ -511,5 +506,15 @@ protected internal virtual Expression VisitPrimaryKeyString(PrimaryKeyStringExpr return new PrimaryKeyStringExpression(id, (TypeImplementedByAllExpression)typeId); } + + protected internal virtual Expression VisitInterval(IntervalExpression interval) + { + Expression min = Visit(interval.Min); + Expression max = Visit(interval.Max); + Expression postgresRange = Visit(interval.PostgresRange); + if (min != interval.Min || max != interval.Max || postgresRange != interval.PostgresRange) + return new IntervalExpression(interval.Type, min, max, postgresRange, interval.AsUtc); + return interval; + } } } diff --git a/Signum.Engine/Linq/ExpressionVisitor/DuplicateHistory.cs b/Signum.Engine/Linq/ExpressionVisitor/DuplicateHistory.cs new file mode 100644 index 0000000000..e809cfdd79 --- /dev/null +++ b/Signum.Engine/Linq/ExpressionVisitor/DuplicateHistory.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using Signum.Engine.Maps; +using Signum.Entities; +using Signum.Utilities; + +namespace Signum.Engine.Linq +{ + /// + /// Rewrite aggregate expressions, moving them into same select expression that has the group-by clause + /// + internal class DuplicateHistory : DbExpressionVisitor + { + private AliasGenerator aliasGenerator; + + public DuplicateHistory(AliasGenerator generator) + { + this.aliasGenerator = generator; + } + + public static Expression Rewrite(Expression expr, AliasGenerator generator) + { + if (!Schema.Current.Settings.IsPostgres) + return expr; + + return new DuplicateHistory(generator).Visit(expr); + } + + public Dictionary> columnReplacements = new Dictionary>(); + + protected internal override Expression VisitTable(TableExpression table) + { + if (table.SystemTime != null) + { + if (table.SystemTime is SystemTime.HistoryTable) + return table; + + var requests = columnReplacements.TryGetC(table.Alias); + + SelectExpression CreateSelect(string tableNameForAlias, SystemTime? systemTime) + { + var tableExp = new TableExpression(aliasGenerator.NextTableAlias(tableNameForAlias), table.Table, systemTime, null); + + ColumnExpression GetTablePeriod() => new ColumnExpression(typeof(NpgsqlTypes.NpgsqlRange), tableExp.Alias, table.Table.SystemVersioned!.PostgreeSysPeriodColumnName); + SqlFunctionExpression tstzrange(DateTime start, DateTime end) => new SqlFunctionExpression(typeof(NpgsqlTypes.NpgsqlRange), null, PostgresFunction.tstzrange.ToString(), + new[] { Expression.Constant(new DateTimeOffset(start)), Expression.Constant(new DateTimeOffset(end)) }); + + var where = table.SystemTime is SystemTime.All ? null : + table.SystemTime is SystemTime.AsOf asOf ? new SqlFunctionExpression(typeof(bool), null, PostgressOperator.Contains, new Expression[] { GetTablePeriod(), Expression.Constant(new DateTimeOffset(asOf.DateTime)) }) : + table.SystemTime is SystemTime.Between b ? new SqlFunctionExpression(typeof(bool), null, PostgressOperator.Overlap, new Expression[] { tstzrange(b.StartDateTime, b.EndtDateTime), GetTablePeriod() }) : + table.SystemTime is SystemTime.ContainedIn ci ? new SqlFunctionExpression(typeof(bool), null, PostgressOperator.Contains, new Expression[] { tstzrange(ci.StartDateTime, ci.EndtDateTime), GetTablePeriod() }) : + throw new UnexpectedValueException(table.SystemTime); + + var newSelect = new SelectExpression(aliasGenerator.NextTableAlias(tableNameForAlias), false, null, + columns: requests?.Select(kvp => new ColumnDeclaration(kvp.Key.Name!, new ColumnExpression(kvp.Key.Type, tableExp.Alias, kvp.Key.Name))), + tableExp, where, null, null, 0); + + return newSelect; + } + + var current = CreateSelect(table.Table.Name.Name, null); + var history = CreateSelect(table.Table.SystemVersioned!.TableName.Name, new SystemTime.HistoryTable()); + + var unionAlias = aliasGenerator.NextTableAlias(table.Table.Name.Name); + if (requests != null) + { + foreach (var col in requests.Keys.ToList()) + { + requests[col] = new ColumnExpression(col.Type, unionAlias, col.Name); + } + } + + return new SetOperatorExpression(SetOperator.UnionAll, current, history, unionAlias); + } + + return base.VisitTable(table); + } + + + + protected internal override Expression VisitJoin(JoinExpression join) + { + this.Visit(join.Condition); + if (join.JoinType == JoinType.CrossApply || join.JoinType == JoinType.OuterApply) + this.VisitSource(join.Right); + + SourceExpression left = this.VisitSource(join.Left); + SourceExpression right = this.VisitSource(join.Right); + Expression condition = this.Visit(join.Condition); + if (left != join.Left || right != join.Right || condition != join.Condition) + { + return new JoinExpression(join.JoinType, left, right, condition); + } + return join; + } + + protected internal override Expression VisitSelect(SelectExpression select) + { + //if (select.SelectRoles == SelectRoles.Where && select.From is TableExpression table && table.SystemTime != null && !(table.SystemTime is SystemTime.HistoryTable)) + //{ + // var current = (SelectExpression)AliasReplacer.Replace(select, this.aliasGenerator); + // var history = (SelectExpression)AliasReplacer.Replace(select, this.aliasGenerator); + + // var newAlias = aliasGenerator.NextSelectAlias(); + + // if (columnReplacements.ContainsKey(select.Alias)) + // throw new InvalidOperationException("Requests to trivial select (only where) not expected"); + + // var requests = columnReplacements.TryGetC(table.Alias).EmptyIfNull().Select(ce => new ColumnDeclaration(ce.Key, )); + + // return new SetOperatorExpression(SetOperator.UnionAll, current, history, table.Alias); + //} + //else + //{ + this.Visit(select.Top); + this.Visit(select.Where); + Visit(select.Columns, VisitColumnDeclaration); + Visit(select.OrderBy, VisitOrderBy); + Visit(select.GroupBy, Visit); + SourceExpression from = this.VisitSource(select.From!); + Expression top = this.Visit(select.Top); + Expression where = this.Visit(select.Where); + ReadOnlyCollection columns = Visit(select.Columns, VisitColumnDeclaration); + ReadOnlyCollection orderBy = Visit(select.OrderBy, VisitOrderBy); + ReadOnlyCollection groupBy = Visit(select.GroupBy, Visit); + + if (top != select.Top || from != select.From || where != select.Where || columns != select.Columns || orderBy != select.OrderBy || groupBy != select.GroupBy) + return new SelectExpression(select.Alias, select.IsDistinct, top, columns, from, where, orderBy, groupBy, select.SelectOptions); + + return select; + //} + } + + protected internal override Expression VisitProjection(ProjectionExpression proj) + { + this.Visit(proj.Projector); + SelectExpression source = (SelectExpression)this.Visit(proj.Select); + Expression projector = this.Visit(proj.Projector); + + if (source != proj.Select || projector != proj.Projector) + return new ProjectionExpression(source, projector, proj.UniqueFunction, proj.Type); + + return proj; + } + + protected internal override Expression VisitColumn(ColumnExpression column) + { + if (column.Name == null) + return column; + + if (this.columnReplacements.TryGetValue(column.Alias, out var dic) && dic.TryGetValue(column, out var repColumn)) + return repColumn ?? column; + + this.columnReplacements.GetOrCreate(column.Alias).Add(column, null); + + return column; + } + + + + } +} diff --git a/Signum.Engine/Linq/ExpressionVisitor/OrderByRewriter.cs b/Signum.Engine/Linq/ExpressionVisitor/OrderByRewriter.cs index b8cf9ac3fd..20c4e6a1cd 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/OrderByRewriter.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/OrderByRewriter.cs @@ -77,7 +77,20 @@ protected internal override Expression VisitSelect(SelectExpression select) if (gatheredKeys != null && (select.IsDistinct || select.GroupBy.HasItems() || select.IsAllAggregates)) savedKeys = gatheredKeys.ToList(); - select = (SelectExpression)base.VisitSelect(select); + if ((AggregateFinder.GetAggregates(select.Columns)?.Any(a => a.AggregateFunction.OrderMatters()) ?? false) && select.From is SelectExpression from) + { + var oldOuterMostSelect = outerMostSelect; + outerMostSelect = from; + + select = (SelectExpression)base.VisitSelect(select); + + outerMostSelect = oldOuterMostSelect; + } + else + { + select = (SelectExpression)base.VisitSelect(select); + } + if (savedKeys != null) gatheredKeys = savedKeys; @@ -235,6 +248,7 @@ private bool IsCountSumOrAvg(SelectExpression select) return false; return aggExp.AggregateFunction == AggregateSqlFunction.Count || + aggExp.AggregateFunction == AggregateSqlFunction.CountDistinct || aggExp.AggregateFunction == AggregateSqlFunction.Sum || aggExp.AggregateFunction == AggregateSqlFunction.Average || aggExp.AggregateFunction == AggregateSqlFunction.StdDev || diff --git a/Signum.Engine/Linq/ExpressionVisitor/QueryBinder.cs b/Signum.Engine/Linq/ExpressionVisitor/QueryBinder.cs index ed56992a12..c493c1fe48 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/QueryBinder.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/QueryBinder.cs @@ -1,6 +1,7 @@ using Microsoft.SqlServer.Server; using Signum.Engine.Basics; using Signum.Engine.Maps; +using Signum.Engine.PostgresCatalog; using Signum.Entities; using Signum.Entities.Basics; using Signum.Entities.DynamicQuery; @@ -33,11 +34,12 @@ internal class QueryBinder : ExpressionVisitor internal SystemTime? systemTime; - internal Schema schema; - + internal Schema schema; + bool isPostgres; public QueryBinder(AliasGenerator aliasGenerator) { this.schema = Schema.Current; + this.isPostgres = this.schema.Settings.IsPostgres; this.systemTime = SystemTime.Current; this.aliasGenerator = aliasGenerator; this.root = null!; @@ -413,16 +415,20 @@ private Expression MapVisitExpandWithIndex(LambdaExpression lambda, ref Projecti private ProjectionExpression VisitCastProjection(Expression source) { - if (source is MethodCallExpression && IsTableValuedFunction((MethodCallExpression)source)) + if (isPostgres && source is MemberExpression m && m.Type.IsArray) { - var oldInTVF = inTableValuedFunction; - inTableValuedFunction = true; + var miUnnest = ReflectionTools.GetMethodInfo(() => PostgresFunctions.unnest(null!)).GetGenericMethodDefinition(); - var visit = Visit(source); + var eType = m.Type.ElementType()!; + + var newSource = Expression.Call(null, miUnnest.MakeGenericMethod(eType), m); - inTableValuedFunction = oldInTVF; + return BindTableValueFunction(newSource); + } - return GetTableValuedFunctionProjection((MethodCallExpression)visit); + if (source is MethodCallExpression mc && IsTableValuedFunction(mc)) + { + return BindTableValueFunction(mc); } else { @@ -432,15 +438,27 @@ private ProjectionExpression VisitCastProjection(Expression source) } } + private ProjectionExpression BindTableValueFunction(MethodCallExpression mc) + { + var oldInTVF = inTableValuedFunction; + inTableValuedFunction = true; + + var visit = (MethodCallExpression)Visit(mc); + + inTableValuedFunction = oldInTVF; + + return GetTableValuedFunctionProjection(visit); + } + private ProjectionExpression AsProjection(Expression expression) { - if (expression is ProjectionExpression) - return (ProjectionExpression)expression; + if (expression is ProjectionExpression pe) + return pe; expression = RemoveProjectionConvert(expression); - if (expression is ProjectionExpression) - return (ProjectionExpression)expression; + if (expression is ProjectionExpression pe2) + return pe2; if (expression.NodeType == ExpressionType.New && expression.Type.IsInstantiationOf(typeof(Grouping<,>))) { @@ -566,19 +584,34 @@ private Expression BindToString(Expression source, Expression separator, MethodI string value = (string)((ConstantExpression)separator).Value; - ColumnDeclaration cd = new ColumnDeclaration(null!, Expression.Add(new SqlConstantExpression(value, typeof(string)), nominated, miStringConcat)); + + if (isPostgres) + { + ColumnDeclaration cd = new ColumnDeclaration(null!, new AggregateExpression(typeof(string), AggregateSqlFunction.string_agg, + new[] { nominated, new SqlConstantExpression(value, typeof(string)) })); - Alias alias = NextSelectAlias(); + Alias alias = NextSelectAlias(); - SelectExpression select = new SelectExpression(alias, false, null, new[] { cd }, projection.Select, null, null, null, SelectOptions.ForXmlPathEmpty); + var select = new SelectExpression(alias, false, null, new[] { cd }, projection.Select, null, null, null, 0); - return new SqlFunctionExpression(typeof(string), null, SqlFunction.STUFF.ToString(), new Expression[] + return new ScalarExpression(typeof(string), select); + } + else { - new ScalarExpression(typeof(string), select), - new SqlConstantExpression(1), - new SqlConstantExpression(value.Length), - new SqlConstantExpression("") - }); + ColumnDeclaration cd = new ColumnDeclaration(null!, Expression.Add(new SqlConstantExpression(value, typeof(string)), nominated, miStringConcat)); + + Alias alias = NextSelectAlias(); + + SelectExpression select = new SelectExpression(alias, false, null, new[] { cd }, projection.Select, null, null, null, SelectOptions.ForXmlPathEmpty); + + return new SqlFunctionExpression(typeof(string), null, SqlFunction.STUFF.ToString(), new Expression[] + { + new ScalarExpression(typeof(string), select), + new SqlConstantExpression(1), + new SqlConstantExpression(value.Length), + new SqlConstantExpression("") + }); + } } static MethodInfo miStringConcat = ReflectionTools.GetMethodInfo(() => string.Concat("", "")); @@ -767,10 +800,11 @@ bool ExtractDistinct(Expression? source, out Expression? innerSource) DbExpressionNominator.FullNominate(exp); var result = new AggregateRequestsExpression(info.GroupAlias, - new AggregateExpression(aggregateFunction == AggregateSqlFunction.Count ? typeof(int) : GetBasicType(nominated), - nominated!, - aggregateFunction, - distinct)); + new AggregateExpression( + aggregateFunction == AggregateSqlFunction.Count ? typeof(int) : GetBasicType(nominated), + aggregateFunction, + new Expression[] { nominated! }) + ); return RestoreWrappedType(result, resultType); } @@ -789,19 +823,19 @@ bool ExtractDistinct(Expression? source, out Expression? innerSource) var nominated = DbExpressionNominator.FullNominate(exp)!.Nullify(); aggregate = (Expression)Expression.Coalesce( - new AggregateExpression(GetBasicType(nominated), nominated, aggregateFunction, distinct), + new AggregateExpression(GetBasicType(nominated), aggregateFunction, new[] { nominated }), new SqlConstantExpression(Activator.CreateInstance(nominated.Type.UnNullify())!)); } else { - var nominated = aggregateFunction == AggregateSqlFunction.Count ? - DbExpressionNominator.FullNominateNotNullable(exp): + var nominated = aggregateFunction == AggregateSqlFunction.Count ? + (exp != null ? DbExpressionNominator.FullNominateNotNullable(exp) : new SqlLiteralExpression(typeof(object), "*")) : DbExpressionNominator.FullNominate(exp); - aggregate = new AggregateExpression(aggregateFunction == AggregateSqlFunction.Count ? typeof(int) : GetBasicType(nominated), - nominated!, - aggregateFunction, - distinct); + aggregate = new AggregateExpression( + aggregateFunction == AggregateSqlFunction.Count ? typeof(int) : GetBasicType(nominated), + distinct ? AggregateSqlFunction.CountDistinct : aggregateFunction, + new[] { nominated!}); } Alias alias = NextSelectAlias(); @@ -927,7 +961,7 @@ private Expression BindContains(Type resultType, Expression source, Expression i if (newItem.Type.UnNullify() == typeof(PrimaryKey)) return SmartEqualizer.InPrimaryKey(newItem, col.Cast().ToArray()); - return SmartEqualizer.In(newItem, col.Cast().ToArray()); + return SmartEqualizer.In(newItem, col.Cast().ToArray(), isPostgres); } else { @@ -1339,27 +1373,52 @@ private ProjectionExpression GetTableValuedFunctionProjection(MethodCallExpressi Type returnType = mce.Method.ReturnType; var type = returnType.GetGenericArguments()[0]; - Table table = schema.ViewBuilder.NewView(type); + var functionName = ObjectName.Parse(mce.Method.GetCustomAttribute()?.Name ?? mce.Method.Name, Schema.Current.Settings.IsPostgres); - Alias tableAlias = NextTableAlias(table.Name); + var arguments = mce.Arguments.Select(a => DbExpressionNominator.FullNominate(a)!).ToList(); - Expression exp = table.GetProjectorExpression(tableAlias, this); - var functionName = mce.Method.GetCustomAttribute()?.Name ?? mce.Method.Name; + if (typeof(IView).IsAssignableFrom(type)) + { + Table table = schema.ViewBuilder.NewView(type); - var argumens = mce.Arguments.Select(a => DbExpressionNominator.FullNominate(a)!).ToList(); + Alias tableAlias = NextTableAlias(table.Name); - SqlTableValuedFunctionExpression tableExpression = new SqlTableValuedFunctionExpression(functionName, table, tableAlias, argumens); + Expression exp = table.GetProjectorExpression(tableAlias, this); - Alias selectAlias = NextSelectAlias(); + SqlTableValuedFunctionExpression tableExpression = new SqlTableValuedFunctionExpression(functionName, table, null, tableAlias, arguments); - ProjectedColumns pc = ColumnProjector.ProjectColumns(exp, selectAlias); + Alias selectAlias = NextSelectAlias(); - ProjectionExpression projection = new ProjectionExpression( - new SelectExpression(selectAlias, false, null, pc.Columns, tableExpression, null, null, null, 0), - pc.Projector, null, returnType); + ProjectedColumns pc = ColumnProjector.ProjectColumns(exp, selectAlias); - return projection; + ProjectionExpression projection = new ProjectionExpression( + new SelectExpression(selectAlias, false, null, pc.Columns, tableExpression, null, null, null, 0), + pc.Projector, null, returnType); + + return projection; + } + else + { + if (!isPostgres) + throw new InvalidOperationException("TableValuedFunctions should return an IQueryable"); + + Alias tableAlias = NextTableAlias(functionName); + + SqlTableValuedFunctionExpression tableExpression = new SqlTableValuedFunctionExpression(functionName, null, type, tableAlias, arguments); + + var columnExpression = new ColumnExpression(type, tableAlias, null); + + Alias selectAlias = NextSelectAlias(); + + var cd = new ColumnDeclaration("val", columnExpression); + + return new ProjectionExpression( + new SelectExpression(selectAlias, false, null, new[] { cd }, tableExpression, null, null, null, 0), + new ColumnExpression(type, selectAlias, "val"), + null, + returnType); + } } internal Expression VisitConstant(object value, Type type) @@ -2221,16 +2280,16 @@ internal CommandExpression BindDelete(Expression source) commands.AddRange(ee.Table.TablesMList().Select(t => { Expression backId = t.BackColumnExpression(aliasGenerator.Table(t.GetName(isHistory))); - return new DeleteExpression(t, isHistory && t.SystemVersioned != null, pr.Select, SmartEqualizer.EqualNullable(backId, ee.ExternalId)); + return new DeleteExpression(t, isHistory && t.SystemVersioned != null, pr.Select, SmartEqualizer.EqualNullable(backId, ee.ExternalId), returnRowCount: false); })); - commands.Add(new DeleteExpression(ee.Table, isHistory && ee.Table.SystemVersioned != null, pr.Select, SmartEqualizer.EqualNullable(id, ee.ExternalId))); + commands.Add(new DeleteExpression(ee.Table, isHistory && ee.Table.SystemVersioned != null, pr.Select, SmartEqualizer.EqualNullable(id, ee.ExternalId), returnRowCount: true)); } else if (pr.Projector is MListElementExpression mlee) { Expression id = mlee.Table.RowIdExpression(aliasGenerator.Table(mlee.Table.GetName(isHistory))); - commands.Add(new DeleteExpression(mlee.Table, isHistory && mlee.Table.SystemVersioned != null, pr.Select, SmartEqualizer.EqualNullable(id, mlee.RowId))); + commands.Add(new DeleteExpression(mlee.Table, isHistory && mlee.Table.SystemVersioned != null, pr.Select, SmartEqualizer.EqualNullable(id, mlee.RowId), returnRowCount: true)); } else if (pr.Projector is EmbeddedEntityExpression eee) { @@ -2238,13 +2297,11 @@ internal CommandExpression BindDelete(Expression source) Expression id = vn.GetIdExpression(aliasGenerator.Table(vn.Name)).ThrowIfNull(() => $"{vn.Name} has no primary name"); - commands.Add(new DeleteExpression(vn, false, pr.Select, SmartEqualizer.EqualNullable(id, eee.GetViewId()))); + commands.Add(new DeleteExpression(vn, false, pr.Select, SmartEqualizer.EqualNullable(id, eee.GetViewId()), returnRowCount: true)); } else throw new InvalidOperationException("Delete not supported for {0}".FormatWith(pr.Projector.GetType().TypeName())); - commands.Add(new SelectRowCountExpression()); - return new CommandAggregateExpression(commands); } @@ -2332,8 +2389,7 @@ internal CommandExpression BindUpdate(Expression source, LambdaExpression? partS var result = new CommandAggregateExpression(new CommandExpression[] { - new UpdateExpression(table, isHistory && table.SystemVersioned != null, pr.Select, condition, assignments), - new SelectRowCountExpression() + new UpdateExpression(table, isHistory && table.SystemVersioned != null, pr.Select, condition, assignments, returnRowCount: true), }); return (CommandAggregateExpression)QueryJoinExpander.ExpandJoins(result, this, cleanRequests: true); @@ -2377,8 +2433,7 @@ internal CommandExpression BindInsert(Expression source, LambdaExpression constr var result = new CommandAggregateExpression(new CommandExpression[] { - new InsertSelectExpression(table, isHistory && table.SystemVersioned != null, pr.Select, assignments), - new SelectRowCountExpression() + new InsertSelectExpression(table, isHistory && table.SystemVersioned != null, pr.Select, assignments, returnRowCount: true), }); return (CommandAggregateExpression)QueryJoinExpander.ExpandJoins(result, this, cleanRequests: true); @@ -2545,7 +2600,7 @@ ColumnAssignment AssignColumn(Expression column, Expression expression) if (col == null) throw new InvalidOperationException("{0} does not represent a column".FormatWith(column.ToString())); - return new ColumnAssignment(col.Name, DbExpressionNominator.FullNominate(expression)!); + return new ColumnAssignment(col.Name!, DbExpressionNominator.FullNominate(expression)!); } #region BinderTools @@ -2676,7 +2731,6 @@ public EntityExpression Completed(EntityExpression entity) var newAlias = NextTableAlias(table.Name); var id = table.GetIdExpression(newAlias)!; var period = table.GenerateSystemPeriod(newAlias, this); - var intersect = period.Intesection(entity.ExternalPeriod); //TODO intersect not used! var bindings = table.GenerateBindings(newAlias, this, id, period); var mixins = table.GenerateMixins(newAlias, this, id, period); @@ -3102,7 +3156,7 @@ public Expression CombineValues(Dictionary implementations, Ty static string GetDefaultName(Expression expression) { if (expression is ColumnExpression) - return ((ColumnExpression)expression).Name; + return ((ColumnExpression)expression).Name ?? "val"; if (expression is UnaryExpression) return GetDefaultName(((UnaryExpression)expression).Operand); @@ -3274,7 +3328,7 @@ protected internal override Expression VisitUpdate(UpdateExpression update) if (source != update.Source || where != update.Where || assigments != update.Assigments) { var select = (source as SourceWithAliasExpression) ?? WrapSelect(source); - return new UpdateExpression(update.Table, update.UseHistoryTable, select, where, assigments); + return new UpdateExpression(update.Table, update.UseHistoryTable, select, where, assigments, update.ReturnRowCount); } return update; } @@ -3286,7 +3340,7 @@ protected internal override Expression VisitInsertSelect(InsertSelectExpression if (source != insertSelect.Source || assigments != insertSelect.Assigments) { var select = (source as SourceWithAliasExpression) ?? WrapSelect(source); - return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, select, assigments); + return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, select, assigments, insertSelect.ReturnRowCount); } return insertSelect; } diff --git a/Signum.Engine/Linq/ExpressionVisitor/QueryFormatter.cs b/Signum.Engine/Linq/ExpressionVisitor/QueryFormatter.cs index 7ddd71f983..425554aba7 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/QueryFormatter.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/QueryFormatter.cs @@ -19,6 +19,9 @@ namespace Signum.Engine.Linq /// internal class QueryFormatter : DbExpressionVisitor { + Schema schema = Schema.Current; + bool isPostgres = Schema.Current.Settings.IsPostgres; + StringBuilder sb = new StringBuilder(); int indent = 2; int depth; @@ -48,28 +51,29 @@ public string GetNextParamAlias() return "@p" + (parameter++); } - MethodInfo miCreateParameter = ReflectionTools.GetMethodInfo((ParameterBuilder s) => s.CreateParameter(null!, SqlDbType.BigInt, null, false, null)); - DbParameterPair CreateParameter(ConstantExpression value) { string name = GetNextParamAlias(); bool nullable = value.Type.IsClass || value.Type.IsNullable(); + object? val = value.Value; Type clrType = value.Type.UnNullify(); if (clrType.IsEnum) + { clrType = typeof(int); + val = val == null ? (int?)null : Convert.ToInt32(val); + } var typePair = Schema.Current.Settings.GetSqlDbTypePair(clrType); var pb = Connector.Current.ParameterBuilder; - var param = pb.CreateParameter(name, typePair.SqlDbType, typePair.UserDefinedTypeName, nullable, value.Value ?? DBNull.Value); + var param = pb.CreateParameter(name, typePair.DbType, typePair.UserDefinedTypeName, nullable, val ?? DBNull.Value); return new DbParameterPair(param, name); } ObjectNameOptions objectNameOptions; - private QueryFormatter() { objectNameOptions = ObjectName.CurrentOptions; @@ -153,7 +157,7 @@ protected override Expression VisitBinary(BinaryExpression b) { if (b.NodeType == ExpressionType.Coalesce) { - sb.Append("IsNull("); + sb.Append("COALESCE("); Visit(b.Left); sb.Append(","); Visit(b.Right); @@ -162,13 +166,18 @@ protected override Expression VisitBinary(BinaryExpression b) else if (b.NodeType == ExpressionType.Equal || b.NodeType == ExpressionType.NotEqual) { sb.Append("("); - Visit(b.Left); sb.Append(b.NodeType == ExpressionType.Equal ? " = " : " <> "); Visit(b.Right); - sb.Append(")"); } + else if (b.NodeType == ExpressionType.ArrayIndex) + { + Visit(b.Left); + sb.Append("["); + Visit(b.Right); + sb.Append("]"); + } else { sb.Append("("); @@ -201,7 +210,10 @@ protected override Expression VisitBinary(BinaryExpression b) case ExpressionType.Add: case ExpressionType.AddChecked: - sb.Append(" + "); + if (this.isPostgres && (b.Left.Type == typeof(string) || b.Right.Type == typeof(string))) + sb.Append(" || "); + else + sb.Append(" + "); break; case ExpressionType.Subtract: case ExpressionType.SubtractChecked: @@ -333,7 +345,7 @@ protected internal override Expression VisitIn(InExpression inExpression) return inExpression; } - protected internal override Expression VisitSqlEnum(SqlEnumExpression sqlEnum) + protected internal override Expression VisitSqlLiteral(SqlLiteralExpression sqlEnum) { sb.Append(sqlEnum.Value); return sqlEnum; @@ -344,9 +356,11 @@ protected internal override Expression VisitSqlCast(SqlCastExpression castExpr) sb.Append("CAST("); Visit(castExpr.Expression); sb.Append(" as "); - sb.Append(castExpr.SqlDbType.ToString().ToUpperInvariant()); - if (castExpr.SqlDbType == SqlDbType.NVarChar || castExpr.SqlDbType == SqlDbType.VarChar) + sb.Append(castExpr.DbType.ToString(schema.Settings.IsPostgres)); + + if (!schema.Settings.IsPostgres && (castExpr.DbType.SqlServer == SqlDbType.NVarChar || castExpr.DbType.SqlServer == SqlDbType.VarChar)) sb.Append("(MAX)"); + sb.Append(")"); return castExpr; } @@ -357,7 +371,7 @@ protected override Expression VisitConstant(ConstantExpression c) sb.Append("NULL"); else { - if (!Schema.Current.Settings.IsDbType(c.Value.GetType().UnNullify())) + if (!schema.Settings.IsDbType(c.Value.GetType().UnNullify())) throw new NotSupportedException(string.Format("The constant for {0} is not supported", c.Value)); var pi = parameterExpressions.GetOrCreate(c, () => this.CreateParameter(c)); @@ -373,12 +387,12 @@ protected internal override Expression VisitSqlConstant(SqlConstantExpression c) sb.Append("NULL"); else { - if (!Schema.Current.Settings.IsDbType(c.Value.GetType().UnNullify())) + if (!schema.Settings.IsDbType(c.Value.GetType().UnNullify())) throw new NotSupportedException(string.Format("The constant for {0} is not supported", c.Value)); - if (c.Value.Equals(true)) + if (!isPostgres && c.Value.Equals(true)) sb.Append("1"); - else if (c.Value.Equals(false)) + else if (!isPostgres && c.Value.Equals(false)) sb.Append("0"); else if (c.Value is string s) sb.Append(s == "" ? "''" : ("'" + s + "'")); @@ -399,9 +413,11 @@ protected internal override Expression VisitSqlVariable(SqlVariableExpression sv protected internal override Expression VisitColumn(ColumnExpression column) { sb.Append(column.Alias.ToString()); - sb.Append("."); - sb.Append(column.Name.SqlEscape()); - + if (column.Name != null) //Is null for PostgressFunctions.unnest and friends (IQueryable table-valued function) + { + sb.Append("."); + sb.Append(column.Name.SqlEscape(isPostgres)); + } return column; } @@ -418,7 +434,7 @@ protected internal override Expression VisitSelect(SelectExpression select) if (select.IsDistinct) sb.Append("DISTINCT "); - if (select.Top != null) + if (select.Top != null && !this.isPostgres) { sb.Append("TOP ("); Visit(select.Top); @@ -491,6 +507,13 @@ protected internal override Expression VisitSelect(SelectExpression select) } } + if (select.Top != null && this.isPostgres) + { + this.AppendNewLine(Indentation.Same); + sb.Append("LIMIT "); + Visit(select.Top); + } + if (select.IsForXmlPathEmpty) { this.AppendNewLine(Indentation.Same); @@ -506,28 +529,45 @@ protected internal override Expression VisitSelect(SelectExpression select) return select; } - Dictionary dic = new Dictionary + + string GetAggregateFunction(AggregateSqlFunction agg) { - {AggregateSqlFunction.Average, "AVG"}, - {AggregateSqlFunction.StdDev, "STDEV"}, - {AggregateSqlFunction.StdDevP, "STDEVP"}, - {AggregateSqlFunction.Count, "COUNT"}, - {AggregateSqlFunction.Max, "MAX"}, - {AggregateSqlFunction.Min, "MIN"}, - {AggregateSqlFunction.Sum, "SUM"} - }; + return agg switch + { + AggregateSqlFunction.Average => "AVG", + AggregateSqlFunction.StdDev => !isPostgres ? "STDEV" : "stddev_samp", + AggregateSqlFunction.StdDevP => !isPostgres? "STDEVP" : "stddev_pop", + AggregateSqlFunction.Count => "COUNT", + AggregateSqlFunction.CountDistinct => "COUNT", + AggregateSqlFunction.Max => "MAX", + AggregateSqlFunction.Min => "MIN", + AggregateSqlFunction.Sum => "SUM", + AggregateSqlFunction.string_agg => "string_agg", + _ => throw new UnexpectedValueException(agg) + }; + } protected internal override Expression VisitAggregate(AggregateExpression aggregate) { - sb.Append(dic[aggregate.AggregateFunction]); + sb.Append(GetAggregateFunction(aggregate.AggregateFunction)); sb.Append("("); - if (aggregate.Distinct) + if (aggregate.AggregateFunction == AggregateSqlFunction.CountDistinct) sb.Append("DISTINCT "); - if (aggregate.Expression == null) + if (aggregate.Arguments.Count == 1 && aggregate.Arguments[0] == null && aggregate.AggregateFunction == AggregateSqlFunction.Count) + { sb.Append("*"); + } else - Visit(aggregate.Expression); + { + for (int i = 0, n = aggregate.Arguments.Count; i < n; i++) + { + Expression exp = aggregate.Arguments[i]; + if (i > 0) + sb.Append(", "); + this.Visit(exp); + } + } sb.Append(")"); return aggregate; @@ -535,7 +575,24 @@ protected internal override Expression VisitAggregate(AggregateExpression aggreg protected internal override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction) { - if (sqlFunction.SqlFunction == SqlFunction.COLLATE.ToString()) + if (isPostgres && sqlFunction.SqlFunction == PostgresFunction.EXTRACT.ToString()) + { + sb.Append(sqlFunction.SqlFunction); + sb.Append("("); + this.Visit(sqlFunction.Arguments[0]); + sb.Append(" from "); + this.Visit(sqlFunction.Arguments[1]); + sb.Append(")"); + } + else if(isPostgres && PostgressOperator.All.Contains(sqlFunction.SqlFunction)) + { + sb.Append("("); + this.Visit(sqlFunction.Arguments[0]); + sb.Append(" " + sqlFunction.SqlFunction + " "); + this.Visit(sqlFunction.Arguments[1]); + sb.Append(")"); + } + else if (sqlFunction.SqlFunction == SqlFunction.COLLATE.ToString()) { this.Visit(sqlFunction.Arguments[0]); sb.Append(" COLLATE "); @@ -543,7 +600,7 @@ protected internal override Expression VisitSqlFunction(SqlFunctionExpression sq sb.Append((string)ce.Value!); } else - { + { if (sqlFunction.Object != null) { Visit(sqlFunction.Object); @@ -565,7 +622,7 @@ protected internal override Expression VisitSqlFunction(SqlFunctionExpression sq protected internal override Expression VisitSqlTableValuedFunction(SqlTableValuedFunctionExpression sqlFunction) { - sb.Append(sqlFunction.SqlFunction); + sb.Append(sqlFunction.SqlFunction.ToString()); sb.Append("("); for (int i = 0, n = sqlFunction.Arguments.Count; i < n; i++) { @@ -585,10 +642,9 @@ private void AppendColumn(ColumnDeclaration column) if (column.Name.HasText() && (c == null || c.Name != column.Name)) { - - sb.Append(column.Name.SqlEscape()); - sb.Append(" = "); this.Visit(column.Expression); + sb.Append(" as "); + sb.Append(column.Name.SqlEscape(isPostgres)); } else { @@ -617,14 +673,6 @@ private void WriteSystemTime(SystemTime st) sb.Append("AS OF "); this.VisitSystemTimeConstant(asOf.DateTime); } - else if (st is SystemTime.FromTo fromTo) - { - sb.Append("FROM "); - this.VisitSystemTimeConstant(fromTo.StartDateTime); - - sb.Append(" TO "); - this.VisitSystemTimeConstant(fromTo.EndtDateTime); - } else if (st is SystemTime.Between between) { sb.Append("BETWEEN "); @@ -709,10 +757,10 @@ protected internal override Expression VisitJoin(JoinExpression join) sb.Append("FULL OUTER JOIN "); break; case JoinType.CrossApply: - sb.Append("CROSS APPLY "); + sb.Append(isPostgres ? "JOIN LATERAL " : "CROSS APPLY "); break; case JoinType.OuterApply: - sb.Append("OUTER APPLY "); + sb.Append(isPostgres ? "LEFT JOIN LATERAL " : "OUTER APPLY "); break; } @@ -733,6 +781,12 @@ protected internal override Expression VisitJoin(JoinExpression join) this.Visit(join.Condition); this.Indent(Indentation.Outer); } + else if (isPostgres && join.JoinType != JoinType.CrossJoin) + { + this.AppendNewLine(Indentation.Inner); + sb.Append("ON true"); + this.Indent(Indentation.Outer); + } return join; } @@ -773,92 +827,133 @@ void VisitSetPart(SourceWithAliasExpression source) protected internal override Expression VisitDelete(DeleteExpression delete) { - sb.Append("DELETE "); - sb.Append(delete.Name.ToString()); - this.AppendNewLine(Indentation.Same); - sb.Append("FROM "); - VisitSource(delete.Source); - if (delete.Where != null) + using (this.PrintSelectRowCount(delete.ReturnRowCount)) { + sb.Append("DELETE FROM "); + sb.Append(delete.Name.ToString()); this.AppendNewLine(Indentation.Same); - sb.Append("WHERE "); - Visit(delete.Where); + + if (isPostgres) + sb.Append("USING "); + else + sb.Append("FROM "); + + VisitSource(delete.Source); + if (delete.Where != null) + { + this.AppendNewLine(Indentation.Same); + sb.Append("WHERE "); + Visit(delete.Where); + } + return delete; } - return delete; } protected internal override Expression VisitUpdate(UpdateExpression update) { - sb.Append("UPDATE "); - sb.Append(update.Name.ToString()); - sb.Append(" SET"); - this.AppendNewLine(Indentation.Inner); - - for (int i = 0, n = update.Assigments.Count; i < n; i++) + using (this.PrintSelectRowCount(update.ReturnRowCount)) { - ColumnAssignment assignment = update.Assigments[i]; - if (i > 0) + sb.Append("UPDATE "); + sb.Append(update.Name.ToString()); + sb.Append(" SET"); + this.AppendNewLine(Indentation.Inner); + + for (int i = 0, n = update.Assigments.Count; i < n; i++) + { + ColumnAssignment assignment = update.Assigments[i]; + if (i > 0) + { + sb.Append(","); + this.AppendNewLine(Indentation.Same); + } + sb.Append(assignment.Column.SqlEscape(isPostgres)); + sb.Append(" = "); + this.Visit(assignment.Expression); + } + this.AppendNewLine(Indentation.Outer); + sb.Append("FROM "); + VisitSource(update.Source); + if (update.Where != null) { - sb.Append(","); this.AppendNewLine(Indentation.Same); + sb.Append("WHERE "); + Visit(update.Where); } - sb.Append(assignment.Column.SqlEscape()); - sb.Append(" = "); - this.Visit(assignment.Expression); - } - this.AppendNewLine(Indentation.Outer); - sb.Append("FROM "); - VisitSource(update.Source); - if (update.Where != null) - { - this.AppendNewLine(Indentation.Same); - sb.Append("WHERE "); - Visit(update.Where); + return update; } - return update; - } protected internal override Expression VisitInsertSelect(InsertSelectExpression insertSelect) { - sb.Append("INSERT INTO "); - sb.Append(insertSelect.Name.ToString()); - sb.Append("("); - for (int i = 0, n = insertSelect.Assigments.Count; i < n; i++) + using (this.PrintSelectRowCount(insertSelect.ReturnRowCount)) { - ColumnAssignment assignment = insertSelect.Assigments[i]; - if (i > 0) + sb.Append("INSERT INTO "); + sb.Append(insertSelect.Name.ToString()); + sb.Append("("); + for (int i = 0, n = insertSelect.Assigments.Count; i < n; i++) { - sb.Append(", "); - if (i % 4 == 0) - this.AppendNewLine(Indentation.Same); + ColumnAssignment assignment = insertSelect.Assigments[i]; + if (i > 0) + { + sb.Append(", "); + if (i % 4 == 0) + this.AppendNewLine(Indentation.Same); + } + sb.Append(assignment.Column.SqlEscape(isPostgres)); } - sb.Append(assignment.Column.SqlEscape()); - } - sb.Append(")"); - this.AppendNewLine(Indentation.Same); - sb.Append("SELECT "); - for (int i = 0, n = insertSelect.Assigments.Count; i < n; i++) - { - ColumnAssignment assignment = insertSelect.Assigments[i]; - if (i > 0) + sb.Append(")"); + this.AppendNewLine(Indentation.Same); + if(this.isPostgres && Administrator.IsIdentityBehaviourDisabled(insertSelect.Table)) { - sb.Append(", "); - if (i % 4 == 0) - this.AppendNewLine(Indentation.Same); + sb.Append("OVERRIDING SYSTEM VALUE"); + this.AppendNewLine(Indentation.Same); } - this.Visit(assignment.Expression); - } - sb.Append(" FROM "); - VisitSource(insertSelect.Source); - return insertSelect; + sb.Append("SELECT "); + for (int i = 0, n = insertSelect.Assigments.Count; i < n; i++) + { + ColumnAssignment assignment = insertSelect.Assigments[i]; + if (i > 0) + { + sb.Append(", "); + if (i % 4 == 0) + this.AppendNewLine(Indentation.Same); + } + this.Visit(assignment.Expression); + } + sb.Append(" FROM "); + VisitSource(insertSelect.Source); + return insertSelect; + } } - protected internal override Expression VisitSelectRowCount(SelectRowCountExpression src) + protected internal IDisposable? PrintSelectRowCount(bool returnRowCount) { - sb.Append("SELECT @@rowcount"); - return src; + if (returnRowCount == false) + return null; + + if (!this.isPostgres) + { + return new Disposable(() => + { + sb.Append("SELECT @@rowcount"); + }); + } + else + { + sb.Append("WITH rows AS ("); + this.AppendNewLine(Indentation.Inner); + + return new Disposable(() => + { + this.AppendNewLine(Indentation.Same); + sb.Append("RETURNING 1"); + this.AppendNewLine(Indentation.Outer); + sb.Append(")"); + this.AppendNewLine(Indentation.Same); + sb.Append("SELECT CAST(COUNT(*) AS INTEGER) FROM rows"); + }); + } } protected internal override Expression VisitCommandAggregate(CommandAggregateExpression cea) diff --git a/Signum.Engine/Linq/ExpressionVisitor/QueryRebinder.cs b/Signum.Engine/Linq/ExpressionVisitor/QueryRebinder.cs index 15909da986..d391467826 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/QueryRebinder.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/QueryRebinder.cs @@ -95,7 +95,7 @@ protected internal override Expression VisitSqlTableValuedFunction(SqlTableValue ReadOnlyCollection args = Visit(sqlFunction.Arguments); if (args != sqlFunction.Arguments) - return new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.Table, sqlFunction.Alias, args); + return new SqlTableValuedFunctionExpression(sqlFunction.SqlFunction, sqlFunction.ViewTable, sqlFunction.SingleColumnType, sqlFunction.Alias, args); return sqlFunction; } @@ -148,7 +148,7 @@ protected internal override Expression VisitDelete(DeleteExpression delete) var where = Visit(delete.Where); if (source != delete.Source || where != delete.Where) - return new DeleteExpression(delete.Table, delete.UseHistoryTable, (SourceWithAliasExpression)source, where); + return new DeleteExpression(delete.Table, delete.UseHistoryTable, (SourceWithAliasExpression)source, where, delete.ReturnRowCount); return delete; } @@ -164,7 +164,7 @@ protected internal override Expression VisitUpdate(UpdateExpression update) var where = Visit(update.Where); var assigments = Visit(update.Assigments, VisitColumnAssigment); if (source != update.Source || where != update.Where || assigments != update.Assigments) - return new UpdateExpression(update.Table, update.UseHistoryTable, (SourceWithAliasExpression)source, where, assigments); + return new UpdateExpression(update.Table, update.UseHistoryTable, (SourceWithAliasExpression)source, where, assigments, update.ReturnRowCount); return update; } @@ -178,7 +178,7 @@ protected internal override Expression VisitInsertSelect(InsertSelectExpression var source = Visit(insertSelect.Source); var assigments = Visit(insertSelect.Assigments, VisitColumnAssigment); if (source != insertSelect.Source || assigments != insertSelect.Assigments) - return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, (SourceWithAliasExpression)source, assigments); + return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, (SourceWithAliasExpression)source, assigments, insertSelect.ReturnRowCount); return insertSelect; } diff --git a/Signum.Engine/Linq/ExpressionVisitor/RedundantSubqueryRemover.cs b/Signum.Engine/Linq/ExpressionVisitor/RedundantSubqueryRemover.cs index 2089253b0b..6e7dd18d6f 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/RedundantSubqueryRemover.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/RedundantSubqueryRemover.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using Signum.Utilities.ExpressionTrees; using Signum.Utilities; +using Signum.Engine.Maps; namespace Signum.Engine.Linq { @@ -14,9 +15,10 @@ private RedundantSubqueryRemover() public static Expression Remove(Expression expression) { - expression = new RedundantSubqueryRemover().Visit(expression); - expression = SubqueryMerger.Merge(expression); - return expression; + var removed = new RedundantSubqueryRemover().Visit(expression); + var merged = SubqueryMerger.Merge(removed); + var simplified = JoinSimplifier.Simplify(merged); + return simplified; } protected internal override Expression VisitSelect(SelectExpression select) @@ -78,11 +80,6 @@ internal static bool IsNameMapProjection(SelectExpression select) return true; } - internal static bool IsInitialProjection(SelectExpression select) - { - return select.From is TableExpression; - } - class RedundantSubqueryGatherer : DbExpressionVisitor { List? redundant; @@ -134,8 +131,47 @@ protected internal override Expression VisitExists(ExistsExpression exists) { return exists; } + + protected internal override Expression VisitJoin(JoinExpression join) + { + var result = (JoinExpression)base.VisitJoin(join); + if (result.JoinType == JoinType.CrossApply || + result.JoinType == JoinType.OuterApply) + { + if (Schema.Current.Settings.IsPostgres && this.redundant != null && result.Right is SelectExpression s && this.redundant.Contains(s)) + { + if (HasJoins(s)) + this.redundant.Remove(s); + } + } + + return result; + } + + protected internal override Expression VisitSetOperator(SetOperatorExpression set) + { + var result = (SetOperatorExpression)base.VisitSetOperator(set); + + if(this.redundant != null) + { + if (result.Left is SelectExpression l) + this.redundant.Remove(l); + + if (result.Right is SelectExpression r) + this.redundant.Remove(r); + } + + return result; + } + + static bool HasJoins(SelectExpression s) + { + return s.From is JoinExpression || s.From is SelectExpression s2 && HasJoins(s2); + } } + + class SubqueryMerger : DbExpressionVisitor { private SubqueryMerger() @@ -324,4 +360,31 @@ protected internal override Expression VisitScalar(ScalarExpression scalar) } } } + + class JoinSimplifier : DbExpressionVisitor + { + internal static Expression Simplify(Expression expression) + { + return new JoinSimplifier().Visit(expression); + } + + protected internal override Expression VisitJoin(JoinExpression join) + { + SourceExpression left = this.VisitSource(join.Left); + SourceExpression right = this.VisitSource(join.Right); + Expression condition = this.Visit(join.Condition); + + if(join.JoinType == JoinType.CrossApply || join.JoinType == JoinType.OuterApply) + { + if (right is TableExpression) + return new JoinExpression(join.JoinType == JoinType.OuterApply ? JoinType.LeftOuterJoin : JoinType.InnerJoin, left, right, new SqlConstantExpression(true)); + } + + if (left != join.Left || right != join.Right || condition != join.Condition) + { + return new JoinExpression(join.JoinType, left, right, condition); + } + return join; + } + } } diff --git a/Signum.Engine/Linq/ExpressionVisitor/SmartEqualizer.cs b/Signum.Engine/Linq/ExpressionVisitor/SmartEqualizer.cs index 1ecebea1ad..8e97e761d7 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/SmartEqualizer.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/SmartEqualizer.cs @@ -571,18 +571,28 @@ internal static Expression TypeIn(Expression typeExpr, IEnumerable collect throw new InvalidOperationException("Impossible to resolve '{0}' in '{1}'".FormatWith(typeExpr.ToString(), collection.ToString(t=>t.TypeName(), ", "))); } - public static Expression In(Expression element, object[] values) + public static Expression In(Expression element, object[] values, bool isPostgres) { var nominate = DbExpressionNominator.FullNominate(element)!; if (nominate is ToDayOfWeekExpression dowe) { - byte dateFirs = ToDayOfWeekExpression.DateFirst.Value.Item1; - var sqlWeekDays = values.Cast() - .Select(a => (object)ToDayOfWeekExpression.ToSqlWeekDay(a, dateFirs)) - .ToArray(); + if (isPostgres) + { + var sqlWeekDays = values.Cast() + .Select(a => (object)(int)a) + .ToArray(); + return InExpression.FromValues(dowe.Expression, sqlWeekDays); + } + else + { - return InExpression.FromValues(dowe.Expression, sqlWeekDays); + byte dateFirs = ((SqlConnector)Connector.Current).DateFirst; + var sqlWeekDays = values.Cast() + .Select(a => (object)ToDayOfWeekExpression.ToSqlWeekDay(a, dateFirs)) + .ToArray(); + return InExpression.FromValues(dowe.Expression, sqlWeekDays); + } } else return InExpression.FromValues(nominate, values); @@ -597,7 +607,12 @@ public static Expression InPrimaryKey(Expression element, PrimaryKey[] values) if (cleanElement == NewId) return False; - return InExpression.FromValues(DbExpressionNominator.FullNominate(cleanElement)!, cleanValues); + cleanElement = DbExpressionNominator.FullNominate(cleanElement)!; + + if (cleanElement.Type == typeof(string)) + return InExpression.FromValues(cleanElement, cleanValues.Select(a => (object)a.ToString()!).ToArray()); + else + return InExpression.FromValues(cleanElement, cleanValues); } private static Expression DispachConditionalTypesIn(ConditionalExpression ce, IEnumerable collection) diff --git a/Signum.Engine/Linq/ExpressionVisitor/SubqueryRemover.cs b/Signum.Engine/Linq/ExpressionVisitor/SubqueryRemover.cs index 1e29164e9d..05ce7bcd5b 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/SubqueryRemover.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/SubqueryRemover.cs @@ -32,7 +32,7 @@ protected internal override Expression VisitSelect(SelectExpression select) protected internal override Expression VisitColumn(ColumnExpression column) { return map.TryGetC(column.Alias) - ?.Let(d => d.GetOrThrow(column.Name, "Reference to undefined column {0}")) ?? column; + ?.Let(d => d.GetOrThrow(column.Name!, "Reference to undefined column {0}")) ?? column; } } } diff --git a/Signum.Engine/Linq/ExpressionVisitor/TranslatorBuilder.cs b/Signum.Engine/Linq/ExpressionVisitor/TranslatorBuilder.cs index 0462541a29..f0c546c622 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/TranslatorBuilder.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/TranslatorBuilder.cs @@ -11,6 +11,7 @@ using Signum.Engine.Maps; using Signum.Entities.Basics; using Signum.Entities.Internal; +using Signum.Utilities.DataStructures; namespace Signum.Engine.Linq { @@ -203,7 +204,7 @@ protected override Expression VisitUnary(UnaryExpression u) if (u.NodeType == ExpressionType.Convert && u.Operand is ColumnExpression && DiffersInNullability(u.Type, u.Operand.Type)) { ColumnExpression column = (ColumnExpression)u.Operand; - return scope.GetColumnExpression(row, column.Alias, column.Name, u.Type); + return scope.GetColumnExpression(row, column.Alias, column.Name!, u.Type); } return base.VisitUnary(u); @@ -219,7 +220,7 @@ bool DiffersInNullability(Type a, Type b) protected internal override Expression VisitColumn(ColumnExpression column) { - return scope.GetColumnExpression(row, column.Alias, column.Name, column.Type); + return scope.GetColumnExpression(row, column.Alias, column.Name!, column.Type); } protected internal override Expression VisitChildProjection(ChildProjectionExpression child) @@ -546,12 +547,37 @@ protected internal override Expression VisitToDayOfWeek(ToDayOfWeekExpression to { var result = this.Visit(toDayOfWeek.Expression); - return Expression.Call(ToDayOfWeekExpression.miToDayOfWeek, result, Expression.Constant(ToDayOfWeekExpression.DateFirst.Value.Item1, typeof(byte))); + if (Schema.Current.Settings.IsPostgres) + { + return Expression.Call(ToDayOfWeekExpression.miToDayOfWeekPostgres, result); + } + else + { + var dateFirst = ((SqlConnector)Connector.Current).DateFirst; + return Expression.Call(ToDayOfWeekExpression.miToDayOfWeekSql, result, Expression.Constant(dateFirst, typeof(byte))); + } + } + + static MethodInfo miToInterval = ReflectionTools.GetMethodInfo(() => ToInterval(new NpgsqlTypes.NpgsqlRange())).GetGenericMethodDefinition(); + static Interval ToInterval(NpgsqlTypes.NpgsqlRange range) where T : struct, IComparable, IEquatable + => new Interval(range.LowerBound, range.UpperBound); + + protected internal override Expression VisitInterval(IntervalExpression interval) + { + var intervalType = interval.Type.GetGenericArguments()[0]; + if (Schema.Current.Settings.IsPostgres) + { + return Expression.Call(miToInterval.MakeGenericMethod(intervalType), Visit(interval.PostgresRange)); + } + else + { + return Expression.New(typeof(Interval<>).MakeGenericType(intervalType).GetConstructor(new[] { intervalType, intervalType })!, Visit(interval.Min), Visit(interval.Max)); + } } protected override Expression VisitNew(NewExpression node) { - var expressions = this.Visit(node.Arguments); + var expressions = this.Visit(node.Arguments); if (node.Members != null) { @@ -561,7 +587,7 @@ protected override Expression VisitNew(NewExpression node) var e = expressions[i]; if (m is PropertyInfo pi && !pi.PropertyType.IsAssignableFrom(e.Type)) { - throw new InvalidOperationException( + throw new InvalidOperationException( $"Impossible to assign a '{e.Type.TypeName()}' to the member '{m.Name}' of type '{pi.PropertyType.TypeName()}'." + (e.Type.IsInstantiationOf(typeof(IEnumerable<>)) ? "\nConsider adding '.ToList()' at the end of your sub-query" : null) ); @@ -569,7 +595,7 @@ protected override Expression VisitNew(NewExpression node) } } - return (Expression) node.Update(expressions); + return (Expression)node.Update(expressions); } } } diff --git a/Signum.Engine/Linq/ExpressionVisitor/UnusedColumnRemover.cs b/Signum.Engine/Linq/ExpressionVisitor/UnusedColumnRemover.cs index 570aa6d42c..99dfcdd186 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/UnusedColumnRemover.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/UnusedColumnRemover.cs @@ -9,7 +9,7 @@ namespace Signum.Engine.Linq { internal class UnusedColumnRemover : DbExpressionVisitor { - Dictionary> allColumnsUsed = new Dictionary>(); + Dictionary> allColumnsUsed = new Dictionary>(); private UnusedColumnRemover() { } @@ -32,7 +32,7 @@ bool IsConstant(Expression exp) protected internal override Expression VisitSelect(SelectExpression select) { // visit column projection first - HashSet columnsUsed = allColumnsUsed.GetOrCreate(select.Alias); // a veces no se usa + HashSet columnsUsed = allColumnsUsed.GetOrCreate(select.Alias); // a veces no se usa ReadOnlyCollection columns = select.Columns.Select(c => { @@ -81,7 +81,7 @@ private void AddSingleColumn(SubqueryExpression subQuery) protected internal override Expression VisitSetOperator(SetOperatorExpression set) { - HashSet columnsUsed = allColumnsUsed.GetOrCreate(set.Alias); // a veces no se usa + HashSet columnsUsed = allColumnsUsed.GetOrCreate(set.Alias); // a veces no se usa allColumnsUsed.GetOrCreate(set.Left.Alias).AddRange(columnsUsed); allColumnsUsed.GetOrCreate(set.Right.Alias).AddRange(columnsUsed); @@ -140,7 +140,7 @@ protected internal override Expression VisitDelete(DeleteExpression delete) var where = Visit(delete.Where); var source = Visit(delete.Source); if (source != delete.Source || where != delete.Where) - return new DeleteExpression(delete.Table, delete.UseHistoryTable, (SourceWithAliasExpression)source, where); + return new DeleteExpression(delete.Table, delete.UseHistoryTable, (SourceWithAliasExpression)source, where, delete.ReturnRowCount); return delete; } @@ -150,7 +150,7 @@ protected internal override Expression VisitUpdate(UpdateExpression update) var assigments = Visit(update.Assigments, VisitColumnAssigment); var source = Visit(update.Source); if (source != update.Source || where != update.Where || assigments != update.Assigments) - return new UpdateExpression(update.Table, update.UseHistoryTable, (SourceWithAliasExpression)source, where, assigments); + return new UpdateExpression(update.Table, update.UseHistoryTable, (SourceWithAliasExpression)source, where, assigments, update.ReturnRowCount); return update; } @@ -159,7 +159,7 @@ protected internal override Expression VisitInsertSelect(InsertSelectExpression var assigments = Visit(insertSelect.Assigments, VisitColumnAssigment); var source = Visit(insertSelect.Source); if (source != insertSelect.Source || assigments != insertSelect.Assigments) - return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, (SourceWithAliasExpression)source, assigments); + return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, (SourceWithAliasExpression)source, assigments, insertSelect.ReturnRowCount); return insertSelect; } diff --git a/Signum.Engine/Linq/ExpressionVisitor/UpdateDeleteSimplifier.cs b/Signum.Engine/Linq/ExpressionVisitor/UpdateDeleteSimplifier.cs index 277e9ade24..1c2b43fa47 100644 --- a/Signum.Engine/Linq/ExpressionVisitor/UpdateDeleteSimplifier.cs +++ b/Signum.Engine/Linq/ExpressionVisitor/UpdateDeleteSimplifier.cs @@ -3,7 +3,7 @@ namespace Signum.Engine.Linq { - class CommandSimplifier: DbExpressionVisitor + class CommandSimplifier : DbExpressionVisitor { bool removeSelectRowCount; AliasGenerator aliasGenerator; @@ -15,16 +15,11 @@ public CommandSimplifier(bool removeSelectRowCount, AliasGenerator aliasGenerato } public static CommandExpression Simplify(CommandExpression ce, bool removeSelectRowCount, AliasGenerator aliasGenerator) - { - return (CommandExpression)new CommandSimplifier(removeSelectRowCount, aliasGenerator).Visit(ce); - } - - protected internal override Expression VisitSelectRowCount(SelectRowCountExpression src) { if (removeSelectRowCount) - return null!; + ce = (CommandExpression)new SelectRowRemover().Visit(ce); - return base.VisitSelectRowCount(src); + return (CommandExpression)new CommandSimplifier(removeSelectRowCount, aliasGenerator).Visit(ce); } protected internal override Expression VisitDelete(DeleteExpression delete) @@ -39,7 +34,7 @@ protected internal override Expression VisitDelete(DeleteExpression delete) if (!TrivialWhere(delete, select)) return delete; - return new DeleteExpression(delete.Table, delete.UseHistoryTable, table, select.Where); + return new DeleteExpression(delete.Table, delete.UseHistoryTable, table, select.Where, delete.ReturnRowCount); } private bool TrivialWhere(DeleteExpression delete, SelectExpression select) @@ -86,7 +81,7 @@ private ColumnExpression ResolveColumn(ColumnExpression ce, SelectExpression sel var result = cd.Expression as ColumnExpression; - if(result == null) + if (result == null) return ce; TableExpression table = (TableExpression)select.From!; @@ -102,4 +97,31 @@ private ColumnExpression ResolveColumn(ColumnExpression ce, SelectExpression sel return ce; } } + + class SelectRowRemover : DbExpressionVisitor + { + protected internal override Expression VisitUpdate(UpdateExpression update) + { + if (update.ReturnRowCount == false) + return update; + + return new UpdateExpression(update.Table, update.UseHistoryTable, update.Source, update.Where, update.Assigments, returnRowCount: false); + } + + protected internal override Expression VisitInsertSelect(InsertSelectExpression insertSelect) + { + if (insertSelect.ReturnRowCount == false) + return insertSelect; + + return new InsertSelectExpression(insertSelect.Table, insertSelect.UseHistoryTable, insertSelect.Source, insertSelect.Assigments, returnRowCount: false); + } + + protected internal override Expression VisitDelete(DeleteExpression delete) + { + if (delete.ReturnRowCount == false) + return delete; + + return new DeleteExpression(delete.Table, delete.UseHistoryTable, delete.Source, delete.Where, returnRowCount: false); + } + } } diff --git a/Signum.Engine/Schema/ObjectName.cs b/Signum.Engine/Schema/ObjectName.cs index 0a2e178292..74433ade6a 100644 --- a/Signum.Engine/Schema/ObjectName.cs +++ b/Signum.Engine/Schema/ObjectName.cs @@ -5,8 +5,16 @@ namespace Signum.Engine.Maps { public static class TableExtensions { - internal static string UnScapeSql(this string name) + internal static string UnScapeSql(this string name, bool isPostgres) { + if (isPostgres) + { + if (name.StartsWith('\"')) + return name.Trim('\"'); + + return name.ToLower(); + } + return name.Trim('[', ']'); } } @@ -14,22 +22,24 @@ internal static string UnScapeSql(this string name) public class ServerName : IEquatable { public string Name { get; private set; } + public bool IsPostgres { get; private set; } /// /// Linked Servers: http://msdn.microsoft.com/en-us/library/ms188279.aspx /// /// - public ServerName(string name) + public ServerName(string name, bool isPostgres) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); this.Name = name; + this.IsPostgres = isPostgres; } public override string ToString() { - return Name.SqlEscape(); + return Name.SqlEscape(IsPostgres); } public override bool Equals(object? obj) => obj is ServerName sn && Equals(sn); @@ -43,35 +53,37 @@ public override int GetHashCode() return Name.GetHashCode(); } - public static ServerName? Parse(string? name) + public static ServerName? Parse(string? name, bool isPostgres) { if (!name.HasText()) return null; - return new ServerName(name.UnScapeSql()); + return new ServerName(name.UnScapeSql(isPostgres), isPostgres); } } public class DatabaseName : IEquatable { public string Name { get; private set; } + public bool IsPostgres { get; private set; } public ServerName? Server { get; private set; } - public DatabaseName(ServerName? server, string name) + public DatabaseName(ServerName? server, string name, bool isPostgres) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); this.Name = name; this.Server = server; + this.IsPostgres = isPostgres; } public override string ToString() { var options = ObjectName.CurrentOptions; - var name = !options.DatabaseNameReplacement.HasText() ? Name.SqlEscape(): Name.Replace(Connector.Current.DatabaseName(), options.DatabaseNameReplacement).SqlEscape(); + var name = !options.DatabaseNameReplacement.HasText() ? Name.SqlEscape(IsPostgres): Name.Replace(Connector.Current.DatabaseName(), options.DatabaseNameReplacement).SqlEscape(IsPostgres); if (Server == null) return name; @@ -91,20 +103,21 @@ public override int GetHashCode() return Name.GetHashCode() ^ (Server == null ? 0 : Server.GetHashCode()); } - public static DatabaseName? Parse(string? name) + public static DatabaseName? Parse(string? name, bool isPostgres) { if (!name.HasText()) return null; - var tuple = ObjectName.SplitLast(name); + var tuple = ObjectName.SplitLast(name, isPostgres); - return new DatabaseName(ServerName.Parse(tuple.prefix), tuple.name); + return new DatabaseName(ServerName.Parse(tuple.prefix, isPostgres), tuple.name, isPostgres); } } public class SchemaName : IEquatable { public string Name { get; private set; } + public bool IsPostgres { get; private set; } readonly DatabaseName? database; @@ -119,25 +132,29 @@ public DatabaseName? Database } } - public static readonly SchemaName Default = new SchemaName(null, "dbo"); + static readonly SchemaName defaultSqlServer = new SchemaName(null, "dbo", isPostgres: false); + static readonly SchemaName defaultPostgreeSql = new SchemaName(null, "public", isPostgres: true); + + public static SchemaName Default(bool isPostgres) => isPostgres ? defaultPostgreeSql : defaultSqlServer; public bool IsDefault() { - return Name == "dbo" && Database == null; + return Database == null && (IsPostgres ? defaultPostgreeSql : defaultSqlServer).Name == Name; } - public SchemaName(DatabaseName? database, string name) + public SchemaName(DatabaseName? database, string name, bool isPostgres) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); this.Name = name; this.database = database; + this.IsPostgres = isPostgres; } public override string ToString() { - var result = Name.SqlEscape(); + var result = Name.SqlEscape(IsPostgres); if (Database == null) return result; @@ -157,33 +174,43 @@ public override int GetHashCode() return Name.GetHashCode() ^ (Database == null ? 0 : Database.GetHashCode()); } - public static SchemaName Parse(string? name) + public static SchemaName Parse(string? name, bool isPostgres) { if (!name.HasText()) - return SchemaName.Default; + return SchemaName.Default(isPostgres); - var tuple = ObjectName.SplitLast(name); + var tuple = ObjectName.SplitLast(name, isPostgres); - return new SchemaName(DatabaseName.Parse(tuple.prefix), (tuple.name)); + return new SchemaName(DatabaseName.Parse(tuple.prefix, isPostgres), tuple.name, isPostgres); } } public class ObjectName : IEquatable { + public static int MaxPostgreeSize = 63; + public string Name { get; private set; } + public bool IsPostgres { get; private set; } - public SchemaName Schema { get; private set; } + public SchemaName Schema { get; private set; } // null only for postgres temporary - public ObjectName(SchemaName schema, string name) + public ObjectName(SchemaName schema, string name, bool isPostgres) { this.Name = name.HasText() ? name : throw new ArgumentNullException(nameof(name)); - this.Schema = schema ?? throw new ArgumentNullException(nameof(schema)); + if (isPostgres && this.Name.Length > MaxPostgreeSize) + throw new InvalidOperationException($"The name '{name}' is too long, consider using TableNameAttribute/ColumnNameAttribute"); + + this.Schema = schema ?? (isPostgres && name.StartsWith("#") ? (SchemaName)null! : throw new ArgumentNullException(nameof(schema))); + this.IsPostgres = isPostgres; } public override string ToString() { - return Schema.ToString() + "." + Name.SqlEscape(); + if (Schema == null) + return Name.SqlEscape(IsPostgres); + + return Schema.ToString() + "." + Name.SqlEscape(IsPostgres); } public override bool Equals(object? obj) => obj is ObjectName on && Equals(on); @@ -195,46 +222,72 @@ public bool Equals(ObjectName other) public override int GetHashCode() { - return Name.GetHashCode() ^ Schema.GetHashCode(); + return Name.GetHashCode() ^ Schema?.GetHashCode() ?? 0; } - public static ObjectName Parse(string? name) + public static ObjectName Parse(string? name, bool isPostgres) { if (!name.HasText()) throw new ArgumentNullException(nameof(name)); - var tuple = SplitLast(name); + var tuple = SplitLast(name, isPostgres); - return new ObjectName(SchemaName.Parse(tuple.prefix), tuple.name); + return new ObjectName(SchemaName.Parse(tuple.prefix, isPostgres), tuple.name, isPostgres); } //FROM "[a.b.c].[d.e.f].[a.b.c].[c.d.f]" //TO ("[a.b.c].[d.e.f].[a.b.c]", "c.d.f") - internal static (string? prefix, string name) SplitLast(string str) + internal static (string? prefix, string name) SplitLast(string str, bool isPostgres) { - if (!str.EndsWith("]")) + if (isPostgres) { + if (!str.EndsWith('\"')) + { + return ( + prefix: str.TryBeforeLast('.'), + name: str.TryAfterLast('.') ?? str + ); + } + + var index = str.LastIndexOf('\"', str.Length - 2); return ( - prefix: str.TryBeforeLast('.'), - name: str.TryAfterLast('.') ?? str - ); + prefix: index == 0 ? null : str.Substring(0, index - 1), + name: str.Substring(index).UnScapeSql(isPostgres) + ); } + else + { - var index = str.LastIndexOf('['); - return ( - prefix: index == 0 ? null : str.Substring(0, index - 1), - name: str.Substring(index).UnScapeSql() - ); + if (!str.EndsWith("]")) + { + return ( + prefix: str.TryBeforeLast('.'), + name: str.TryAfterLast('.') ?? str + ); + } + + var index = str.LastIndexOf('['); + return ( + prefix: index == 0 ? null : str.Substring(0, index - 1), + name: str.Substring(index).UnScapeSql(isPostgres) + ); + } } public ObjectName OnDatabase(DatabaseName? databaseName) { - return new ObjectName(new SchemaName(databaseName, Schema.Name), Name); + if (databaseName != null && databaseName.IsPostgres != this.IsPostgres) + throw new Exception("Inconsitent IsPostgres"); + + return new ObjectName(new SchemaName(databaseName, Schema!.Name, IsPostgres), Name, IsPostgres); } public ObjectName OnSchema(SchemaName schemaName) { - return new ObjectName(schemaName, Name); + if (schemaName.IsPostgres != this.IsPostgres) + throw new Exception("Inconsitent IsPostgres"); + + return new ObjectName(schemaName, Name, IsPostgres); } static readonly ThreadVariable optionsVariable = Statics.ThreadVariable("objectNameOptions"); diff --git a/Signum.Engine/Schema/Schema.Basics.cs b/Signum.Engine/Schema/Schema.Basics.cs index e7bf3e3122..33279325ab 100644 --- a/Signum.Engine/Schema/Schema.Basics.cs +++ b/Signum.Engine/Schema/Schema.Basics.cs @@ -11,6 +11,7 @@ using System.Collections; using Signum.Engine.Linq; using System.Globalization; +using NpgsqlTypes; namespace Signum.Engine.Maps { @@ -36,14 +37,17 @@ public interface ITable SystemVersionedInfo? SystemVersioned { get; } + bool IdentityBehaviour { get; } + FieldEmbedded.EmbeddedHasValueColumn? GetHasValueColumn(IColumn column); } public class SystemVersionedInfo { public ObjectName TableName; - public string StartColumnName; - public string EndColumnName; + public string? StartColumnName; + public string? EndColumnName; + public string? PostgreeSysPeriodColumnName; public SystemVersionedInfo(ObjectName tableName, string startColumnName, string endColumnName) { @@ -52,13 +56,25 @@ public SystemVersionedInfo(ObjectName tableName, string startColumnName, string EndColumnName = endColumnName; } + public SystemVersionedInfo(ObjectName tableName, string postgreeSysPeriodColumnName) + { + TableName = tableName; + PostgreeSysPeriodColumnName = postgreeSysPeriodColumnName; + } + internal IEnumerable Columns() { - return new[] - { - new Column(this.StartColumnName, ColumnType.Start), - new Column(this.EndColumnName, ColumnType.End) - }; + if (PostgreeSysPeriodColumnName != null) + return new[] + { + new PostgreePeriodColumn(this.PostgreeSysPeriodColumnName!), + }; + else + return new[] + { + new SqlServerPeriodColumn(this.StartColumnName!, ColumnType.Start), + new SqlServerPeriodColumn(this.EndColumnName!, ColumnType.End) + }; } public enum ColumnType @@ -67,9 +83,9 @@ public enum ColumnType End, } - public class Column : IColumn + public class SqlServerPeriodColumn : IColumn { - public Column(string name, ColumnType systemVersionColumnType) + public SqlServerPeriodColumn(string name, ColumnType systemVersionColumnType) { this.Name = name; this.SystemVersionColumnType = systemVersionColumnType; @@ -79,7 +95,31 @@ public Column(string name, ColumnType systemVersionColumnType) public ColumnType SystemVersionColumnType { get; private set; } public IsNullable Nullable => IsNullable.No; - public SqlDbType SqlDbType => SqlDbType.DateTime2; + public AbstractDbType DbType => new AbstractDbType(SqlDbType.DateTime2); + public Type Type => typeof(DateTime); + public string? UserDefinedTypeName => null; + public bool PrimaryKey => false; + public bool IdentityBehaviour => false; + public bool Identity => false; + public string? Default { get; set; } + public int? Size => null; + public int? Scale => null; + public string? Collation => null; + public Table? ReferenceTable => null; + public bool AvoidForeignKey => false; + } + + public class PostgreePeriodColumn : IColumn + { + public PostgreePeriodColumn(string name) + { + this.Name = name; + } + + public string Name { get; private set; } + + public IsNullable Nullable => IsNullable.No; + public AbstractDbType DbType => new AbstractDbType(NpgsqlDbType.Range | NpgsqlDbType.TimestampTz); public Type Type => typeof(DateTime); public string? UserDefinedTypeName => null; public bool PrimaryKey => false; @@ -103,10 +143,11 @@ interface ITablePrivate public partial class Table : IFieldFinder, ITable, ITablePrivate { public Type Type { get; private set; } + public Schema Schema { get; private set; } public ObjectName Name { get; set; } - public bool IdentityBehaviour { get; set; } + public bool IdentityBehaviour { get; internal set; } public bool IsView { get; internal set; } public string CleanTypeName { get; set; } @@ -349,7 +390,7 @@ public virtual IEnumerable GenerateIndexes(ITable table) }; if(attribute.AllowMultipleNulls) - result.Where = IndexWhereExpressionVisitor.IsNull(this, false); + result.Where = IndexWhereExpressionVisitor.IsNull(this, false, Schema.Current.Settings.IsPostgres); return result; } @@ -391,7 +432,7 @@ public partial interface IColumn { string Name { get; } IsNullable Nullable { get; } - SqlDbType SqlDbType { get; } + AbstractDbType DbType { get; } Type Type { get; } string? UserDefinedTypeName { get; } bool PrimaryKey { get; } @@ -420,14 +461,14 @@ public static bool ToBool(this IsNullable isNullable) return isNullable != IsNullable.No; } - public static string GetSqlDbTypeString(this IColumn column) - { - return column.SqlDbType.ToString().ToUpper(CultureInfo.InvariantCulture) + SqlBuilder.GetSizeScale(column.Size, column.Scale); - } + //public static string GetSqlDbTypeString(this IColumn column) + //{ + // return column.SqlDbType.ToString().ToUpper(CultureInfo.InvariantCulture) + SqlBuilder.GetSizeScale(column.Size, column.Scale); + //} public static GeneratedAlwaysType GetGeneratedAlwaysType(this IColumn column) { - if (column is SystemVersionedInfo.Column svc) + if (column is SystemVersionedInfo.SqlServerPeriodColumn svc) return svc.SystemVersionColumnType == SystemVersionedInfo.ColumnType.Start ? GeneratedAlwaysType.AsRowStart : GeneratedAlwaysType.AsRowEnd; return GeneratedAlwaysType.None; @@ -446,7 +487,7 @@ public partial class FieldPrimaryKey : Field, IColumn { public string Name { get; set; } IsNullable IColumn.Nullable { get { return IsNullable.No; } } - public SqlDbType SqlDbType { get; set; } + public AbstractDbType DbType { get; set; } public string? UserDefinedTypeName { get; set; } bool IColumn.PrimaryKey { get { return true; } } public bool Identity { get; set; } @@ -483,7 +524,7 @@ public override IEnumerable GenerateIndexes(ITable table) if (this.UniqueIndex != null) throw new InvalidOperationException("Changing IndexType is not allowed for FieldPrimaryKey"); - return new[] { new PrimaryClusteredIndex(table) }; + return new[] { new PrimaryKeyIndex(table) }; } internal override IEnumerable> GetTables() @@ -501,7 +542,7 @@ public partial class FieldValue : Field, IColumn { public string Name { get; set; } public IsNullable Nullable { get; set; } - public SqlDbType SqlDbType { get; set; } + public AbstractDbType DbType { get; set; } public string? UserDefinedTypeName { get; set; } public bool PrimaryKey { get; set; } bool IColumn.Identity { get { return false; } } @@ -523,7 +564,7 @@ public override string ToString() { return "{0} {1} ({2},{3},{4})".FormatWith( Name, - SqlDbType, + DbType, Nullable.ToBool() ? "Nullable" : "", Size, Scale); @@ -567,7 +608,7 @@ public partial class EmbeddedHasValueColumn : IColumn { public string Name { get; set; } public IsNullable Nullable { get { return IsNullable.No; } } //even on neasted embeddeds - public SqlDbType SqlDbType { get { return SqlDbType.Bit; } } + public AbstractDbType DbType => new AbstractDbType(SqlDbType.Bit, NpgsqlDbType.Boolean); string? IColumn.UserDefinedTypeName { get { return null; } } bool IColumn.PrimaryKey { get { return false; } } bool IColumn.Identity { get { return false; } } @@ -763,7 +804,7 @@ public partial class FieldReference : Field, IColumn, IFieldReference int? IColumn.Scale { get { return null; } } public Table ReferenceTable { get; set; } Table? IColumn.ReferenceTable => ReferenceTable; - public SqlDbType SqlDbType { get { return ReferenceTable.PrimaryKey.SqlDbType; } } + public AbstractDbType DbType { get { return ReferenceTable.PrimaryKey.DbType; } } public string? Collation { get { return ReferenceTable.PrimaryKey.Collation; } } public string? UserDefinedTypeName { get { return ReferenceTable.PrimaryKey.UserDefinedTypeName; } } public virtual Type Type { get { return this.Nullable.ToBool() ? ReferenceTable.PrimaryKey.Type.Nullify() : ReferenceTable.PrimaryKey.Type; } } @@ -1007,7 +1048,7 @@ public partial class ImplementationColumn : IColumn int? IColumn.Scale { get { return null; } } public Table ReferenceTable { get; private set; } Table? IColumn.ReferenceTable => ReferenceTable; - public SqlDbType SqlDbType { get { return ReferenceTable.PrimaryKey.SqlDbType; } } + public AbstractDbType DbType { get { return ReferenceTable.PrimaryKey.DbType; } } public string? Collation { get { return ReferenceTable.PrimaryKey.Collation; } } public string? UserDefinedTypeName { get { return ReferenceTable.PrimaryKey.UserDefinedTypeName; } } public Type Type { get { return this.Nullable.ToBool() ? ReferenceTable.PrimaryKey.Type.Nullify() : ReferenceTable.PrimaryKey.Type; } } @@ -1033,7 +1074,7 @@ public partial class ImplementationStringColumn : IColumn int? IColumn.Scale { get { return null; } } public string? Collation { get; set; } public Table? ReferenceTable { get { return null; } } - public SqlDbType SqlDbType { get { return SqlDbType.NVarChar; } } + public AbstractDbType DbType => new AbstractDbType(SqlDbType.NVarChar, NpgsqlDbType.Varchar); public Type Type { get { return typeof(string); } } public bool AvoidForeignKey { get { return false; } } public string? Default { get; set; } @@ -1108,7 +1149,7 @@ public class PrimaryKeyColumn : IColumn { public string Name { get; set; } IsNullable IColumn.Nullable { get { return IsNullable.No; } } - public SqlDbType SqlDbType { get; set; } + public AbstractDbType DbType { get; set; } public string? Collation { get; set; } public string? UserDefinedTypeName { get; set; } bool IColumn.PrimaryKey { get { return true; } } @@ -1181,7 +1222,7 @@ public List GeneratAllIndexes() { var result = new List { - new PrimaryClusteredIndex(this) + new PrimaryKeyIndex(this) }; result.AddRange(BackReference.GenerateIndexes(this)); @@ -1230,6 +1271,8 @@ IColumn ITable.PrimaryKey get { return PrimaryKey; } } + public bool IdentityBehaviour => true; //For now + internal object[] BulkInsertDataRow(Entity entity, object value, int order) { return this.cache.Value.BulkInsertDataRow(entity, value, order); @@ -1248,4 +1291,212 @@ public IEnumerable> GetTables() return null; } } + + public struct AbstractDbType : IEquatable + { + SqlDbType? sqlServer; + public SqlDbType SqlServer => sqlServer ?? throw new InvalidOperationException("No SqlDbType type defined"); + + NpgsqlDbType? postgreSql; + public NpgsqlDbType PostgreSql => postgreSql ?? throw new InvalidOperationException("No PostgresSql type defined"); + + public bool IsPostgres => postgreSql.HasValue; + + public AbstractDbType(SqlDbType sqlDbType) + { + this.sqlServer = sqlDbType; + this.postgreSql = null; + } + + public AbstractDbType(NpgsqlDbType npgsqlDbType) + { + this.sqlServer = null; + this.postgreSql = npgsqlDbType; + } + + public AbstractDbType(SqlDbType sqlDbType, NpgsqlDbType npgsqlDbType) + { + this.sqlServer = sqlDbType; + this.postgreSql = npgsqlDbType; + } + + public override bool Equals(object? obj) => obj is AbstractDbType adt && Equals(adt); + public bool Equals(AbstractDbType adt) => + Schema.Current.Settings.IsPostgres ? + this.postgreSql == adt.postgreSql : + this.sqlServer == adt.sqlServer; + public override int GetHashCode() => this.postgreSql.GetHashCode() ^ this.sqlServer.GetHashCode(); + + public bool IsDate() + { + if (sqlServer is SqlDbType s) + switch (s) + { + case SqlDbType.Date: + case SqlDbType.DateTime: + case SqlDbType.DateTime2: + case SqlDbType.SmallDateTime: + return true; + default: + return false; + } + + if (postgreSql is NpgsqlDbType p) + switch (p) + { + case NpgsqlDbType.Date: + case NpgsqlDbType.Timestamp: + return true; + default: + return false; + } + + throw new NotImplementedException(); + } + + public bool IsNumber() + { + if(sqlServer is SqlDbType s) + switch (s) + { + case SqlDbType.BigInt: + case SqlDbType.Float: + case SqlDbType.Decimal: + case SqlDbType.Int: + case SqlDbType.Bit: + case SqlDbType.Money: + case SqlDbType.Real: + case SqlDbType.TinyInt: + case SqlDbType.SmallInt: + case SqlDbType.SmallMoney: + return true; + default: + return false; + } + + if (postgreSql is NpgsqlDbType p) + switch (p) + { + case NpgsqlDbType.Smallint: + case NpgsqlDbType.Integer: + case NpgsqlDbType.Bigint: + case NpgsqlDbType.Numeric: + case NpgsqlDbType.Money: + case NpgsqlDbType.Real: + case NpgsqlDbType.Double: + return true; + default: + return false; + } + + throw new NotImplementedException(); + } + + public bool IsString() + { + if (sqlServer is SqlDbType s) + switch (s) + { + case SqlDbType.NText: + case SqlDbType.NVarChar: + case SqlDbType.Text: + case SqlDbType.VarChar: + return true; + default: + return false; + } + + + if (postgreSql is NpgsqlDbType p) + switch (p) + { + case NpgsqlDbType.Char: + case NpgsqlDbType.Varchar: + case NpgsqlDbType.Text: + return true; + default: + return false; + } + + throw new NotImplementedException(); + } + + + public override string? ToString() => ToString(Schema.Current.Settings.IsPostgres); + public string ToString(bool isPostgres) + { + if (!isPostgres) + return sqlServer.ToString()!.ToUpperInvariant(); + + var pg = postgreSql!.Value; + if ((pg & NpgsqlDbType.Array) != 0) + return (pg & ~NpgsqlDbType.Range).ToString() + "[]"; + + if ((pg & NpgsqlDbType.Range) != 0) + switch (pg & ~NpgsqlDbType.Range) + { + case NpgsqlDbType.Integer: return "int4range"; + case NpgsqlDbType.Bigint : return "int8range"; + case NpgsqlDbType.Numeric: return "numrange"; + case NpgsqlDbType.TimestampTz: return "tstzrange"; + case NpgsqlDbType.Date: return "daterange"; + throw new InvalidOperationException(""); + } + + if (pg == NpgsqlDbType.Double) + return "double precision"; + + return pg.ToString()!; + } + + public bool IsGuid() + { + if (sqlServer is SqlDbType s) + switch (s) + { + case SqlDbType.UniqueIdentifier: + return true; + default: + return false; + } + + + if (postgreSql is NpgsqlDbType p) + switch (p) + { + case NpgsqlDbType.Uuid: + return true; + default: + return false; + } + + throw new NotImplementedException(); + } + + internal bool IsDecimal() + { + if (sqlServer is SqlDbType s) + switch (s) + { + case SqlDbType.Decimal: + return true; + default: + return false; + } + + if (postgreSql is NpgsqlDbType p) + switch (p) + { + case NpgsqlDbType.Numeric: + return true; + default: + return false; + } + + throw new NotImplementedException(); + } + } } + + + diff --git a/Signum.Engine/Schema/Schema.Delete.cs b/Signum.Engine/Schema/Schema.Delete.cs index 8064bfd690..89c4ae18cf 100644 --- a/Signum.Engine/Schema/Schema.Delete.cs +++ b/Signum.Engine/Schema/Schema.Delete.cs @@ -18,33 +18,54 @@ public SqlPreCommand DeleteSqlSync(T entity, Expression>? where var declaration = where != null ? DeclarePrimaryKeyVariable(entity, where) : null; var variableOrId = entity.Id.VariableName ?? entity.Id.Object; - + var isPostgres = Schema.Current.Settings.IsPostgres; var pre = OnPreDeleteSqlSync(entity); var collections = (from tml in this.TablesMList() - select new SqlPreCommandSimple("DELETE {0} WHERE {1} = {2} --{3}" - .FormatWith(tml.Name, tml.BackReference.Name.SqlEscape(), variableOrId, comment ?? entity.ToString()))).Combine(Spacing.Simple); + select new SqlPreCommandSimple("DELETE FROM {0} WHERE {1} = {2}; --{3}" + .FormatWith(tml.Name, tml.BackReference.Name.SqlEscape(isPostgres), variableOrId, comment ?? entity.ToString()))).Combine(Spacing.Simple); + + var main = new SqlPreCommandSimple("DELETE FROM {0} WHERE {1} = {2}; --{3}" + .FormatWith(Name, this.PrimaryKey.Name.SqlEscape(isPostgres), variableOrId, comment ?? entity.ToString())); - var main = new SqlPreCommandSimple("DELETE {0} WHERE {1} = {2} --{3}" - .FormatWith(Name, this.PrimaryKey.Name.SqlEscape(), variableOrId, comment ?? entity.ToString())); + if (isPostgres && declaration != null) + return PostgresDoBlock(entity.Id.VariableName!, declaration, SqlPreCommand.Combine(Spacing.Simple, pre, collections, main)!); return SqlPreCommand.Combine(Spacing.Simple, declaration, pre, collections, main)!; } int parameterIndex; - private SqlPreCommand DeclarePrimaryKeyVariable(T entity, Expression> where) where T : Entity + private SqlPreCommandSimple DeclarePrimaryKeyVariable(T entity, Expression> where) where T : Entity { var query = DbQueryProvider.Single.GetMainSqlCommand(Database.Query().Where(where).Select(a => a.Id).Expression); - string variableName = SqlParameterBuilder.GetParameterName(this.Name.Name + "Id_" + (parameterIndex++)); + string variableName = this.Name.Name + "Id_" + (parameterIndex++); + if (!Schema.Current.Settings.IsPostgres) + variableName = SqlParameterBuilder.GetParameterName(variableName); + entity.SetId(new Entities.PrimaryKey(entity.id!.Value.Object, variableName)); string queryString = query.PlainSql().Lines().ToString(" "); - var result = new SqlPreCommandSimple($"DECLARE {variableName} {SqlBuilder.GetColumnType(this.PrimaryKey)}; SET {variableName} = COALESCE(({queryString}), 1 / 0)"); + var result = Schema.Current.Settings.IsPostgres ? + new SqlPreCommandSimple(@$"{variableName} {Connector.Current.SqlBuilder.GetColumnType(this.PrimaryKey)} = ({queryString});") : + new SqlPreCommandSimple($"DECLARE {variableName} {Connector.Current.SqlBuilder.GetColumnType(this.PrimaryKey)}; SET {variableName} = COALESCE(({queryString}), 1 / 0);"); return result; } + private SqlPreCommandSimple PostgresDoBlock(string variableName, SqlPreCommandSimple declaration, SqlPreCommand block) + { + return new SqlPreCommandSimple(@$"DO $$ +DECLARE +{declaration.PlainSql().Indent(4)} +BEGIN + IF {variableName} IS NULL THEN + RAISE EXCEPTION 'Not found'; + END IF; +{block.PlainSql().Indent(4)} +END $$;"); + } + public event Func PreDeleteSqlSync; SqlPreCommand? OnPreDeleteSqlSync(Entity entity) diff --git a/Signum.Engine/Schema/Schema.Expressions.cs b/Signum.Engine/Schema/Schema.Expressions.cs index a8675539f3..2296b07ffd 100644 --- a/Signum.Engine/Schema/Schema.Expressions.cs +++ b/Signum.Engine/Schema/Schema.Expressions.cs @@ -11,6 +11,7 @@ using Signum.Utilities.Reflection; using Signum.Utilities.ExpressionTrees; using Signum.Utilities.DataStructures; +using NpgsqlTypes; namespace Signum.Engine.Maps { @@ -44,15 +45,17 @@ internal Expression GetProjectorExpression(Alias tableAlias, QueryBinder binder) internal static ConstructorInfo intervalConstructor = typeof(Interval).GetConstructor(new[] { typeof(DateTime), typeof(DateTime) })!; - internal NewExpression? GenerateSystemPeriod(Alias tableAlias, QueryBinder binder, bool force = false) + internal IntervalExpression? GenerateSystemPeriod(Alias tableAlias, QueryBinder binder, bool force = false) { - return this.SystemVersioned != null && (force || binder.systemTime is SystemTime.Interval) ? Expression.New(intervalConstructor, - new ColumnExpression(typeof(DateTime), tableAlias, this.SystemVersioned.StartColumnName), - new ColumnExpression(typeof(DateTime), tableAlias, this.SystemVersioned.EndColumnName) + return this.SystemVersioned != null && (force || binder.systemTime is SystemTime.Interval) ? new IntervalExpression(typeof(Interval), + this.SystemVersioned.StartColumnName?.Let(c => new ColumnExpression(typeof(DateTime), tableAlias, c)), + this.SystemVersioned.EndColumnName?.Let(c => new ColumnExpression(typeof(DateTime), tableAlias, c)), + this.SystemVersioned.PostgreeSysPeriodColumnName?.Let(c => new ColumnExpression(typeof(NpgsqlRange), tableAlias, c)), + asUtc: true ) : null; } - internal ReadOnlyCollection GenerateBindings(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal ReadOnlyCollection GenerateBindings(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { List result = new List { @@ -73,7 +76,7 @@ internal ReadOnlyCollection GenerateBindings(Alias tableAlias, Que return result.ToReadOnly(); } - internal ReadOnlyCollection? GenerateMixins(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal ReadOnlyCollection? GenerateMixins(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { if (this.Mixins == null) return null; @@ -133,7 +136,7 @@ internal ColumnExpression OrderExpression(Alias tableAlias) return new ColumnExpression(typeof(int), tableAlias, ((IColumn)this.Order!).Name); } - internal Expression FieldExpression(Alias tableAlias, QueryBinder binder, NewExpression? externalPeriod, bool withRowId) + internal Expression FieldExpression(Alias tableAlias, QueryBinder binder, IntervalExpression? externalPeriod, bool withRowId) { var rowId = RowIdExpression(tableAlias); @@ -158,7 +161,7 @@ internal Expression GetProjectorExpression(Alias tableAlias, QueryBinder binder) Type elementType = typeof(MListElement<,>).MakeGenericType(BackReference.FieldType, Field.FieldType); var rowId = RowIdExpression(tableAlias); - NewExpression? period = GenerateSystemPeriod(tableAlias, binder); + IntervalExpression? period = GenerateSystemPeriod(tableAlias, binder); return new MListElementExpression( rowId, @@ -170,11 +173,13 @@ internal Expression GetProjectorExpression(Alias tableAlias, QueryBinder binder) tableAlias); } - internal NewExpression? GenerateSystemPeriod(Alias tableAlias, QueryBinder binder, bool force = false) + internal IntervalExpression? GenerateSystemPeriod(Alias tableAlias, QueryBinder binder, bool force = false) { - return this.SystemVersioned != null && (force || binder.systemTime is SystemTime.Interval) ? Expression.New(Table.intervalConstructor, - new ColumnExpression(typeof(DateTime), tableAlias, this.SystemVersioned.StartColumnName), - new ColumnExpression(typeof(DateTime), tableAlias, this.SystemVersioned.EndColumnName) + return this.SystemVersioned != null && (force || binder.systemTime is SystemTime.Interval) ? new IntervalExpression(typeof(Interval), + this.SystemVersioned.StartColumnName?.Let(c => new ColumnExpression(typeof(DateTime), tableAlias, c)), + this.SystemVersioned.EndColumnName?.Let(c => new ColumnExpression(typeof(DateTime), tableAlias, c)), + this.SystemVersioned.PostgreeSysPeriodColumnName?.Let(c => new ColumnExpression(typeof(NpgsqlRange), tableAlias, c)), + asUtc: true ) : null; } @@ -186,12 +191,12 @@ ColumnExpression ITablePrivate.GetPrimaryOrder(Alias alias) public abstract partial class Field { - internal abstract Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period); + internal abstract Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period); } public partial class FieldPrimaryKey { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { return new PrimaryKeyExpression(new ColumnExpression(this.Type.Nullify(), tableAlias, this.Name).Nullify()); } @@ -199,7 +204,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldValue { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { var column = new ColumnExpression(this.Type, tableAlias, this.Name); @@ -214,7 +219,7 @@ public partial class FieldTicks { public static readonly PropertyInfo piDateTimeTicks = ReflectionTools.GetPropertyInfo((DateTime d) => d.Ticks); - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { if (this.Type == this.FieldType) return new ColumnExpression(this.Type, tableAlias, this.Name); @@ -228,7 +233,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldReference { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { Type cleanType = IsLite ? Lite.Extract(FieldType)! : FieldType; @@ -243,7 +248,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldEnum { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { return Expression.Convert(new ColumnExpression(this.Type, tableAlias, Name), FieldType); } @@ -251,7 +256,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldMList { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { return new MListExpression(FieldType, (PrimaryKeyExpression)id, period, TableMList); // keep back id empty for some seconds } @@ -259,7 +264,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldEmbedded { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { var bindings = (from kvp in EmbeddedFields let fi = kvp.Value.FieldInfo @@ -277,7 +282,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldMixin { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { var bindings = (from kvp in Fields let fi = kvp.Value.FieldInfo @@ -291,7 +296,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldImplementedBy { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { var implementations = ImplementationColumns.SelectDictionary(t => t, (t, ic) => new EntityExpression(t, new PrimaryKeyExpression(new ColumnExpression(ic.Type.Nullify(), tableAlias, ic.Name)), period, null, null, null, null, AvoidExpandOnRetrieving)); @@ -307,7 +312,7 @@ internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, public partial class FieldImplementedByAll { - internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, NewExpression? period) + internal override Expression GetExpression(Alias tableAlias, QueryBinder binder, Expression id, IntervalExpression? period) { Expression result = new ImplementedByAllExpression( IsLite ? Lite.Extract(FieldType)! : FieldType, diff --git a/Signum.Engine/Schema/Schema.Save.cs b/Signum.Engine/Schema/Schema.Save.cs index 59d34d1cc7..6c027b3d8e 100644 --- a/Signum.Engine/Schema/Schema.Save.cs +++ b/Signum.Engine/Schema/Schema.Save.cs @@ -62,6 +62,22 @@ public EntityForbidden(Entity entity, DirectedGraph? graph) public partial class Table { + internal static string Var(bool isPostgres, string varName) + { + if (isPostgres) + return varName; + else + return "@" + varName; + } + + internal static string DeclareTempTable(string variableName, FieldPrimaryKey id, bool isPostgres) + { + if (isPostgres) + return $"CREATE TEMP TABLE {variableName} ({id.Name.SqlEscape(isPostgres)} {id.DbType.ToString(isPostgres)});"; + else + return $"DECLARE {variableName} TABLE({id.Name.SqlEscape(isPostgres)} {id.DbType.ToString(isPostgres)});"; + } + ResetLazy inserterIdentity; ResetLazy inserterDisableIdentity; @@ -69,7 +85,7 @@ internal void InsertMany(List list, DirectedGraph? backEdges) { using (HeavyProfiler.LogNoStackTrace("InsertMany", () => this.Type.TypeName())) { - if (IdentityBehaviour) + if (IdentityBehaviour && !Administrator.IsIdentityBehaviourDisabled(this)) { InsertCacheIdentity ic = inserterIdentity.Value; list.SplitStatements(this.Columns.Count, ls => ic.GetInserter(ls.Count)(ls, backEdges)); @@ -91,22 +107,25 @@ internal object[] BulkInsertDataRow(object/*Entity or IView*/ entity) internal List GetInsertParameters(object entity) { - return IdentityBehaviour ? - inserterIdentity.Value.InsertParameters((Entity)entity, new Forbidden(), "") : - inserterDisableIdentity.Value.InsertParameters(entity, new Forbidden(), ""); + List parameters = new List(); + if (IdentityBehaviour && !Administrator.IsIdentityBehaviourDisabled(this)) + inserterIdentity.Value.InsertParameters((Entity)entity, new Forbidden(), "", parameters); + else + inserterDisableIdentity.Value.InsertParameters(entity, new Forbidden(), "", parameters); + return parameters; } class InsertCacheDisableIdentity { internal Table table; - public Func SqlInsertPattern; - public Func> InsertParameters; + public Func SqlInsertPattern; + public Action> InsertParameters; ConcurrentDictionary, DirectedGraph?>> insertDisableIdentityCache = new ConcurrentDictionary, DirectedGraph?>>(); - public InsertCacheDisableIdentity(Table table, Func sqlInsertPattern, Func> insertParameters) + public InsertCacheDisableIdentity(Table table, Func sqlInsertPattern, Action> insertParameters) { this.table = table; SqlInsertPattern = sqlInsertPattern; @@ -118,10 +137,9 @@ public InsertCacheDisableIdentity(Table table, Func sqlInsertPat return insertDisableIdentityCache.GetOrAdd(numElements, (int num) => num == 1 ? GetInsertDisableIdentity() : GetInsertMultiDisableIdentity(num)); } - Action, DirectedGraph?> GetInsertDisableIdentity() { - string sqlSingle = SqlInsertPattern(""); + string sqlSingle = SqlInsertPattern(new[] { "" }); return (list, graph) => { @@ -135,7 +153,9 @@ public InsertCacheDisableIdentity(Table table, Func sqlInsertPat var forbidden = new Forbidden(graph, entity); - new SqlPreCommandSimple(sqlSingle, InsertParameters(entity, forbidden, "")).ExecuteNonQuery(); + var parameters = new List(); + InsertParameters(entity, forbidden, "", parameters); + new SqlPreCommandSimple(sqlSingle, parameters).ExecuteNonQuery(); entity.IsNew = false; if (table.saveCollections.Value != null) @@ -143,11 +163,9 @@ public InsertCacheDisableIdentity(Table table, Func sqlInsertPat }; } - - Action, DirectedGraph?> GetInsertMultiDisableIdentity(int num) { - string sqlMulti = Enumerable.Range(0, num).ToString(i => SqlInsertPattern(i.ToString()), ";\r\n"); + string sqlMulti = SqlInsertPattern(Enumerable.Range(0, num).Select(i => i.ToString()).ToArray()); return (entities, graph) => { @@ -163,7 +181,7 @@ public InsertCacheDisableIdentity(Table table, Func sqlInsertPat List result = new List(); for (int i = 0; i < entities.Count; i++) - result.AddRange(InsertParameters(entities[i], new Forbidden(graph, entities[i]), i.ToString())); + InsertParameters(entities[i], new Forbidden(graph, entities[i]), i.ToString(), result); new SqlPreCommandSimple(sqlMulti, result).ExecuteNonQuery(); for (int i = 0; i < num; i++) @@ -187,6 +205,7 @@ internal static InsertCacheDisableIdentity InitializeInsertDisableIdentity(Table var paramIdent = Expression.Parameter(typeof(object) /*Entity*/, "ident"); var paramForbidden = Expression.Parameter(typeof(Forbidden), "forbidden"); var paramSuffix = Expression.Parameter(typeof(string), "suffix"); + var paramList = Expression.Parameter(typeof(List), "dbParams"); var cast = Expression.Parameter(table.Type, "casted"); assigments.Add(Expression.Assign(cast, Expression.Convert(paramIdent, table.Type))); @@ -198,13 +217,16 @@ internal static InsertCacheDisableIdentity InitializeInsertDisableIdentity(Table foreach (var item in table.Mixins.Values) item.CreateParameter(trios, assigments, cast, paramForbidden, paramSuffix); - Func insertPattern = (suffix) => - "INSERT {0} ({1})\r\n VALUES ({2})".FormatWith(table.Name, - trios.ToString(p => p.SourceColumn.SqlEscape(), ", "), - trios.ToString(p => p.ParameterName + suffix, ", ")); + var isPostgres = Schema.Current.Settings.IsPostgres; + + Func insertPattern = (suffixes) => + "INSERT INTO {0}\r\n ({1})\r\n{2}VALUES\r\n{3};".FormatWith(table.Name, + trios.ToString(p => p.SourceColumn.SqlEscape(isPostgres), ", "), + isPostgres? "OVERRIDING SYSTEM VALUE\r\n" : null, + suffixes.ToString(s => " (" + trios.ToString(p => p.ParameterName + s, ", ") + ")", ",\r\n")); - var expr = Expression.Lambda>>( - CreateBlock(trios.Select(a => a.ParameterBuilder), assigments), paramIdent, paramForbidden, paramSuffix); + var expr = Expression.Lambda>>( + CreateBlock(trios.Select(a => a.ParameterBuilder), assigments, paramList), paramIdent, paramForbidden, paramSuffix, paramList); return new InsertCacheDisableIdentity(table, insertPattern, expr.Compile()); @@ -216,13 +238,13 @@ class InsertCacheIdentity { internal Table table; - public Func SqlInsertPattern; - public Func> InsertParameters; + public Func SqlInsertPattern; + public Action> InsertParameters; ConcurrentDictionary, DirectedGraph?>> insertIdentityCache = new ConcurrentDictionary, DirectedGraph?>>(); - public InsertCacheIdentity(Table table, Func sqlInsertPattern, Func> insertParameters) + public InsertCacheIdentity(Table table, Func sqlInsertPattern, Action> insertParameters) { this.table = table; SqlInsertPattern = sqlInsertPattern; @@ -236,10 +258,9 @@ public InsertCacheIdentity(Table table, Func sqlInsertPatt Action, DirectedGraph?> GetInsertMultiIdentity(int num) { - string sqlMulti = new StringBuilder() - .AppendLine("DECLARE @MyTable TABLE (Id " + this.table.PrimaryKey.SqlDbType.ToString().ToUpperInvariant() + ");") - .AppendLines(Enumerable.Range(0, num).Select(i => SqlInsertPattern(i.ToString(), true))) - .AppendLine("SELECT Id from @MyTable").ToString(); + var isPostgres = Schema.Current.Settings.IsPostgres; + var newIds = Var(isPostgres, "newIDs"); + string sqlMulti = SqlInsertPattern(Enumerable.Range(0, num).Select(i => i.ToString()).ToArray(), true); return (entities, graph) => { @@ -255,7 +276,7 @@ public InsertCacheIdentity(Table table, Func sqlInsertPatt List result = new List(); for (int i = 0; i < entities.Count; i++) - result.AddRange(InsertParameters(entities[i], new Forbidden(graph, entities[i]), i.ToString())); + InsertParameters(entities[i], new Forbidden(graph, entities[i]), i.ToString(), result); DataTable dt = new SqlPreCommandSimple(sqlMulti, result).ExecuteDataTable(); @@ -282,6 +303,7 @@ internal static InsertCacheIdentity InitializeInsertIdentity(Table table) var paramIdent = Expression.Parameter(typeof(Entity), "ident"); var paramForbidden = Expression.Parameter(typeof(Forbidden), "forbidden"); var paramSuffix = Expression.Parameter(typeof(string), "suffix"); + var paramList = Expression.Parameter(typeof(List), "dbParams"); var cast = Expression.Parameter(table.Type, "casted"); assigments.Add(Expression.Assign(cast, Expression.Convert(paramIdent, table.Type))); @@ -293,16 +315,19 @@ internal static InsertCacheIdentity InitializeInsertIdentity(Table table) foreach (var item in table.Mixins.Values) item.CreateParameter(trios, assigments, cast, paramForbidden, paramSuffix); - Func sqlInsertPattern = (suffix, output) => - "INSERT {0} ({1})\r\n{2} VALUES ({3})".FormatWith( + var isPostgres = Schema.Current.Settings.IsPostgres; + var newIds = Var(isPostgres, "newIDs"); + Func sqlInsertPattern = (suffixes, output) => + "INSERT INTO {0}\r\n ({1})\r\n{2}VALUES\r\n{3}{4};".FormatWith( table.Name, - trios.ToString(p => p.SourceColumn.SqlEscape(), ", "), - output ? "OUTPUT INSERTED.Id into @MyTable \r\n" : null, - trios.ToString(p => p.ParameterName + suffix, ", ")); + trios.ToString(p => p.SourceColumn.SqlEscape(isPostgres), ", "), + output && !isPostgres ? $"OUTPUT INSERTED.{table.PrimaryKey.Name.SqlEscape(isPostgres)}\r\n" : null, + suffixes.ToString(s => " (" + trios.ToString(p => p.ParameterName + s, ", ") + ")", ",\r\n"), + output && isPostgres ? $"\r\nRETURNING {table.PrimaryKey.Name.SqlEscape(isPostgres)}" : null); - var expr = Expression.Lambda>>( - CreateBlock(trios.Select(a => a.ParameterBuilder), assigments), paramIdent, paramForbidden, paramSuffix); + var expr = Expression.Lambda>>( + CreateBlock(trios.Select(a => a.ParameterBuilder), assigments, paramList), paramIdent, paramForbidden, paramSuffix, paramList); return new InsertCacheIdentity(table, sqlInsertPattern, expr.Compile()); } @@ -373,12 +398,12 @@ class UpdateCache internal Table table; public Func SqlUpdatePattern; - public Func> UpdateParameters; + public Action> UpdateParameters; ConcurrentDictionary, DirectedGraph?>> updateCache = new ConcurrentDictionary, DirectedGraph?>>(); - public UpdateCache(Table table, Func sqlUpdatePattern, Func> updateParameters) + public UpdateCache(Table table, Func sqlUpdatePattern, Action> updateParameters) { this.table = table; SqlUpdatePattern = sqlUpdatePattern; @@ -408,7 +433,7 @@ public UpdateCache(Table table, Func sqlUpdatePattern, Fun var forbidden = new Forbidden(graph, ident); - int num = (int)new SqlPreCommandSimple(sqlUpdate, UpdateParameters(ident, oldTicks, forbidden, "")).ExecuteNonQuery(); + int num = (int)new SqlPreCommandSimple(sqlUpdate, new List().Do(ps => UpdateParameters(ident, oldTicks, forbidden, "", ps))).ExecuteNonQuery(); if (num != 1) throw new ConcurrencyException(ident.GetType(), ident.Id); @@ -426,7 +451,7 @@ public UpdateCache(Table table, Func sqlUpdatePattern, Fun var forbidden = new Forbidden(graph, ident); - int num = (int)new SqlPreCommandSimple(sqlUpdate, UpdateParameters(ident, -1, forbidden, "")).ExecuteNonQuery(); + int num = (int)new SqlPreCommandSimple(sqlUpdate, new List().Do(ps => UpdateParameters(ident, -1, forbidden, "", ps))).ExecuteNonQuery(); if (num != 1) throw new EntityNotFoundException(ident.GetType(), ident.Id); }; @@ -435,10 +460,17 @@ public UpdateCache(Table table, Func sqlUpdatePattern, Fun Action, DirectedGraph?> GetUpdateMultiple(int num) { - string sqlMulti = new StringBuilder() - .AppendLine("DECLARE @NotFound TABLE (Id " + this.table.PrimaryKey.SqlDbType.ToString().ToUpperInvariant() + ");") - .AppendLines(Enumerable.Range(0, num).Select(i => SqlUpdatePattern(i.ToString(), true))) - .AppendLine("SELECT Id from @NotFound").ToString(); + var isPostgres = Schema.Current.Settings.IsPostgres; + var updated = Table.Var(isPostgres, "updated"); + var id = this.table.PrimaryKey.Name.SqlEscape(isPostgres); + string sqlMulti = num == 1 ? SqlUpdatePattern("", true) : + new StringBuilder() + .AppendLine(Table.DeclareTempTable(updated, this.table.PrimaryKey, isPostgres)) + .AppendLine() + .AppendLines(Enumerable.Range(0, num).Select(i => SqlUpdatePattern(i.ToString(), true) + "\r\n")) + .AppendLine() + .AppendLine($"SELECT {id} from {updated}") + .ToString(); if (table.Ticks != null) { @@ -452,13 +484,24 @@ public UpdateCache(Table table, Func sqlUpdatePattern, Fun long oldTicks = entity.Ticks; entity.Ticks = TimeZoneManager.Now.Ticks; - parameters.AddRange(UpdateParameters(entity, oldTicks, new Forbidden(graph, entity), i.ToString())); + UpdateParameters(entity, oldTicks, new Forbidden(graph, entity), i.ToString(), parameters); } DataTable dt = new SqlPreCommandSimple(sqlMulti, parameters).ExecuteDataTable(); - if (dt.Rows.Count > 0) - throw new ConcurrencyException(table.Type, dt.Rows.Cast().Select(r => new PrimaryKey((IComparable)r[0])).ToArray()); + if (dt.Rows.Count != idents.Count) + { + var updated = dt.Rows.Cast().Select(r => new PrimaryKey((IComparable)r[0])).ToList(); + + var missing = idents.Select(a => a.Id).Except(updated).ToArray(); + + throw new ConcurrencyException(table.Type, missing); + } + + if (isPostgres && num > 1) + { + new SqlPreCommandSimple($"DROP TABLE {updated}").ExecuteNonQuery(); + } if (table.saveCollections.Value != null) table.saveCollections.Value.UpdateCollections(idents.Select(e => new EntityForbidden(e, new Forbidden(graph, e))).ToList()); @@ -472,7 +515,7 @@ public UpdateCache(Table table, Func sqlUpdatePattern, Fun for (int i = 0; i < num; i++) { var ident = idents[i]; - parameters.AddRange(UpdateParameters(ident, -1, new Forbidden(graph, ident), i.ToString())); + UpdateParameters(ident, -1, new Forbidden(graph, ident), i.ToString(), parameters); } DataTable dt = new SqlPreCommandSimple(sqlMulti, parameters).ExecuteDataTable(); @@ -501,6 +544,7 @@ internal static UpdateCache InitializeUpdate(Table table) var paramForbidden = Expression.Parameter(typeof(Forbidden), "forbidden"); var paramOldTicks = Expression.Parameter(typeof(long), "oldTicks"); var paramSuffix = Expression.Parameter(typeof(string), "suffix"); + var paramList = Expression.Parameter(typeof(List), "paramList"); var cast = Expression.Parameter(table.Type); assigments.Add(Expression.Assign(cast, Expression.Convert(paramIdent, table.Type))); @@ -518,39 +562,44 @@ internal static UpdateCache InitializeUpdate(Table table) string oldTicksParamName = ParameterBuilder.GetParameterName("old_ticks"); - Func sqlUpdatePattern = (suffix, output) => - { - string update = "UPDATE {0} SET \r\n{1}\r\n WHERE {2} = {3}".FormatWith( - table.Name, - trios.ToString(p => "{0} = {1}".FormatWith(p.SourceColumn.SqlEscape(), p.ParameterName + suffix).Indent(2), ",\r\n"), - table.PrimaryKey.Name.SqlEscape(), - idParamName + suffix); - + var isPostgres = Schema.Current.Settings.IsPostgres; - if (table.Ticks != null) - update += " AND {0} = {1}".FormatWith(table.Ticks.Name.SqlEscape(), oldTicksParamName + suffix); + var id = table.PrimaryKey.Name.SqlEscape(isPostgres); + var updated = Table.Var(isPostgres, "updated"); - if (!output) - return update; - else - return update + "\r\nIF @@ROWCOUNT = 0 INSERT INTO @NotFound (id) VALUES ({0})".FormatWith(idParamName + suffix); + Func sqlUpdatePattern = (suffix, output) => + { + var result = $"UPDATE {table.Name} SET\r\n" + +trios.ToString(p => "{0} = {1}".FormatWith(p.SourceColumn.SqlEscape(isPostgres), p.ParameterName + suffix).Indent(2), ",\r\n") + "\r\n" + +(output && !isPostgres ? $"OUTPUT INSERTED.{id} INTO {updated}\r\n" : null) + +$"WHERE {id} = {idParamName + suffix}" + (table.Ticks != null ? $" AND {table.Ticks.Name.SqlEscape(isPostgres)} = {oldTicksParamName + suffix}" : null) + "\r\n" + +(output && isPostgres ? $"RETURNING ({id})" : null); + + if (!(isPostgres && output)) + return result.Trim() + ";"; + + return $@"WITH rows AS ( +{result.Indent(4)} +) +INSERT INTO {updated}({id}) +SELECT {id} FROM rows;"; }; List parameters = new List { - pb.ParameterFactory(Trio.Concat(idParamName, paramSuffix), table.PrimaryKey.SqlDbType, null, false, + pb.ParameterFactory(Trio.Concat(idParamName, paramSuffix), table.PrimaryKey.DbType, null, false, Expression.Field(Expression.Property(Expression.Field(paramIdent, fiId), "Value"), "Object")) }; if (table.Ticks != null) { - parameters.Add(pb.ParameterFactory(Trio.Concat(oldTicksParamName, paramSuffix), table.Ticks.SqlDbType, null, false, table.Ticks.ConvertTicks(paramOldTicks))); + parameters.Add(pb.ParameterFactory(Trio.Concat(oldTicksParamName, paramSuffix), table.Ticks.DbType, null, false, table.Ticks.ConvertTicks(paramOldTicks))); } parameters.AddRange(trios.Select(a => (Expression)a.ParameterBuilder)); - var expr = Expression.Lambda>>( - CreateBlock(parameters, assigments), paramIdent, paramOldTicks, paramForbidden, paramSuffix); + var expr = Expression.Lambda>>( + CreateBlock(parameters, assigments, paramList), paramIdent, paramOldTicks, paramForbidden, paramSuffix, paramList); return new UpdateCache(table, sqlUpdatePattern, expr.Compile()); @@ -629,36 +678,41 @@ public SqlPreCommand InsertSqlSync(Entity ident, bool includeCollections = true, PrepareEntitySync(ident); SetToStrField(ident); - bool isGuid = this.PrimaryKey.SqlDbType == SqlDbType.UniqueIdentifier; + bool isGuid = this.PrimaryKey.DbType.IsGuid(); SqlPreCommand? collections = GetInsertCollectionSync(ident, includeCollections, suffix); - SqlPreCommandSimple insert = IdentityBehaviour ? + SqlPreCommandSimple insert = IdentityBehaviour && !Administrator.IsIdentityBehaviourDisabled(this) ? new SqlPreCommandSimple( - inserterIdentity.Value.SqlInsertPattern(suffix, isGuid && collections != null), - inserterIdentity.Value.InsertParameters(ident, new Forbidden(), suffix)).AddComment(comment) : + inserterIdentity.Value.SqlInsertPattern(new[] { suffix }, isGuid && collections != null), + new List().Do(dbParams => inserterIdentity.Value.InsertParameters(ident, new Forbidden(), suffix, dbParams))).AddComment(comment) : new SqlPreCommandSimple( - inserterDisableIdentity.Value.SqlInsertPattern(suffix), - inserterDisableIdentity.Value.InsertParameters(ident, new Forbidden(), suffix)).AddComment(comment); + inserterDisableIdentity.Value.SqlInsertPattern(new[] { suffix }), + new List().Do(dbParams => inserterDisableIdentity.Value.InsertParameters(ident, new Forbidden(), suffix, dbParams))).AddComment(comment); if (collections == null) return insert; + bool isPostgres = Schema.Current.Settings.IsPostgres; + var pkType = this.PrimaryKey.DbType.ToString(isPostgres); + var newIds = Table.Var(isPostgres, "newIDs"); + var parentId = Table.Var(isPostgres, "parentId"); + if (isGuid) { - SqlPreCommand idTable = new SqlPreCommandSimple("DECLARE @MyTable table (Id UNIQUEIDENTIFIER)") { GoBefore = true }; - - SqlPreCommand declareParent = new SqlPreCommandSimple("DECLARE @parentId UNIQUEIDENTIFIER"); - SqlPreCommand setParent = new SqlPreCommandSimple("SELECT @parentId= ID FROM @MyTable"); - - return SqlPreCommand.Combine(Spacing.Simple, idTable, insert, declareParent, setParent, collections)!; + return SqlPreCommand.Combine(Spacing.Simple, + insert, + new SqlPreCommandSimple($"DECLARE {parentId} {pkType};"), + new SqlPreCommandSimple($"SELECT {parentId} = ID FROM {newIds};"), + collections)!; } else { - SqlPreCommand declareParent = new SqlPreCommandSimple("DECLARE @parentId INT") { GoBefore = true }; - SqlPreCommand setParent = new SqlPreCommandSimple("SET @parentId = @@Identity"); - - return SqlPreCommand.Combine(Spacing.Simple, declareParent, insert, setParent, collections)!; + return SqlPreCommand.Combine(Spacing.Simple, + new SqlPreCommandSimple($"DECLARE {parentId} {pkType};") { GoBefore = true }, + insert, + new SqlPreCommandSimple($"SET {parentId} = @@Identity;"), + collections)!; } } @@ -678,14 +732,19 @@ public SqlPreCommand InsertSqlSync(Entity ident, bool includeCollections = true, var uc = updater.Value; var sql = uc.SqlUpdatePattern(suffix, false); - var parameters = uc.UpdateParameters(entity, (entity as Entity)?.Ticks ?? -1, new Forbidden(), suffix); + var parameters = new List().Do(ps => uc.UpdateParameters(entity, (entity as Entity)?.Ticks ?? -1, new Forbidden(), suffix, ps)); SqlPreCommand? update; if (where != null) { - update = SqlPreCommand.Combine(Spacing.Simple, - DeclarePrimaryKeyVariable(entity, where), - new SqlPreCommandSimple(sql, parameters).AddComment(comment).ReplaceFirstParameter(entity.Id.VariableName)); + bool isPostgres = Schema.Current.Settings.IsPostgres; + + var declare = DeclarePrimaryKeyVariable(entity, where); + var updateSql = new SqlPreCommandSimple(sql, parameters).AddComment(comment).ReplaceFirstParameter(entity.Id.VariableName); + + update = isPostgres ? + PostgresDoBlock(entity.Id.VariableName!, declare, updateSql) : + SqlPreCommand.Combine(Spacing.Simple, declare, updateSql);; } else { @@ -726,8 +785,8 @@ public class Trio public Trio(IColumn column, Expression value, Expression suffix) { this.SourceColumn = column.Name; - this.ParameterName = Engine.ParameterBuilder.GetParameterName(column.Name); - this.ParameterBuilder = Connector.Current.ParameterBuilder.ParameterFactory(Concat(this.ParameterName, suffix), column.SqlDbType, column.UserDefinedTypeName, column.Nullable.ToBool(), value); + this.ParameterName = Signum.Engine.ParameterBuilder.GetParameterName(column.Name); + this.ParameterBuilder = Connector.Current.ParameterBuilder.ParameterFactory(Concat(this.ParameterName, suffix), column.DbType, column.UserDefinedTypeName, column.Nullable.ToBool(), value); } public string SourceColumn; @@ -747,14 +806,13 @@ internal static Expression Concat(string baseName, Expression suffix) } } - static ConstructorInfo ciNewList = ReflectionTools.GetConstuctorInfo(() => new List(1)); + static MethodInfo miAdd = ReflectionTools.GetMethodInfo(() => new List(1).Add(null!)); - public static Expression CreateBlock(IEnumerable parameters, IEnumerable assigments) + public static Expression CreateBlock(IEnumerable parameters, IEnumerable assigments, Expression parameterList) { - return Expression.Block(assigments.OfType().Select(a => (ParameterExpression)a.Left), - assigments.And( - Expression.ListInit(Expression.New(ciNewList, Expression.Constant(parameters.Count())), - parameters))); + return Expression.Block( + assigments.OfType().Select(a => (ParameterExpression)a.Left), + assigments.Concat(parameters.Select(p => Expression.Call(parameterList, miAdd, p)))); } } @@ -828,7 +886,7 @@ public MListDelete(Entity ident, PrimaryKey[] exceptRowIds) internal bool hasOrder = false; internal bool isEmbeddedEntity = false; internal Func sqlUpdate = null!; - public Func> UpdateParameters = null!; + public Action> UpdateParameters = null!; public ConcurrentDictionary>> updateCache = new ConcurrentDictionary>>(); @@ -847,7 +905,7 @@ Action> GetUpdate(int numElements) var row = pair.MList.InnerList[pair.Index]; - parameters.AddRange(UpdateParameters(pair.Entity, row.RowId!.Value, row.Element, pair.Index, pair.Forbidden, i.ToString())); + UpdateParameters(pair.Entity, row.RowId!.Value, row.Element, pair.Index, pair.Forbidden, i.ToString(), parameters); } new SqlPreCommandSimple(sql, parameters).ExecuteNonQuery(); }; @@ -870,8 +928,8 @@ public MListUpdate(EntityForbidden ef, MList mlist, int index) } } - internal Func sqlInsert = null!; - public Func> InsertParameters = null!; + internal Func sqlInsert = null!; + public Action> InsertParameters = null!; public ConcurrentDictionary>> insertCache = new ConcurrentDictionary>>(); @@ -879,10 +937,10 @@ Action> GetInsert(int numElements) { return insertCache.GetOrAdd(numElements, num => { - string sqlMulti = new StringBuilder() - .AppendLine("DECLARE @MyTable TABLE (Id " + this.table.PrimaryKey.SqlDbType.ToString().ToUpperInvariant() + ");") - .AppendLines(Enumerable.Range(0, num).Select(i => sqlInsert(i.ToString(), true))) - .AppendLine("SELECT Id from @MyTable").ToString(); + bool isPostgres = Schema.Current.Settings.IsPostgres; + var newIds = Table.Var(isPostgres, "newIDs"); + + string sqlMulti = sqlInsert(Enumerable.Range(0, num).Select(i => i.ToString()).ToArray(), true); return (List list) => { @@ -890,7 +948,7 @@ Action> GetInsert(int numElements) for (int i = 0; i < num; i++) { var pair = list[i]; - result.AddRange(InsertParameters(pair.Entity, pair.MList.InnerList[pair.Index].Element, pair.Index, pair.Forbidden, i.ToString())); + InsertParameters(pair.Entity, pair.MList.InnerList[pair.Index].Element, pair.Index, pair.Forbidden, i.ToString(), result); } DataTable dt = new SqlPreCommandSimple(sqlMulti, result).ExecuteDataTable(); @@ -926,7 +984,9 @@ public MListInsert(EntityForbidden ef, MList mlist, int index) public object[] BulkInsertDataRow(Entity entity, object value, int order) { - return InsertParameters(entity, (T)value, order, new Forbidden(null), "").Select(a => a.Value).ToArray(); + List paramList = new List(); + InsertParameters(entity, (T)value, order, new Forbidden(null), "", paramList); + return paramList.Select(a => a.Value).ToArray(); } public Func> Getter = null!; @@ -1037,13 +1097,16 @@ public void RelationalUpdates(List idents) if (parent.IsNew) { + var isPostgres = Schema.Current.Settings.IsPostgres; + var parentIdVar = Table.Var(isPostgres, "parentId"); return collection.Select((e, i) => { - var parameters = InsertParameters(parent, e, i, new Forbidden(new HashSet { parent }), suffix + "_" + i); + var parameters = new List(); + InsertParameters(parent, e, i, new Forbidden(new HashSet { parent }), suffix + "_" + i, parameters); var parentId = parameters.First(); // wont be replaced, generating @parentId parameters.RemoveAt(0); - string script = sqlInsert(suffix + "_" + i, false); - script = script.Replace(parentId.ParameterName, "@parentId"); + string script = sqlInsert(new[] { suffix + "_" + i }, false); + script = script.Replace(parentId.ParameterName, parentIdVar); return new SqlPreCommandSimple(script, parameters).AddComment(e?.ToString()); }).Combine(Spacing.Simple); } @@ -1051,7 +1114,7 @@ public void RelationalUpdates(List idents) { return SqlPreCommand.Combine(Spacing.Simple, new SqlPreCommandSimple(sqlDelete(suffix), new List { DeleteParameter(parent, suffix) }).ReplaceFirstParameter(replaceParameter ? parent.Id.VariableName : null), - collection.Select((e, i) => new SqlPreCommandSimple(sqlInsert(suffix + "_" + i, false), InsertParameters(parent, e, i, new Forbidden(), suffix + "_" + i)) + collection.Select((e, i) => new SqlPreCommandSimple(sqlInsert(new[] { suffix + "_" + i }, false), new List().Do(ps => InsertParameters(parent, e, i, new Forbidden(), suffix + "_" + i, ps))) .AddComment(e?.ToString()) .ReplaceFirstParameter(replaceParameter ? parent.Id.VariableName : null) ).Combine(Spacing.Simple)); @@ -1067,22 +1130,23 @@ public void RelationalUpdates(List idents) TableMListCache CreateCache() { var pb = Connector.Current.ParameterBuilder; + var isPostgres = Schema.Current.Settings.IsPostgres; TableMListCache result = new TableMListCache { table = this, Getter = entity => (MList)Getter(entity), - sqlDelete = suffix => "DELETE {0} WHERE {1} = {2}".FormatWith(Name, BackReference.Name.SqlEscape(), ParameterBuilder.GetParameterName(BackReference.Name + suffix)), + sqlDelete = suffix => "DELETE FROM {0} WHERE {1} = {2}".FormatWith(Name, BackReference.Name.SqlEscape(isPostgres), ParameterBuilder.GetParameterName(BackReference.Name + suffix)), DeleteParameter = (ident, suffix) => pb.CreateReferenceParameter(ParameterBuilder.GetParameterName(BackReference.Name + suffix), ident.Id, this.BackReference.ReferenceTable.PrimaryKey), sqlDeleteExcept = num => { - var sql = "DELETE {0} WHERE {1} = {2}" - .FormatWith(Name, BackReference.Name.SqlEscape(), ParameterBuilder.GetParameterName(BackReference.Name)); + var sql = "DELETE FROM {0} WHERE {1} = {2}" + .FormatWith(Name, BackReference.Name.SqlEscape(isPostgres), ParameterBuilder.GetParameterName(BackReference.Name)); sql += " AND {0} NOT IN ({1})" - .FormatWith(PrimaryKey.Name.SqlEscape(), 0.To(num).Select(i => ParameterBuilder.GetParameterName("e" + i)).ToString(", ")); + .FormatWith(PrimaryKey.Name.SqlEscape(isPostgres), 0.To(num).Select(i => ParameterBuilder.GetParameterName("e" + i)).ToString(", ")); return sql; }, @@ -1104,8 +1168,7 @@ TableMListCache CreateCache() var paramOrder = Expression.Parameter(typeof(int), "order"); var paramForbidden = Expression.Parameter(typeof(Forbidden), "forbidden"); var paramSuffix = Expression.Parameter(typeof(string), "suffix"); - - + var paramList = Expression.Parameter(typeof(List), "paramList"); { var trios = new List(); var assigments = new List(); @@ -1115,13 +1178,16 @@ TableMListCache CreateCache() Order.CreateParameter(trios, assigments, paramOrder, paramForbidden, paramSuffix); Field.CreateParameter(trios, assigments, paramItem, paramForbidden, paramSuffix); - result.sqlInsert = (suffix, output) => "INSERT {0} ({1})\r\n{2} VALUES ({3})".FormatWith(Name, - trios.ToString(p => p.SourceColumn.SqlEscape(), ", "), - output ? "OUTPUT INSERTED.Id into @MyTable \r\n" : null, - trios.ToString(p => p.ParameterName + suffix, ", ")); + var newIds = Table.Var(isPostgres, "newIDs"); + + result.sqlInsert = (suffixes, output) => "INSERT INTO {0} ({1})\r\n{2}VALUES\r\n{3}{4};".FormatWith(Name, + trios.ToString(p => p.SourceColumn.SqlEscape(isPostgres), ", "), + output && !isPostgres ? $"OUTPUT INSERTED.{PrimaryKey.Name.SqlEscape(isPostgres)}\r\n" : null, + suffixes.ToString(s => " (" + trios.ToString(p => p.ParameterName + s, ", ") + ")", ",\r\n"), + output && isPostgres ? $"\r\nRETURNING {PrimaryKey.Name.SqlEscape(isPostgres)}" : null); - var expr = Expression.Lambda>>( - Table.CreateBlock(trios.Select(a => a.ParameterBuilder), assigments), paramIdent, paramItem, paramOrder, paramForbidden, paramSuffix); + var expr = Expression.Lambda>>( + Table.CreateBlock(trios.Select(a => a.ParameterBuilder), assigments, paramList), paramIdent, paramItem, paramOrder, paramForbidden, paramSuffix, paramList); result.InsertParameters = expr.Compile(); } @@ -1144,20 +1210,20 @@ TableMListCache CreateCache() Order.CreateParameter(trios, assigments, paramOrder, paramForbidden, paramSuffix); Field.CreateParameter(trios, assigments, paramItem, paramForbidden, paramSuffix); - result.sqlUpdate = suffix => "UPDATE {0} SET \r\n{1}\r\n WHERE {2} = {3} AND {4} = {5}".FormatWith(Name, - trios.ToString(p => "{0} = {1}".FormatWith(p.SourceColumn.SqlEscape(), p.ParameterName + suffix).Indent(2), ",\r\n"), - this.BackReference.Name.SqlEscape(), ParameterBuilder.GetParameterName(parentId + suffix), - this.PrimaryKey.Name.SqlEscape(), ParameterBuilder.GetParameterName(rowId + suffix)); + result.sqlUpdate = suffix => "UPDATE {0} SET \r\n{1}\r\n WHERE {2} = {3} AND {4} = {5};".FormatWith(Name, + trios.ToString(p => "{0} = {1}".FormatWith(p.SourceColumn.SqlEscape(isPostgres), p.ParameterName + suffix).Indent(2), ",\r\n"), + this.BackReference.Name.SqlEscape(isPostgres), ParameterBuilder.GetParameterName(parentId + suffix), + this.PrimaryKey.Name.SqlEscape(isPostgres), ParameterBuilder.GetParameterName(rowId + suffix)); var parameters = trios.Select(a => a.ParameterBuilder).ToList(); - parameters.Add(pb.ParameterFactory(Table.Trio.Concat(parentId, paramSuffix), this.BackReference.SqlDbType, null, false, + parameters.Add(pb.ParameterFactory(Table.Trio.Concat(parentId, paramSuffix), this.BackReference.DbType, null, false, Expression.Field(Expression.Property(Expression.Field(paramIdent, Table.fiId), "Value"), "Object"))); - parameters.Add(pb.ParameterFactory(Table.Trio.Concat(rowId, paramSuffix), this.PrimaryKey.SqlDbType, null, false, + parameters.Add(pb.ParameterFactory(Table.Trio.Concat(rowId, paramSuffix), this.PrimaryKey.DbType, null, false, Expression.Field(paramRowId, "Object"))); - var expr = Expression.Lambda>>( - Table.CreateBlock(parameters, assigments), paramIdent, paramRowId, paramItem, paramOrder, paramForbidden, paramSuffix); + var expr = Expression.Lambda>>( + Table.CreateBlock(parameters, assigments, paramList), paramIdent, paramRowId, paramItem, paramOrder, paramForbidden, paramSuffix, paramList); result.UpdateParameters = expr.Compile(); } diff --git a/Signum.Engine/Schema/Schema.cs b/Signum.Engine/Schema/Schema.cs index 594354ffed..d76d463f53 100644 --- a/Signum.Engine/Schema/Schema.cs +++ b/Signum.Engine/Schema/Schema.cs @@ -61,8 +61,10 @@ public Dictionary Tables get { return tables; } } - const string errorType = "TypeEntity table not cached. Remember to call Schema.Current.Initialize"; - + public List PostgresExtensions = new List() + { + "uuid-ossp" + }; #region Events @@ -241,7 +243,7 @@ internal void OnPreBulkInsert(Type type, bool inMListTable) return ee.CacheController; } - internal IEnumerable GetAdditionalQueryBindings(PropertyRoute parent, PrimaryKeyExpression id, NewExpression? period) + internal IEnumerable GetAdditionalQueryBindings(PropertyRoute parent, PrimaryKeyExpression id, IntervalExpression? period) { //AssertAllowed(parent.RootType, inUserInterface: false); @@ -541,6 +543,8 @@ static Schema() ModifiableEntity.SetIsRetrievingFunc(() => EntityCache.HasRetriever); } + + internal Schema(SchemaSettings settings) { this.typeCachesLazy = null!; @@ -549,6 +553,8 @@ internal Schema(SchemaSettings settings) this.ViewBuilder = new Maps.ViewBuilder(this); Generating += SchemaGenerator.SnapshotIsolation; + Generating += SchemaGenerator.PostgresExtensions; + Generating += SchemaGenerator.PostgreeTemporalTableScript; Generating += SchemaGenerator.CreateSchemasScript; Generating += SchemaGenerator.CreateTablesScript; Generating += SchemaGenerator.InsertEnumValuesScript; diff --git a/Signum.Engine/Schema/SchemaAssets.cs b/Signum.Engine/Schema/SchemaAssets.cs index 722a024d55..bec86cd647 100644 --- a/Signum.Engine/Schema/SchemaAssets.cs +++ b/Signum.Engine/Schema/SchemaAssets.cs @@ -2,6 +2,9 @@ using System.Linq; using Signum.Utilities; using Signum.Engine.SchemaInfoTables; +using System; +using Signum.Engine.PostgresCatalog; +using Signum.Engine.Engine; namespace Signum.Engine.Maps { @@ -53,7 +56,8 @@ public SqlPreCommandSimple AlterView() public View IncludeView(string viewName, string viewDefinition) { - return IncludeView(new ObjectName(SchemaName.Default, viewName), viewDefinition); + var isPostgres = Schema.Current.Settings.IsPostgres; + return IncludeView(new ObjectName(SchemaName.Default(isPostgres), viewName, isPostgres), viewDefinition); } public Dictionary Views = new Dictionary(); @@ -69,14 +73,30 @@ public View IncludeView(ObjectName viewName, string viewDefinition) SqlPreCommand? SyncViews(Replacements replacements) { + var isPostgres = Schema.Current.Settings.IsPostgres; var oldView = Schema.Current.DatabaseNames().SelectMany(db => { - using (Administrator.OverrideDatabaseInSysViews(db)) + if (isPostgres) { - return (from v in Database.View() - join s in Database.View() on v.schema_id equals s.schema_id - join m in Database.View() on v.object_id equals m.object_id - select KeyValuePair.Create(new ObjectName(new SchemaName(db, s.name), v.name), m.definition)).ToList(); + if (db != null) + throw new InvalidOperationException("Multi-database not supported in postgress"); + + return (from p in Database.View() + where p.relkind == RelKind.View + let ns = p.Namespace() + where !PostgresCatalogSchema.systemSchemas.Contains(ns.nspname) + let definition = PostgresFunctions.pg_get_viewdef(p.oid) + select KeyValuePair.Create(new ObjectName(new SchemaName(db, ns.nspname, isPostgres), p.relname, isPostgres), definition)).ToList(); + } + else + { + using (Administrator.OverrideDatabaseInSysViews(db)) + { + return (from v in Database.View() + join s in Database.View() on v.schema_id equals s.schema_id + join m in Database.View() on v.object_id equals m.object_id + select KeyValuePair.Create(new ObjectName(new SchemaName(db, s.name, isPostgres), v.name, isPostgres), m.definition)).ToList(); + } } }).ToDictionary(); @@ -95,7 +115,8 @@ join m in Database.View() on v.object_id equals m.object_id public Dictionary StoreProcedures = new Dictionary(); public Procedure IncludeStoreProcedure(string procedureName, string procedureCodeAndArguments) { - return IncludeStoreProcedure(new ObjectName(SchemaName.Default, procedureName), procedureCodeAndArguments); + var isPostgres = Schema.Current.Settings.IsPostgres; + return IncludeStoreProcedure(new ObjectName(SchemaName.Default(isPostgres), procedureName, isPostgres), procedureCodeAndArguments); } public Procedure IncludeStoreProcedure(ObjectName procedureName, string procedureCodeAndArguments) @@ -105,7 +126,8 @@ public Procedure IncludeStoreProcedure(ObjectName procedureName, string procedur public Procedure IncludeUserDefinedFunction(string functionName, string functionCodeAndArguments) { - return IncludeUserDefinedFunction(new ObjectName(SchemaName.Default, functionName), functionCodeAndArguments); + var isPostgres = Schema.Current.Settings.IsPostgres; + return IncludeUserDefinedFunction(new ObjectName(SchemaName.Default(isPostgres), functionName, isPostgres), functionCodeAndArguments); } public Procedure IncludeUserDefinedFunction(ObjectName functionName, string functionCodeAndArguments) @@ -120,15 +142,30 @@ public Procedure IncludeUserDefinedFunction(ObjectName functionName, string func SqlPreCommand? SyncProcedures(Replacements replacements) { + var isPostgres = Schema.Current.Settings.IsPostgres; var oldProcedures = Schema.Current.DatabaseNames().SelectMany(db => { - using (Administrator.OverrideDatabaseInSysViews(db)) + if (isPostgres) + { + if (db != null) + throw new InvalidOperationException("Multi-database not supported in postgress"); + + return (from v in Database.View() + let ns = v.Namespace() + where !PostgresCatalogSchema.systemSchemas.Contains(ns.nspname) + let definition = PostgresFunctions.pg_get_viewdef(v.oid) + select KeyValuePair.Create(new ObjectName(new SchemaName(db, ns.nspname, isPostgres), v.proname, isPostgres), definition)).ToList(); + } + else { - return (from p in Database.View() - join s in Database.View() on p.schema_id equals s.schema_id - where p.type == "P" || p.type == "IF" || p.type == "FN" - join m in Database.View() on p.object_id equals m.object_id - select KeyValuePair.Create(new ObjectName(new SchemaName(db, s.name), p.name), m.definition)).ToList(); + using (Administrator.OverrideDatabaseInSysViews(db)) + { + return (from p in Database.View() + join s in Database.View() on p.schema_id equals s.schema_id + where p.type == "P" || p.type == "IF" || p.type == "FN" + join m in Database.View() on p.object_id equals m.object_id + select KeyValuePair.Create(new ObjectName(new SchemaName(db, s.name, isPostgres), p.name, isPostgres), m.definition)).ToList(); + } } }).ToDictionary(); diff --git a/Signum.Engine/Schema/SchemaBuilder/SchemaBuilder.cs b/Signum.Engine/Schema/SchemaBuilder/SchemaBuilder.cs index 301785f54d..b437d402d6 100644 --- a/Signum.Engine/Schema/SchemaBuilder/SchemaBuilder.cs +++ b/Signum.Engine/Schema/SchemaBuilder/SchemaBuilder.cs @@ -38,8 +38,6 @@ public SchemaBuilder(bool isDefault) cleanName => schema.NameToType.TryGetC(cleanName)); FromEnumMethodExpander.miQuery = ReflectionTools.GetMethodInfo(() => Database.Query()).GetGenericMethodDefinition(); - Include() - .WithUniqueIndex(t => new { t.Namespace, t.ClassName }); } Settings.AssertNotIncluded = MixinDeclarations.AssertNotIncluded = t => @@ -304,8 +302,13 @@ void Complete(Table table) if (att == null) return null; - var tn = att.TemporalTableName != null ? ObjectName.Parse(att.TemporalTableName) : - new ObjectName(tableName.Schema, tableName.Name + "_History"); + var isPostgres = this.schema.Settings.IsPostgres; + + var tn = att.TemporalTableName != null ? ObjectName.Parse(att.TemporalTableName, isPostgres) : + new ObjectName(tableName.Schema, tableName.Name + "_History", isPostgres); + + if (isPostgres) + return new SystemVersionedInfo(tn, att.PostgreeSysPeriodColumname); return new SystemVersionedInfo(tn, att.StartDateColumnName, att.EndDateColumnName); } @@ -437,7 +440,7 @@ protected virtual Field GenerateField(ITable table, PropertyRoute route, NameSeq { using (HeavyProfiler.LogNoStackTrace("GenerateField", () => route.ToString())) { - KindOfField kof = GetKindOfField(route).ThrowIfNull(() => "Field {0} of type {1} has no database representation".FormatWith(route, route.Type.Name)); + KindOfField kof = GetKindOfField(route); if (kof == KindOfField.MList && inMList) throw new InvalidOperationException("Field {0} of type {1} can not be nested in another MList".FormatWith(route, route.Type.TypeName(), kof)); @@ -495,7 +498,7 @@ public enum KindOfField MList, } - protected virtual KindOfField? GetKindOfField(PropertyRoute route) + protected virtual KindOfField GetKindOfField(PropertyRoute route) { if (route.FieldInfo != null && ReflectionTools.FieldEquals(route.FieldInfo, fiId)) return KindOfField.PrimaryKey; @@ -503,7 +506,7 @@ public enum KindOfField if (route.FieldInfo != null && ReflectionTools.FieldEquals(route.FieldInfo, fiTicks)) return KindOfField.Ticks; - if (Settings.TryGetSqlDbType(Settings.FieldAttribute(route), route.Type) != null) + if (Settings.TryGetSqlDbType(Settings.FieldAttribute(route), route.Type) != null) return KindOfField.Value; if (route.Type.UnNullify().IsEnum) @@ -518,7 +521,13 @@ public enum KindOfField if (Reflector.IsMList(route.Type)) return KindOfField.MList; - return null; + if (Settings.IsPostgres && route.Type.IsArray) + { + if (Settings.TryGetSqlDbType(Settings.FieldAttribute(route), route.Type.ElementType()!) != null) + return KindOfField.Value; + } + + throw new InvalidOperationException($"Field {route} of type {route.Type.Name} has no database representation"); } protected virtual Field GenerateFieldPrimaryKey(Table table, PropertyRoute route, NameSequence name) @@ -527,14 +536,14 @@ protected virtual Field GenerateFieldPrimaryKey(Table table, PropertyRoute route PrimaryKey.PrimaryKeyType.SetDefinition(table.Type, attr.Type); - SqlDbTypePair pair = Settings.GetSqlDbType(attr, attr.Type); + DbTypePair pair = Settings.GetSqlDbType(attr, attr.Type); return table.PrimaryKey = new FieldPrimaryKey(route, table, attr.Name, attr.Type) { - SqlDbType = pair.SqlDbType, + DbType = pair.DbType, Collation = Settings.GetCollate(attr), UserDefinedTypeName = pair.UserDefinedTypeName, - Default = attr.Default, + Default = attr.GetDefault(Settings.IsPostgres), Identity = attr.Identity, }; } @@ -561,43 +570,45 @@ protected virtual FieldValue GenerateFieldTicks(Table table, PropertyRoute route Type type = ticksAttr?.Type ?? route.Type; - SqlDbTypePair pair = Settings.GetSqlDbType(ticksAttr, type); + DbTypePair pair = Settings.GetSqlDbType(ticksAttr, type); string ticksName = ticksAttr?.Name ?? name.ToString(); return table.Ticks = new FieldTicks(route, type, ticksName) { - SqlDbType = pair.SqlDbType, + DbType = pair.DbType, Collation = Settings.GetCollate(ticksAttr), UserDefinedTypeName = pair.UserDefinedTypeName, Nullable = IsNullable.No, - Size = Settings.GetSqlSize(ticksAttr, null, pair.SqlDbType), - Scale = Settings.GetSqlScale(ticksAttr, null, pair.SqlDbType), - Default = ticksAttr?.Default, + Size = Settings.GetSqlSize(ticksAttr, null, pair.DbType), + Scale = Settings.GetSqlScale(ticksAttr, null, pair.DbType), + Default = ticksAttr?.GetDefault(Settings.IsPostgres), }; } protected virtual FieldValue GenerateFieldValue(ITable table, PropertyRoute route, NameSequence name, bool forceNull) { - var att = Settings.FieldAttribute(route); + var att = Settings.FieldAttribute(route); - SqlDbTypePair pair = Settings.GetSqlDbType(att, route.Type); + DbTypePair pair = Settings.IsPostgres && route.Type.IsArray && route.Type != typeof(byte[]) ? + Settings.GetSqlDbType(att, route.Type.ElementType()!) : + Settings.GetSqlDbType(att, route.Type); return new FieldValue(route, null, name.ToString()) { - SqlDbType = pair.SqlDbType, + DbType = pair.DbType, Collation = Settings.GetCollate(att), UserDefinedTypeName = pair.UserDefinedTypeName, Nullable = Settings.GetIsNullable(route, forceNull), - Size = Settings.GetSqlSize(att, route, pair.SqlDbType), - Scale = Settings.GetSqlScale(att, route, pair.SqlDbType), - Default = att?.Default, + Size = Settings.GetSqlSize(att, route, pair.DbType), + Scale = Settings.GetSqlScale(att, route, pair.DbType), + Default = att?.GetDefault(Settings.IsPostgres), }.Do(f => f.UniqueIndex = f.GenerateUniqueIndex(table, Settings.FieldAttribute(route))); } protected virtual FieldEnum GenerateFieldEnum(ITable table, PropertyRoute route, NameSequence name, bool forceNull) { - var att = Settings.FieldAttribute(route); + var att = Settings.FieldAttribute(route); Type cleanEnum = route.Type.UnNullify(); @@ -608,7 +619,7 @@ protected virtual FieldEnum GenerateFieldEnum(ITable table, PropertyRoute route, Nullable = Settings.GetIsNullable(route, forceNull), IsLite = false, AvoidForeignKey = Settings.FieldAttribute(route) != null, - Default = att?.Default, + Default = att?.GetDefault(Settings.IsPostgres), }.Do(f => f.UniqueIndex = f.GenerateUniqueIndex(table, Settings.FieldAttribute(route))); } @@ -624,7 +635,7 @@ protected virtual FieldReference GenerateFieldReference(ITable table, PropertyRo IsLite = route.Type.IsLite(), AvoidForeignKey = Settings.FieldAttribute(route) != null, AvoidExpandOnRetrieving = Settings.FieldAttribute(route) != null, - Default = Settings.FieldAttribute(route)?.Default + Default = Settings.FieldAttribute(route)?.GetDefault(Settings.IsPostgres) }.Do(f => f.UniqueIndex = f.GenerateUniqueIndex(table, Settings.FieldAttribute(route))); } @@ -703,12 +714,12 @@ protected virtual FieldMList GenerateFieldMList(Table table, PropertyRoute route order = new FieldValue(route: null!, fieldType: typeof(int), orderAttr.Name ?? "Order") { - SqlDbType = pair.SqlDbType, + DbType = pair.DbType, Collation = Settings.GetCollate(orderAttr), UserDefinedTypeName = pair.UserDefinedTypeName, Nullable = IsNullable.No, - Size = Settings.GetSqlSize(orderAttr, null, pair.SqlDbType), - Scale = Settings.GetSqlScale(orderAttr, null, pair.SqlDbType), + Size = Settings.GetSqlSize(orderAttr, null, pair.DbType), + Scale = Settings.GetSqlScale(orderAttr, null, pair.DbType), }; } @@ -719,10 +730,10 @@ protected virtual FieldMList GenerateFieldMList(Table table, PropertyRoute route primaryKey = new TableMList.PrimaryKeyColumn(keyAttr.Type, keyAttr.Name) { - SqlDbType = pair.SqlDbType, + DbType = pair.DbType, Collation = Settings.GetCollate(orderAttr), UserDefinedTypeName = pair.UserDefinedTypeName, - Default = keyAttr.Default, + Default = keyAttr.GetDefault(Settings.IsPostgres), Identity = keyAttr.Identity, }; } @@ -796,26 +807,32 @@ protected static Type CleanType(Type type) public virtual ObjectName GenerateTableName(Type type, TableNameAttribute? tn) { - SchemaName sn = tn != null ? GetSchemaName(tn) : SchemaName.Default; + var isPostgres = Schema.Settings.IsPostgres; + + SchemaName sn = tn != null ? GetSchemaName(tn) : SchemaName.Default(isPostgres); string name = tn?.Name ?? EnumEntity.Extract(type)?.Name ?? Reflector.CleanTypeName(type); - return new ObjectName(sn, name); + return new ObjectName(sn, name, isPostgres); } private SchemaName GetSchemaName(TableNameAttribute tn) { - ServerName? server = tn.ServerName == null ? null : new ServerName(tn.ServerName); - DatabaseName? dataBase = tn.DatabaseName == null && server == null ? null : new DatabaseName(server, tn.DatabaseName!); - SchemaName schema = tn.SchemaName == null && dataBase == null ? SchemaName.Default : new SchemaName(dataBase, tn.SchemaName!); + var isPostgres = Schema.Settings.IsPostgres; + + ServerName? server = tn.ServerName == null ? null : new ServerName(tn.ServerName, isPostgres); + DatabaseName? dataBase = tn.DatabaseName == null && server == null ? null : new DatabaseName(server, tn.DatabaseName!, isPostgres); + SchemaName schema = tn.SchemaName == null && dataBase == null ? (tn.Name.StartsWith("#") && isPostgres ? null! : SchemaName.Default(isPostgres)) : new SchemaName(dataBase, tn.SchemaName!, isPostgres); return schema; } public virtual ObjectName GenerateTableNameCollection(Table table, NameSequence name, TableNameAttribute? tn) { - SchemaName sn = tn != null ? GetSchemaName(tn) : SchemaName.Default; + var isPostgres = Schema.Settings.IsPostgres; + + SchemaName sn = tn != null ? GetSchemaName(tn) : SchemaName.Default(isPostgres); - return new ObjectName(sn, tn?.Name ?? (table.Name.Name + name.ToString())); + return new ObjectName(sn, tn?.Name ?? (table.Name.Name + name.ToString()), isPostgres); } public virtual string GenerateMListFieldName(PropertyRoute route, KindOfField kindOfField) @@ -826,7 +843,7 @@ public virtual string GenerateMListFieldName(PropertyRoute route, KindOfField ki { case KindOfField.Value: case KindOfField.Embedded: - return type.Name.FirstUpper(); + return type.Name; case KindOfField.Enum: case KindOfField.Reference: return (EnumEntity.Extract(type)?.Name ?? Reflector.CleanTypeName(type)) + "ID"; @@ -838,7 +855,7 @@ public virtual string GenerateMListFieldName(PropertyRoute route, KindOfField ki public virtual string GenerateFieldName(PropertyRoute route, KindOfField kindOfField) { string name = route.PropertyInfo != null ? (route.PropertyInfo.Name.TryAfterLast('.') ?? route.PropertyInfo.Name) - : route.FieldInfo!.Name.FirstUpper(); + : route.FieldInfo!.Name; switch (kindOfField) { @@ -1024,7 +1041,7 @@ protected override FieldValue GenerateFieldValue(ITable table, PropertyRoute rou protected override FieldEnum GenerateFieldEnum(ITable table, PropertyRoute route, NameSequence name, bool forceNull) { - var att = Settings.FieldAttribute(route); + var att = Settings.FieldAttribute(route); Type cleanEnum = route.Type.UnNullify(); @@ -1035,7 +1052,7 @@ protected override FieldEnum GenerateFieldEnum(ITable table, PropertyRoute route Nullable = Settings.GetIsNullable(route, forceNull), IsLite = false, AvoidForeignKey = Settings.FieldAttribute(route) != null, - Default = att?.Default, + Default = att?.GetDefault(Settings.IsPostgres), }.Do(f => f.UniqueIndex = f.GenerateUniqueIndex(table, Settings.FieldAttribute(route))); } } diff --git a/Signum.Engine/Schema/SchemaBuilder/SchemaBuilderSettings.cs b/Signum.Engine/Schema/SchemaBuilder/SchemaSettings.cs similarity index 70% rename from Signum.Engine/Schema/SchemaBuilder/SchemaBuilderSettings.cs rename to Signum.Engine/Schema/SchemaBuilder/SchemaSettings.cs index 7c3c78ac17..66a28058ab 100644 --- a/Signum.Engine/Schema/SchemaBuilder/SchemaBuilderSettings.cs +++ b/Signum.Engine/Schema/SchemaBuilder/SchemaSettings.cs @@ -14,6 +14,7 @@ using Signum.Utilities.ExpressionTrees; using System.Runtime.CompilerServices; using Signum.Utilities.Reflection; +using NpgsqlTypes; namespace Signum.Engine.Maps { @@ -21,9 +22,11 @@ public class SchemaSettings { public SchemaSettings() { - } + public bool IsPostgres { get; set; } + public bool PostresVersioningFunctionNoChecks { get; set; } + public PrimaryKeyAttribute DefaultPrimaryKeyAttribute = new PrimaryKeyAttribute(typeof(int), "ID"); public int DefaultImplementedBySize = 40; @@ -36,6 +39,15 @@ public SchemaSettings() public ConcurrentDictionary TypeAttributesCache = new ConcurrentDictionary(); public Dictionary CustomOrder = new Dictionary(); + + internal Dictionary? desambiguatedNames; + public void Desambiguate(Type type, string cleanName) + { + if (desambiguatedNames == null) + desambiguatedNames = new Dictionary(); + + desambiguatedNames[type] = cleanName; + } public Dictionary UdtSqlName = new Dictionary() { @@ -44,31 +56,32 @@ public SchemaSettings() //{ typeof(SqlGeometry), "Geometry"}, }; - public Dictionary TypeValues = new Dictionary - { - {typeof(bool), SqlDbType.Bit}, - - {typeof(byte), SqlDbType.TinyInt}, - {typeof(short), SqlDbType.SmallInt}, - {typeof(int), SqlDbType.Int}, - {typeof(long), SqlDbType.BigInt}, - - {typeof(float), SqlDbType.Real}, - {typeof(double), SqlDbType.Float}, - {typeof(decimal), SqlDbType.Decimal}, - - {typeof(char), SqlDbType.NChar}, - {typeof(string), SqlDbType.NVarChar}, - {typeof(DateTime), SqlDbType.DateTime2}, - {typeof(DateTimeOffset), SqlDbType.DateTimeOffset}, - - {typeof(byte[]), SqlDbType.VarBinary}, - - {typeof(Guid), SqlDbType.UniqueIdentifier}, + public Dictionary TypeValues = new Dictionary + { + {typeof(bool), new AbstractDbType(SqlDbType.Bit, NpgsqlDbType.Boolean)}, + + {typeof(byte), new AbstractDbType(SqlDbType.TinyInt, NpgsqlDbType.Smallint)}, + {typeof(short), new AbstractDbType(SqlDbType.SmallInt, NpgsqlDbType.Smallint)}, + {typeof(int), new AbstractDbType(SqlDbType.Int, NpgsqlDbType.Integer)}, + {typeof(long), new AbstractDbType(SqlDbType.BigInt, NpgsqlDbType.Bigint)}, + + {typeof(float), new AbstractDbType(SqlDbType.Real, NpgsqlDbType.Real)}, + {typeof(double), new AbstractDbType(SqlDbType.Float, NpgsqlDbType.Double)}, + {typeof(decimal), new AbstractDbType(SqlDbType.Decimal, NpgsqlDbType.Numeric)}, + + {typeof(char), new AbstractDbType(SqlDbType.NChar, NpgsqlDbType.Char)}, + {typeof(string), new AbstractDbType(SqlDbType.NVarChar, NpgsqlDbType.Varchar)}, + {typeof(Date), new AbstractDbType(SqlDbType.Date, NpgsqlDbType.Date)}, + {typeof(DateTime), new AbstractDbType(SqlDbType.DateTime2, NpgsqlDbType.Timestamp)}, + {typeof(DateTimeOffset), new AbstractDbType(SqlDbType.DateTimeOffset, NpgsqlDbType.TimestampTz)}, + {typeof(TimeSpan), new AbstractDbType(SqlDbType.Time, NpgsqlDbType.Time)}, + + {typeof(byte[]), new AbstractDbType(SqlDbType.VarBinary, NpgsqlDbType.Bytea)}, + + {typeof(Guid), new AbstractDbType(SqlDbType.UniqueIdentifier, NpgsqlDbType.Uuid)}, }; - internal Dictionary? desambiguatedNames; - readonly Dictionary defaultSize = new Dictionary() + readonly Dictionary defaultSizeSqlServer = new Dictionary() { {SqlDbType.NVarChar, 200}, {SqlDbType.VarChar, 200}, @@ -79,11 +92,24 @@ public SchemaSettings() {SqlDbType.Decimal, 18}, }; - readonly Dictionary defaultScale = new Dictionary() + readonly Dictionary defaultSizePostgreSql = new Dictionary() + { + {NpgsqlDbType.Varbit, 200}, + {NpgsqlDbType.Varchar, 200}, + {NpgsqlDbType.Char, 1}, + {NpgsqlDbType.Numeric, 18}, + }; + + readonly Dictionary defaultScaleSqlServer = new Dictionary() { {SqlDbType.Decimal, 2}, }; + readonly Dictionary defaultScalePostgreSql = new Dictionary() + { + {NpgsqlDbType.Numeric, 2}, + }; + public AttributeCollection FieldAttributes(Expression> propertyRoute) where T : IRootEntity { @@ -268,58 +294,98 @@ public Implementations GetImplementations(PropertyRoute propertyRoute) FieldAttribute(propertyRoute)); } - internal SqlDbTypePair GetSqlDbType(SqlDbTypeAttribute? att, Type type) + public AbstractDbType ToAbstractDbType(DbTypeAttribute att) { - if (att != null && att.HasSqlDbType) - return new SqlDbTypePair(att.SqlDbType, att.UserDefinedTypeName); + if (att.HasNpgsqlDbType && att.HasSqlDbType) + return new AbstractDbType(att.SqlDbType, att.NpgsqlDbType); + + if (att.HasNpgsqlDbType) + return new AbstractDbType(att.NpgsqlDbType); + + if (att.HasNpgsqlDbType && att.HasSqlDbType) + return new AbstractDbType(att.SqlDbType, att.NpgsqlDbType); + + throw new InvalidOperationException("Not type found in DbTypeAttribute"); + } + + internal DbTypePair GetSqlDbType(DbTypeAttribute? att, Type type) + { + if (att != null && (att.HasSqlDbType || att.HasNpgsqlDbType)) + return new DbTypePair(ToAbstractDbType(att), att.UserDefinedTypeName); return GetSqlDbTypePair(type.UnNullify()); } - internal SqlDbTypePair? TryGetSqlDbType(SqlDbTypeAttribute? att, Type type) + internal DbTypePair? TryGetSqlDbType(DbTypeAttribute? att, Type type) { - if (att != null && att.HasSqlDbType) - return new SqlDbTypePair(att.SqlDbType, att.UserDefinedTypeName); + if (att != null && (att.HasSqlDbType || att.HasNpgsqlDbType)) + return new DbTypePair(ToAbstractDbType(att), att.UserDefinedTypeName); return TryGetSqlDbTypePair(type.UnNullify()); } - internal int? GetSqlSize(SqlDbTypeAttribute? att, PropertyRoute? route, SqlDbType sqlDbType) + internal int? GetSqlSize(DbTypeAttribute? att, PropertyRoute? route, AbstractDbType dbType) { + if (this.IsPostgres && dbType.PostgreSql == NpgsqlDbType.Bytea) + return null; + if (att != null && att.HasSize) return att.Size; - if(route != null && route.Type == typeof(string)) + if (route != null && route.Type == typeof(string)) { var sla = ValidatorAttribute(route); if (sla != null) return sla.Max == -1 ? int.MaxValue : sla.Max; } - return defaultSize.TryGetS(sqlDbType); + if (!this.IsPostgres) + return defaultSizeSqlServer.TryGetS(dbType.SqlServer); + else + return defaultSizePostgreSql.TryGetS(dbType.PostgreSql); + } + + internal int? GetSqlScale(DbTypeAttribute? att, PropertyRoute? route, AbstractDbType dbType) + { + bool isDecimal = dbType.IsDecimal(); + if (att != null && att.HasScale) + { + if (!isDecimal) + throw new InvalidOperationException($"{dbType} can not have Scale"); + } + + if(isDecimal && route != null) + { + var dv = ValidatorAttribute(route); + if (dv != null) + return dv.DecimalPlaces; + } + + if (!this.IsPostgres) + return defaultScaleSqlServer.TryGetS(dbType.SqlServer); + else + return defaultScalePostgreSql.TryGetS(dbType.PostgreSql); } - internal int? GetSqlScale(SqlDbTypeAttribute? att, PropertyRoute? route, SqlDbType sqlDbType) + internal int? GetSqlScale(DbTypeAttribute? att, PropertyRoute? route, NpgsqlDbType npgsqlDbType) { if (att != null && att.HasScale) { - if(sqlDbType != SqlDbType.Decimal) - throw new InvalidOperationException($"{sqlDbType} can not have Scale"); return att.Scale; } - if(sqlDbType == SqlDbType.Decimal && route != null) + if (npgsqlDbType == NpgsqlDbType.Numeric && route != null) { var dv = ValidatorAttribute(route); if (dv != null) return dv.DecimalPlaces; } - return defaultScale.TryGetS(sqlDbType); + return defaultScalePostgreSql.TryGetS(npgsqlDbType); } - internal string? GetCollate(SqlDbTypeAttribute? att) + internal string? GetCollate(DbTypeAttribute? att) { if (att != null && att.Collation != null) return att.Collation; @@ -327,20 +393,12 @@ internal SqlDbTypePair GetSqlDbType(SqlDbTypeAttribute? att, Type type) return null; } - internal SqlDbType DefaultSqlType(Type type) + internal AbstractDbType DefaultSqlType(Type type) { return this.TypeValues.GetOrThrow(type, "Type {0} not registered"); } - public void Desambiguate(Type type, string cleanName) - { - if (desambiguatedNames == null) - desambiguatedNames = new Dictionary(); - - desambiguatedNames[type] = cleanName; - } - - public SqlDbTypePair GetSqlDbTypePair(Type type) + public DbTypePair GetSqlDbTypePair(Type type) { var result = TryGetSqlDbTypePair(type); if (result == null) @@ -349,14 +407,14 @@ public SqlDbTypePair GetSqlDbTypePair(Type type) return result; } - public SqlDbTypePair? TryGetSqlDbTypePair(Type type) + public DbTypePair? TryGetSqlDbTypePair(Type type) { - if (TypeValues.TryGetValue(type, out SqlDbType result)) - return new SqlDbTypePair(result, null); + if (TypeValues.TryGetValue(type, out AbstractDbType result)) + return new DbTypePair(result, null); string? udtTypeName = GetUdtName(type); if (udtTypeName != null) - return new SqlDbTypePair(SqlDbType.Udt, udtTypeName); + return new DbTypePair(new AbstractDbType(SqlDbType.Udt), udtTypeName); return null; } @@ -382,14 +440,16 @@ public void RegisterCustomOrder(Expression> customOrder) wher } } - public class SqlDbTypePair + + + public class DbTypePair { - public SqlDbType SqlDbType { get; private set; } + public AbstractDbType DbType { get; private set; } public string? UserDefinedTypeName { get; private set; } - public SqlDbTypePair(SqlDbType type, string? udtTypeName) + public DbTypePair(AbstractDbType type, string? udtTypeName) { - this.SqlDbType = type; + this.DbType = type; this.UserDefinedTypeName = udtTypeName; } } diff --git a/Signum.Engine/Schema/UniqueTableIndex.cs b/Signum.Engine/Schema/UniqueTableIndex.cs index 57de2580d1..ecaf3cbd3d 100644 --- a/Signum.Engine/Schema/UniqueTableIndex.cs +++ b/Signum.Engine/Schema/UniqueTableIndex.cs @@ -43,20 +43,33 @@ public TableIndex(ITable table, params IColumn[] columns) this.Columns = columns; } - public virtual string IndexName + public virtual string GetIndexName(ObjectName tableName) { - get { return "IX_{0}".FormatWith(ColumnSignature()).TryStart(Connector.Current.MaxNameLength); } + int maxLength = MaxNameLength(); + + return StringHashEncoder.ChopHash("IX_{0}_{1}".FormatWith(tableName.Name, ColumnSignature()), maxLength) + WhereSignature(); + } + + protected static int MaxNameLength() + { + return Connector.Current.MaxNameLength - StringHashEncoder.HashSize - 1; } + public string IndexName => GetIndexName(Table.Name); + protected string ColumnSignature() { - string columns = Columns.ToString(c => c.Name, "_"); + return Columns.ToString(c => c.Name, "_"); + } + + protected string? WhereSignature() + { var includeColumns = IncludeColumns.HasItems() ? IncludeColumns.ToString(c => c.Name, "_") : null; - if (string.IsNullOrEmpty(Where) && includeColumns == null) - return columns; + if (string.IsNullOrEmpty(Where) && includeColumns == null) + return null; - return columns + "__" + StringHashEncoder.Codify(Where + includeColumns); + return "__" + StringHashEncoder.Codify(Where + includeColumns); } public override string ToString() @@ -70,14 +83,14 @@ public string HintText() } } - public class PrimaryClusteredIndex : TableIndex + public class PrimaryKeyIndex : TableIndex { - public PrimaryClusteredIndex(ITable table) : base(table, new[] { table.PrimaryKey }) + public PrimaryKeyIndex(ITable table) : base(table, new[] { table.PrimaryKey }) { } - public override string IndexName => GetPrimaryKeyName(this.Table.Name); + public override string GetIndexName(ObjectName tableName) => GetPrimaryKeyName(tableName); public static string GetPrimaryKeyName(ObjectName tableName) { @@ -87,12 +100,17 @@ public static string GetPrimaryKeyName(ObjectName tableName) public class UniqueTableIndex : TableIndex { - public UniqueTableIndex(ITable table, IColumn[] columns) : base(table, columns) { } + public UniqueTableIndex(ITable table, IColumn[] columns) + : base(table, columns) + { + } - public override string IndexName + public override string GetIndexName(ObjectName tableName) { - get { return "UIX_{0}".FormatWith(ColumnSignature()).TryStart(Connector.Current.MaxNameLength); } + var maxSize = MaxNameLength(); + + return StringHashEncoder.ChopHash("UIX_{0}_{1}".FormatWith(tableName.Name, ColumnSignature()), maxSize) + WhereSignature(); } public string? ViewName @@ -105,7 +123,9 @@ public string? ViewName if (Connector.Current.AllowsIndexWithWhere(Where)) return null; - return "VIX_{0}_{1}".FormatWith(Table.Name.Name, ColumnSignature()).TryStart(Connector.Current.MaxNameLength); + var maxSize = MaxNameLength(); + + return StringHashEncoder.ChopHash("VIX_{0}_{1}".FormatWith(Table.Name.Name, ColumnSignature()), maxSize) + WhereSignature(); } } @@ -184,10 +204,12 @@ public class IndexWhereExpressionVisitor : ExpressionVisitor StringBuilder sb = new StringBuilder(); IFieldFinder RootFinder; + bool isPostgres; public IndexWhereExpressionVisitor(IFieldFinder rootFinder) { RootFinder = rootFinder; + this.isPostgres = Schema.Current.Settings.IsPostgres; } public static string GetIndexWhere(LambdaExpression lambda, IFieldFinder rootFiender) @@ -240,7 +262,7 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression b) if (f is FieldReference fr) { if (b.TypeOperand.IsAssignableFrom(fr.FieldType)) - sb.Append(fr.Name.SqlEscape() + " IS NOT NULL"); + sb.Append(fr.Name.SqlEscape(isPostgres) + " IS NOT NULL"); else throw new InvalidOperationException("A {0} will never be {1}".FormatWith(fr.FieldType.TypeName(), b.TypeOperand.TypeName())); @@ -254,7 +276,7 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression b) var imp = fib.ImplementationColumns.Where(kvp => typeOperant.IsAssignableFrom(kvp.Key)); if (imp.Any()) - sb.Append(imp.ToString(kvp => kvp.Value.Name.SqlEscape() + " IS NOT NULL", " OR ")); + sb.Append(imp.ToString(kvp => kvp.Value.Name.SqlEscape(isPostgres) + " IS NOT NULL", " OR ")); else throw new InvalidOperationException("No implementation ({0}) will never be {1}".FormatWith(fib.ImplementationColumns.Keys.ToString(t => t.TypeName(), ", "), b.TypeOperand.TypeName())); @@ -276,7 +298,7 @@ protected override Expression VisitMember(MemberExpression m) { var field = GetField(m); - sb.Append(Equals(field, true, true)); + sb.Append(Equals(field, value: true, equals: true, isPostgres)); return m; } @@ -308,52 +330,52 @@ protected override Expression VisitUnary(UnaryExpression u) } - public static string IsNull(Field field, bool equals) + public static string IsNull(Field field, bool equals, bool isPostgres) { string isNull = equals ? "{0} IS NULL" : "{0} IS NOT NULL"; if (field is IColumn col) { - string result = isNull.FormatWith(col.Name.SqlEscape()); + string result = isNull.FormatWith(col.Name.SqlEscape(isPostgres)); - if (!SqlBuilder.IsString(col.SqlDbType)) + if (!col.DbType.IsString()) return result; - return result + (equals ? " OR " : " AND ") + (col.Name.SqlEscape() + (equals ? " = " : " <> ") + "''"); + return result + (equals ? " OR " : " AND ") + (col.Name.SqlEscape(isPostgres) + (equals ? " = " : " <> ") + "''"); } else if (field is FieldImplementedBy ib) { - return ib.ImplementationColumns.Values.Select(ic => isNull.FormatWith(ic.Name.SqlEscape())).ToString(equals ? " AND " : " OR "); + return ib.ImplementationColumns.Values.Select(ic => isNull.FormatWith(ic.Name.SqlEscape(isPostgres))).ToString(equals ? " AND " : " OR "); } else if (field is FieldImplementedByAll iba) { - return isNull.FormatWith(iba.Column.Name.SqlEscape()) + + return isNull.FormatWith(iba.Column.Name.SqlEscape(isPostgres)) + (equals ? " AND " : " OR ") + - isNull.FormatWith(iba.ColumnType.Name.SqlEscape()); + isNull.FormatWith(iba.ColumnType.Name.SqlEscape(isPostgres)); } else if (field is FieldEmbedded fe) { if (fe.HasValue == null) throw new NotSupportedException("{0} is not nullable".FormatWith(field)); - return fe.HasValue.Name.SqlEscape() + " = 1"; + return fe.HasValue.Name.SqlEscape(isPostgres) + " = 1"; } throw new NotSupportedException(isNull.FormatWith(field.GetType())); } - static string Equals(Field field, object value, bool equals) + static string Equals(Field field, object value, bool equals, bool isPostgres) { if (value == null) { - return IsNull(field, equals); + return IsNull(field, equals, isPostgres); } else { if (field is IColumn) { - return ((IColumn)field).Name.SqlEscape() + + return ((IColumn)field).Name.SqlEscape(isPostgres) + (equals ? " = " : " <> ") + SqlPreCommandSimple.Encode(value); } @@ -380,13 +402,13 @@ protected override Expression VisitBinary(BinaryExpression b) Field field = GetField(b.Right); - sb.Append(Equals(field, ((ConstantExpression)b.Left).Value, b.NodeType == ExpressionType.Equal)); + sb.Append(Equals(field, ((ConstantExpression)b.Left).Value, b.NodeType == ExpressionType.Equal, isPostgres)); } else if (b.Right is ConstantExpression) { Field field = GetField(b.Left); - sb.Append(Equals(field, ((ConstantExpression)b.Right).Value, b.NodeType == ExpressionType.Equal)); + sb.Append(Equals(field, ((ConstantExpression)b.Right).Value, b.NodeType == ExpressionType.Equal, isPostgres)); } else throw new NotSupportedException("Impossible to translate {0}".FormatWith(b.ToString())); diff --git a/Signum.Engine/Signum.Engine.csproj b/Signum.Engine/Signum.Engine.csproj index 7a7c2c69a4..8d9d72e2db 100644 --- a/Signum.Engine/Signum.Engine.csproj +++ b/Signum.Engine/Signum.Engine.csproj @@ -2,7 +2,6 @@ netcoreapp3.1 - preview true enable true @@ -11,11 +10,17 @@ - - + + + + + + + + diff --git a/Signum.Entities/Basics/Exception.cs b/Signum.Entities/Basics/Exception.cs index e9eb922111..0952421c49 100644 --- a/Signum.Entities/Basics/Exception.cs +++ b/Signum.Entities/Basics/Exception.cs @@ -29,10 +29,10 @@ public ExceptionEntity(Exception ex) public DateTime CreationDate { get; private set; } = TimeZoneManager.Now; - [ForceNotNullable, SqlDbType(Size = 100)] + [ForceNotNullable, DbType(Size = 100)] public string? ExceptionType { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] string exceptionMessage; public string ExceptionMessage { @@ -46,7 +46,7 @@ public string ExceptionMessage public int ExceptionMessageHash { get; private set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] string stackTrace; public string StackTrace { @@ -64,49 +64,49 @@ public string StackTrace public Lite? User { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? Environment { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? Version { get; set; } - [SqlDbType(Size = 300)] + [DbType(Size = 300)] public string? UserAgent { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] public string? RequestUrl { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? ControllerName { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? ActionName { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] public string? UrlReferer { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? MachineName { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? ApplicationName { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? UserHostAddress { get; set; } - [SqlDbType(Size = 100)] + [DbType(Size = 100)] public string? UserHostName { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] public string? Form { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] public string? QueryString { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] public string? Session { get; set; } - [SqlDbType(Size = int.MaxValue)] + [DbType(Size = int.MaxValue)] public string? Data { get; set; } public int HResult { get; internal set; } diff --git a/Signum.Entities/Entity.cs b/Signum.Entities/Entity.cs index dab8bf4f06..187554e7da 100644 --- a/Signum.Entities/Entity.cs +++ b/Signum.Entities/Entity.cs @@ -21,7 +21,7 @@ public abstract class Entity : ModifiableEntity, IEntity internal PrimaryKey? id; - [Ignore, DebuggerBrowsable(DebuggerBrowsableState.Never)] + [Ignore, DebuggerBrowsable(DebuggerBrowsableState.Never), ColumnName("ToStr")] protected internal string? toStr; //for queries and lites on entities with non-expression ToString [HiddenProperty, Description("Id")] diff --git a/Signum.Entities/FieldAttributes.cs b/Signum.Entities/FieldAttributes.cs index 63051ce840..037b0a4978 100644 --- a/Signum.Entities/FieldAttributes.cs +++ b/Signum.Entities/FieldAttributes.cs @@ -7,6 +7,7 @@ using Signum.Entities.Reflection; using Signum.Utilities.ExpressionTrees; using Signum.Entities.Basics; +using NpgsqlTypes; namespace Signum.Entities { @@ -242,57 +243,63 @@ public sealed class ForceNullableAttribute: Attribute [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] - public class SqlDbTypeAttribute : Attribute - { + public class DbTypeAttribute : Attribute + { SqlDbType? sqlDbType; - int? size; - int? scale; - + public bool HasSqlDbType => sqlDbType.HasValue; public SqlDbType SqlDbType { get { return sqlDbType!.Value; } set { sqlDbType = value; } } - public bool HasSqlDbType + NpgsqlDbType? npgsqlDbType; + public bool HasNpgsqlDbType => npgsqlDbType.HasValue; + public NpgsqlDbType NpgsqlDbType { - get { return sqlDbType.HasValue; } + get { return npgsqlDbType!.Value; } + set { npgsqlDbType = value; } } + int? size; + public bool HasSize => size.HasValue; public int Size { get { return size!.Value; } set { size = value; } } - public bool HasSize - { - get { return size.HasValue; } - } + int? scale; + public bool HasScale => scale.HasValue; public int Scale { get { return scale!.Value; } set { scale = value; } } - public bool HasScale - { - get { return scale.HasValue; } - } public string? UserDefinedTypeName { get; set; } public string? Default { get; set; } + public string? DefaultSqlServer { get; set; } + public string? DefaultPostgres { get; set; } + + public string? GetDefault(bool isPostgres) + { + return (isPostgres ? DefaultPostgres : DefaultSqlServer) ?? Default; + } + public string? Collation { get; set; } - public const string NewId = "NEWID()"; - public const string NewSequentialId = "NEWSEQUENTIALID()"; + public const string SqlServer_NewId = "NEWID()"; + public const string SqlServer_NewSequentialId = "NEWSEQUENTIALID()"; + public const string Postgres_UuidGenerateV1= "uuid_generate_v1()"; } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Field | AttributeTargets.Property /*MList fields*/, Inherited = true, AllowMultiple = false)] - public sealed class PrimaryKeyAttribute : SqlDbTypeAttribute + public sealed class PrimaryKeyAttribute : DbTypeAttribute { public Type Type { get; set; } @@ -309,9 +316,10 @@ public bool IdentityBehaviour set { identityBehaviour = value; - if (Type == typeof(Guid)) + if (Type == typeof(Guid) && identityBehaviour) { - this.Default = identityBehaviour ? NewId : null; + this.DefaultSqlServer = SqlServer_NewId; + this.DefaultPostgres = Postgres_UuidGenerateV1; } } } @@ -371,7 +379,7 @@ public TableNameAttribute(string fullName) } [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] - public sealed class TicksColumnAttribute : SqlDbTypeAttribute + public sealed class TicksColumnAttribute : DbTypeAttribute { public bool HasTicks { get; private set; } @@ -394,6 +402,7 @@ public sealed class SystemVersionedAttribute : Attribute public string? TemporalTableName { get; set; } public string StartDateColumnName { get; set; } = "SysStartDate"; public string EndDateColumnName { get; set; } = "SysEndDate"; + public string PostgreeSysPeriodColumname { get; set; } = "sys_period"; } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] diff --git a/Signum.Entities/MList.cs b/Signum.Entities/MList.cs index d00f202464..404462ff43 100644 --- a/Signum.Entities/MList.cs +++ b/Signum.Entities/MList.cs @@ -858,7 +858,7 @@ public static MList ToMList(this IEnumerable collection) } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class PreserveOrderAttribute : SqlDbTypeAttribute + public sealed class PreserveOrderAttribute : DbTypeAttribute { public string? Name { get; set; } diff --git a/Signum.Entities/Reflection/StringHashEncoder.cs b/Signum.Entities/Reflection/StringHashEncoder.cs index f6660cfc0e..67a83abedd 100644 --- a/Signum.Entities/Reflection/StringHashEncoder.cs +++ b/Signum.Entities/Reflection/StringHashEncoder.cs @@ -1,9 +1,18 @@ -using System.Text; +using System.Text; namespace Signum.Entities.Reflection { public static class StringHashEncoder { + public const int HashSize = 7; + public static string ChopHash(string str, int maxLength) + { + if (str.Length > maxLength) + return str.Substring(0, maxLength - HashSize) + Codify(str.Substring(maxLength - HashSize)); + + return str; + } + static readonly string letters = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; public static string Codify(string str) diff --git a/Signum.Entities/Signum.Entities.csproj b/Signum.Entities/Signum.Entities.csproj index fe5c9675b1..db445a664a 100644 --- a/Signum.Entities/Signum.Entities.csproj +++ b/Signum.Entities/Signum.Entities.csproj @@ -2,7 +2,6 @@ netcoreapp3.1 - preview true enable true @@ -10,17 +9,23 @@ NU1605 - - + - + + + PreserveNewest diff --git a/Signum.Entities/SystemTime.cs b/Signum.Entities/SystemTime.cs index e3a1993ec8..c6907035cb 100644 --- a/Signum.Entities/SystemTime.cs +++ b/Signum.Entities/SystemTime.cs @@ -12,7 +12,7 @@ public abstract class SystemTime static Variable currentVariable = Statics.ThreadVariable("systemTime"); public static SystemTime? Current => currentVariable.Value; - + public static IDisposable Override(DateTime asOf) => Override(new SystemTime.AsOf(asOf)); public static IDisposable Override(SystemTime? systemTime) { @@ -48,22 +48,8 @@ public AsOf(DateTime dateTime) public abstract class Interval : SystemTime { - - - } - public class FromTo : Interval - { - public DateTime StartDateTime { get; private set; } - public DateTime EndtDateTime { get; private set; } - public FromTo(DateTime startDateTime, DateTime endDateTime) - { - this.StartDateTime = ValidateUTC(startDateTime); - this.EndtDateTime = ValidateUTC(endDateTime); - } - - public override string ToString() => $"FROM {StartDateTime:u} TO {EndtDateTime:u}"; } public class Between : Interval @@ -116,55 +102,5 @@ public static Interval SystemPeriod(this MListElement mlis { throw new InvalidOperationException("Only for queries"); } - - - static MethodInfo miOverlaps = ReflectionTools.GetMethodInfo((Interval pair) => pair.Overlaps(new Interval())); - internal static Expression? Overlaps(this NewExpression? interval1, NewExpression? interval2) - { - if (interval1 == null) - return null; - - if (interval2 == null) - return null; - - var min1 = interval1.Arguments[0]; - var max1 = interval1.Arguments[1]; - var min2 = interval2.Arguments[0]; - var max2 = interval2.Arguments[1]; - - return Expression.And( - Expression.GreaterThan(max1, min2), - Expression.GreaterThan(max2, min1) - ); - } - - - - static ConstructorInfo ciInterval = ReflectionTools.GetConstuctorInfo(() => new Interval(new DateTime(), new DateTime())); - internal static Expression? Intesection(this NewExpression? interval1, NewExpression? interval2) - { - if (interval1 == null) - return interval2; - - if (interval2 == null) - return interval1; - - var min1 = interval1.Arguments[0]; - var max1 = interval1.Arguments[1]; - var min2 = interval2.Arguments[0]; - var max2 = interval2.Arguments[1]; - - return Expression.New(ciInterval, - Expression.Condition(Expression.LessThan(min1, min2), min1, min2), - Expression.Condition(Expression.GreaterThan(max1, max2), max1, max2)); - } - - public static Expression And(this Expression expression, Expression? other) - { - if (other == null) - return expression; - - return Expression.And(expression, other); - } } } diff --git a/Signum.MSBuildTask/Program.cs b/Signum.MSBuildTask/Program.cs index 40797b3468..d80af683cb 100644 --- a/Signum.MSBuildTask/Program.cs +++ b/Signum.MSBuildTask/Program.cs @@ -51,8 +51,6 @@ public static int Main(string[] args) return 0; } - log.WriteLine("Signum.MSBuildTask doing nothing"); - bool errors = false; errors |= new ExpressionFieldGenerator(assembly, resolver, log).FixAutoExpressionField(); errors |= new FieldAutoInitializer(assembly, resolver, log).FixAutoInitializer(); diff --git a/Signum.MSBuildTask/Properties/launchSettings.json b/Signum.MSBuildTask/Properties/launchSettings.json index a1cc64f38b..4ff5492d03 100644 --- a/Signum.MSBuildTask/Properties/launchSettings.json +++ b/Signum.MSBuildTask/Properties/launchSettings.json @@ -2,8 +2,8 @@ "profiles": { "Signum.Entities": { "commandName": "Project", - "commandLineArgs": "\"obj\\x64\\Debug\\netcoreapp2.2\\Signum.Entities.Extensions.dll\" \"D:\\Signum\\southwind\\Extensions\\Signum.Entities.Extensions\\obj\\SignumReferences.txt\"", - "workingDirectory": "D:\\Signum\\southwind\\Extensions\\Signum.Entities.Extensions\\" + "commandLineArgs": "\"obj\\x64\\Debug\\netcoreapp3.1\\Signum.Entities.dll\" \"D:\\Signum\\southwind\\Framework\\Signum.Entities\\obj\\SignumReferences.txt\"", + "workingDirectory": "D:\\Signum\\southwind\\Framework\\Signum.Entities\\" } } } diff --git a/Signum.MSBuildTask/Signum.MSBuildTask.csproj b/Signum.MSBuildTask/Signum.MSBuildTask.csproj index e811125ed2..fa6b1020e4 100644 --- a/Signum.MSBuildTask/Signum.MSBuildTask.csproj +++ b/Signum.MSBuildTask/Signum.MSBuildTask.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.1 @@ -10,11 +10,11 @@ latest x64;AnyCPU Signum.MSBuildTask.nuspec - 1.0.7 + 1.1.4 - + - \ No newline at end of file + diff --git a/Signum.MSBuildTask/Signum.MSBuildTask.nuspec b/Signum.MSBuildTask/Signum.MSBuildTask.nuspec index 1e51edb86a..c9bc462d14 100644 --- a/Signum.MSBuildTask/Signum.MSBuildTask.nuspec +++ b/Signum.MSBuildTask/Signum.MSBuildTask.nuspec @@ -2,7 +2,7 @@ Signum.MSBuildTask - 1.0.7 + 1.1.4 IL rewriter for Signum Framework applications Olmo del Corral MIT diff --git a/Signum.MSBuildTask/Signum.MSBuildTask.targets b/Signum.MSBuildTask/Signum.MSBuildTask.targets index 35aba2376c..fd12a43e31 100644 --- a/Signum.MSBuildTask/Signum.MSBuildTask.targets +++ b/Signum.MSBuildTask/Signum.MSBuildTask.targets @@ -1,8 +1,8 @@  - + - \ No newline at end of file + diff --git a/Signum.React/ApiControllers/QueryController.cs b/Signum.React/ApiControllers/QueryController.cs index 4f23a43b38..58c2368bfd 100644 --- a/Signum.React/ApiControllers/QueryController.cs +++ b/Signum.React/ApiControllers/QueryController.cs @@ -393,15 +393,9 @@ public SystemTimeTS(SystemTime systemTime) startDate = between.StartDateTime; endDate = between.EndtDateTime; } - else if (systemTime is SystemTime.FromTo fromTo) - { - mode = SystemTimeMode.Between; //Same - startDate = fromTo.StartDateTime; - endDate = fromTo.EndtDateTime; - } else if (systemTime is SystemTime.ContainedIn containedIn) { - mode = SystemTimeMode.Between; //Same + mode = SystemTimeMode.ContainedIn; startDate = containedIn.StartDateTime; endDate = containedIn.EndtDateTime; } diff --git a/Signum.React/Signum.React.csproj b/Signum.React/Signum.React.csproj index d24a535cb1..32b2b93f8d 100644 --- a/Signum.React/Signum.React.csproj +++ b/Signum.React/Signum.React.csproj @@ -4,7 +4,6 @@ netcoreapp3.1 3.7 true - preview true enable true @@ -12,6 +11,7 @@ x64;x86;win86;win64;AnyCPU;linux64 NU1605 + 7b3bfbd4-af24-4a37-a213-f5f281fa39f5 @@ -34,23 +34,22 @@ - + runtime; build; native; contentfiles; analyzers all + - - - - + - + + @@ -67,6 +66,7 @@ + diff --git a/Signum.TSGenerator/Program.cs b/Signum.TSGenerator/Program.cs index 1b92311583..0d4b280f3c 100644 --- a/Signum.TSGenerator/Program.cs +++ b/Signum.TSGenerator/Program.cs @@ -13,8 +13,6 @@ namespace Signum.TSGenerator { public static class Program { - - public static int Main(string[] args) { var log = Console.Out; diff --git a/Signum.TSGenerator/Signum.TSGenerator.2.2.2.nupkg b/Signum.TSGenerator/Signum.TSGenerator.2.2.2.nupkg deleted file mode 100644 index 71408b517a..0000000000 Binary files a/Signum.TSGenerator/Signum.TSGenerator.2.2.2.nupkg and /dev/null differ diff --git a/Signum.TSGenerator/Signum.TSGenerator.csproj b/Signum.TSGenerator/Signum.TSGenerator.csproj index 0fb225f910..4ea51f8d40 100644 --- a/Signum.TSGenerator/Signum.TSGenerator.csproj +++ b/Signum.TSGenerator/Signum.TSGenerator.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.1 @@ -9,7 +9,7 @@ true latest x64;AnyCPU - 2.2.2 + 2.2.5 @@ -17,6 +17,6 @@ - + diff --git a/Signum.TSGenerator/Signum.TSGenerator.nuspec b/Signum.TSGenerator/Signum.TSGenerator.nuspec index 2af3932246..cda81921e4 100644 --- a/Signum.TSGenerator/Signum.TSGenerator.nuspec +++ b/Signum.TSGenerator/Signum.TSGenerator.nuspec @@ -2,7 +2,7 @@ Signum.TSGenerator - 2.2.2 + 2.2.5 TypeScript generator for Signum Framework applications Olmo del Corral MIT diff --git a/Signum.Test/Environment/Entities.cs b/Signum.Test/Environment/Entities.cs index 9760de714d..686818839b 100644 --- a/Signum.Test/Environment/Entities.cs +++ b/Signum.Test/Environment/Entities.cs @@ -361,14 +361,14 @@ public class EmbeddedConfigEmbedded : EmbeddedEntity public static class MinimumExtensions { - [SqlMethod(Name = "dbo.MinimumTableValued")] + [SqlMethod(Name = "MinimumTableValued")] public static IQueryable MinimumTableValued(int? a, int? b) { throw new InvalidOperationException("sql only"); } - [SqlMethod(Name = "dbo.MinimumScalar")] + [SqlMethod(Name = "MinimumScalar")] public static int? MinimumScalar(int? a, int? b) { throw new InvalidOperationException("sql only"); @@ -376,19 +376,42 @@ public static IQueryable MinimumTableValued(int? a, int? b) internal static void IncludeFunction(SchemaAssets assets) { - assets.IncludeUserDefinedFunction("MinimumTableValued", @"(@Param1 Integer, @Param2 Integer) + if (Schema.Current.Settings.IsPostgres) + { + assets.IncludeUserDefinedFunction("MinimumTableValued", @"(p1 integer, p2 integer) +RETURNS TABLE(""MinValue"" integer) +AS $$ +BEGIN + RETURN QUERY + SELECT Case When p1 < p2 Then p1 + Else COALESCE(p2, p1) End as MinValue; + END +$$ LANGUAGE plpgsql;"); + + assets.IncludeUserDefinedFunction("MinimumScalar", @"(p1 integer, p2 integer) +RETURNS integer +AS $$ +BEGIN + RETURN (Case When p1 < p2 Then p1 + Else COALESCE(p2, p1) End); +END +$$ LANGUAGE plpgsql;"); + } + else + { + assets.IncludeUserDefinedFunction("MinimumTableValued", @"(@Param1 Integer, @Param2 Integer) RETURNS Table As RETURN (SELECT Case When @Param1 < @Param2 Then @Param1 Else COALESCE(@Param2, @Param1) End MinValue)"); - - assets.IncludeUserDefinedFunction("MinimumScalar", @"(@Param1 Integer, @Param2 Integer) + assets.IncludeUserDefinedFunction("MinimumScalar", @"(@Param1 Integer, @Param2 Integer) RETURNS Integer AS BEGIN RETURN (Case When @Param1 < @Param2 Then @Param1 Else COALESCE(@Param2, @Param1) End); END"); + } } } diff --git a/Signum.Test/Environment/MusicStarter.cs b/Signum.Test/Environment/MusicStarter.cs index a525fe95c2..1844dd97de 100644 --- a/Signum.Test/Environment/MusicStarter.cs +++ b/Signum.Test/Environment/MusicStarter.cs @@ -47,11 +47,16 @@ public static void Start(string connectionString) { SchemaBuilder sb = new SchemaBuilder(true); - //Connector.Default = new SqlCeConnector(@"Data Source=C:\BaseDatos.sdf", sb.Schema); - - var sqlVersion = SqlServerVersionDetector.Detect(connectionString); - - Connector.Default = new SqlConnector(connectionString, sb.Schema, sqlVersion ?? SqlServerVersion.SqlServer2017); + if (connectionString.Contains("Data Source")) + { + var sqlVersion = SqlServerVersionDetector.Detect(connectionString); + Connector.Default = new SqlConnector(connectionString, sb.Schema, sqlVersion ?? SqlServerVersion.SqlServer2017); + } + else + { + var postgreeVersion = PostgresVersionDetector.Detect(connectionString); + Connector.Default = new PostgreSqlConnector(connectionString, sb.Schema, postgreeVersion); + } sb.Schema.Version = typeof(MusicStarter).Assembly.GetName().Version!; @@ -63,13 +68,7 @@ public static void Start(string connectionString) sb.Schema.Settings.TypeAttributes().Add(new SystemVersionedAttribute()); } - if (!Schema.Current.Settings.TypeValues.ContainsKey(typeof(TimeSpan))) - { - sb.Settings.FieldAttributes((AlbumEntity a) => a.Songs[0].Duration).Add(new Signum.Entities.IgnoreAttribute()); - sb.Settings.FieldAttributes((AlbumEntity a) => a.BonusTrack!.Duration).Add(new Signum.Entities.IgnoreAttribute()); - } - - if(sqlVersion > SqlServerVersion.SqlServer2008) + if(Connector.Default is SqlConnector c && c.Version > SqlServerVersion.SqlServer2008) { sb.Settings.UdtSqlName.Add(typeof(SqlHierarchyId), "HierarchyId"); } diff --git a/Signum.Test/LinqProvider/SelectTest.cs b/Signum.Test/LinqProvider/SelectTest.cs index 5c6f42b9cc..11e62e7f77 100644 --- a/Signum.Test/LinqProvider/SelectTest.cs +++ b/Signum.Test/LinqProvider/SelectTest.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Linq.Expressions; using Signum.Utilities.DataStructures; - +using Signum.Engine.Maps; namespace Signum.Test.LinqProvider { @@ -636,7 +636,14 @@ public void SelectEmbedded() [Fact] public void SelectView() { - var list = Database.View().ToList(); + if (Schema.Current.Settings.IsPostgres) + { + var list = Database.View().ToList(); + } + else + { + var list = Database.View().ToList(); + } } [Fact] @@ -649,7 +656,10 @@ public void SelectRetrieve() [Fact] public void SelectWithHint() { - var list = Database.Query().WithHint("INDEX(IX_LabelID)").Select(a => a.Label.Name).ToList(); + if (!Schema.Current.Settings.IsPostgres) + { + var list = Database.Query().WithHint("INDEX(IX_LabelID)").Select(a => a.Label.Name).ToList(); + } } [Fact] diff --git a/Signum.Test/LinqProvider/SingleFirstTest.cs b/Signum.Test/LinqProvider/SingleFirstTest.cs index aba93dd365..1007147644 100644 --- a/Signum.Test/LinqProvider/SingleFirstTest.cs +++ b/Signum.Test/LinqProvider/SingleFirstTest.cs @@ -5,6 +5,7 @@ using Signum.Entities; using Signum.Utilities.ExpressionTrees; using Signum.Test.Environment; +using Signum.Engine.Maps; namespace Signum.Test.LinqProvider { @@ -70,7 +71,7 @@ public void SelectDoubleSingle() query.ToList(); - Assert.Equal(1, query.QueryText().CountRepetitions("APPLY")); + Assert.Equal(1, query.QueryText().CountRepetitions(Schema.Current.Settings.IsPostgres ? "LATERAL" : "APPLY")); } [Fact] diff --git a/Signum.Test/LinqProvider/SqlFunctionsTest.cs b/Signum.Test/LinqProvider/SqlFunctionsTest.cs index dd35f699a2..77ae50ecf4 100644 --- a/Signum.Test/LinqProvider/SqlFunctionsTest.cs +++ b/Signum.Test/LinqProvider/SqlFunctionsTest.cs @@ -96,6 +96,17 @@ public void DateTimeFunctions() Dump((NoteWithDateEntity n) => n.CreationTime.Millisecond); } + [Fact] + public void DateTimeFunctionsStart() + { + Dump((NoteWithDateEntity n) => n.CreationTime.MonthStart()); + Dump((NoteWithDateEntity n) => n.CreationTime.Date); + Dump((NoteWithDateEntity n) => n.CreationTime.WeekStart()); + Dump((NoteWithDateEntity n) => n.CreationTime.HourStart()); + Dump((NoteWithDateEntity n) => n.CreationTime.MinuteStart()); + Dump((NoteWithDateEntity n) => n.CreationTime.SecondStart()); + } + [Fact] public void DayOfWeekWhere() { @@ -161,6 +172,13 @@ public void DateDiffFunctions() Dump((NoteWithDateEntity n) => (n.CreationTime.AddDays(1) - n.CreationTime).TotalMilliseconds.InSql()); } + [Fact] + public void DateDiffFunctionsTo() + { + Dump((NoteWithDateEntity n) => n.CreationTime.YearsTo(n.CreationTime).InSql()); + Dump((NoteWithDateEntity n) => n.CreationTime.MonthsTo(n.CreationTime).InSql()); + } + [Fact] public void DateFunctions() { @@ -355,9 +373,11 @@ from s4 in songs select MinimumExtensions.MinimumScalar(x, y)).ToList(); var t4 = PerfCounter.Ticks; - - Assert.True(PerfCounter.ToMilliseconds(t1, t2) < PerfCounter.ToMilliseconds(t3, t4)); - Assert.True(PerfCounter.ToMilliseconds(t2, t3) < PerfCounter.ToMilliseconds(t3, t4)); + if (!Schema.Current.Settings.IsPostgres) + { + Assert.True(PerfCounter.ToMilliseconds(t1, t2) < PerfCounter.ToMilliseconds(t3, t4)); + Assert.True(PerfCounter.ToMilliseconds(t2, t3) < PerfCounter.ToMilliseconds(t3, t4)); + } } [Fact] diff --git a/Signum.Test/LinqProvider/SystemTimeTest.cs b/Signum.Test/LinqProvider/SystemTimeTest.cs index 44dde0a7ed..c5e727f346 100644 --- a/Signum.Test/LinqProvider/SystemTimeTest.cs +++ b/Signum.Test/LinqProvider/SystemTimeTest.cs @@ -66,19 +66,23 @@ public void TimeBetween() period = Database.Query().Where(a => a.Name == "X2").Select(a => a.SystemPeriod()).Single(); } + period = new Interval( + new DateTime(period.Min.Ticks, DateTimeKind.Utc), + new DateTime(period.Max.Ticks, DateTimeKind.Utc)); //Hack + using (SystemTime.Override(new SystemTime.AsOf(period.Min))) { - var a = Database.Query().Where(f1 => f1.Name == "X2").ToList(); + var list = Database.Query().Where(f1 => f1.Name == "X2").Select(a => a.SystemPeriod()).ToList(); } using (SystemTime.Override(new SystemTime.Between(period.Max, period.Max.AddSeconds(1)))) { - var a = Database.Query().Where(f1 => f1.Name == "X2").ToList(); + var list = Database.Query().Where(f1 => f1.Name == "X2").Select(a => a.SystemPeriod()).ToList(); } - using (SystemTime.Override(new SystemTime.FromTo(period.Max, period.Max.AddSeconds(1)))) + using (SystemTime.Override(new SystemTime.ContainedIn(period.Max, period.Max.AddSeconds(1)))) { - var b = Database.Query().Where(f2 => f2.Name == "X2").ToList(); + var list = Database.Query().Where(f2 => f2.Name == "X2").Select(a => a.SystemPeriod()).ToList(); } } } diff --git a/Signum.Test/LinqProvider/TakeSkipTest.cs b/Signum.Test/LinqProvider/TakeSkipTest.cs index 6230482fb7..97f51330bc 100644 --- a/Signum.Test/LinqProvider/TakeSkipTest.cs +++ b/Signum.Test/LinqProvider/TakeSkipTest.cs @@ -97,7 +97,11 @@ public void SkipTakeOrder() [Fact] public void InnerTake() { - var result = Database.Query().Where(dr => dr.Songs.OrderByDescending(a => a.Seconds).Take(1).Where(a => a.Name.Contains("1976")).Any()).Select(a => a.ToLite()).ToList(); + var result = Database.Query() + .Where(dr => dr.Songs.OrderByDescending(a => a.Seconds).Take(1).Where(a => a.Name.Contains("Tonight")).Any()) + .Select(a => a.ToLite()) + .ToList(); + Assert.Empty(result); } @@ -112,7 +116,7 @@ public void OrderBySelectPaginate() { TestPaginate(Database.Query().OrderBy(a => a.Name).Select(a => a.Name)); } - + [Fact] public void OrderByDescendingSelectPaginate() { @@ -139,14 +143,14 @@ public void SelectOrderByDescendingPaginate() private void TestPaginate(IQueryable query) { - var list = query.ToList(); + var list = query.OrderAlsoByKeys().ToList(); int pageSize = 2; var list2 = 0.To(((list.Count / pageSize) + 1)).SelectMany(page => query.OrderAlsoByKeys().Skip(pageSize * page).Take(pageSize).ToList()).ToList(); - Assert.True(list.SequenceEqual(list2)); + Assert.Equal(list, list2); } } } diff --git a/Signum.Test/LinqProvider/ToStringTest.cs b/Signum.Test/LinqProvider/ToStringTest.cs index ba5af86d42..4592c201b2 100644 --- a/Signum.Test/LinqProvider/ToStringTest.cs +++ b/Signum.Test/LinqProvider/ToStringTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Xunit; @@ -65,7 +65,7 @@ orderby b.Name select new { b.Name, - MembersToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).ToString(a => a.Name, " | "), + AlbumnsToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).ToString(a => a.Name, " | "), }).ToList(); var result2 = (from b in Database.Query() @@ -73,10 +73,10 @@ orderby b.Name select new { b.Name, - MembersToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).Select(a => a.Name).ToList().ToString(" | "), + AlbumnsToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).Select(a => a.Name).ToList().ToString(" | "), }).ToList(); - Assert.True(Enumerable.SequenceEqual(result1, result2)); + Assert.Equal(result1, result2); } @@ -88,7 +88,7 @@ orderby b.Name select new { b.Name, - MembersToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).ToString(a => a.Id.ToString(), " | "), + AlbumnsToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).ToString(a => a.Id.ToString(), " | "), }).ToList(); var result2 = (from b in Database.Query() @@ -96,7 +96,7 @@ orderby b.Name select new { b.Name, - MembersToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).Select(a => a.Id).ToString(" | "), + AlbumnsToString = Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).Select(a => a.Id).ToString(" | "), }).ToList(); Func, string> toString = list => list.ToString(" | "); @@ -106,7 +106,7 @@ orderby b.Name select new { b.Name, - MembersToString = toString(Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).Select(a => a.Id).ToList()), + AlbumnsToString = toString(Database.Query().Where(a => a.Author == b).OrderBy(a => a.Name).Select(a => a.Id).ToList()), }).ToList(); diff --git a/Signum.Test/ObjectNameTest.cs b/Signum.Test/ObjectNameTest.cs index c102e71c29..bee944447c 100644 --- a/Signum.Test/ObjectNameTest.cs +++ b/Signum.Test/ObjectNameTest.cs @@ -1,49 +1,92 @@ using Xunit; using Signum.Engine.Maps; using System.Globalization; +using Signum.Test.Environment; +using Signum.Engine; +using Signum.Utilities; namespace Signum.Test { public class ObjectNameTest { + bool isPostgres; + public ObjectNameTest() + { + MusicStarter.StartAndLoad(); + Connector.CurrentLogger = new DebugTextWriter(); + this.isPostgres = Signum.Engine.Connector.Current.Schema.Settings.IsPostgres; + } + [Fact] public void ParseDbo() { - var simple = ObjectName.Parse("MyTable"); + var simple = ObjectName.Parse("MyTable", isPostgres); Assert.Equal("MyTable", simple.Name); - Assert.Equal("dbo", simple.Schema.ToString()); + Assert.Equal(isPostgres? "\"public\"" : "dbo", simple.Schema.ToString()); } [Fact] public void ParseSchema() { - var simple = ObjectName.Parse("MySchema.MyTable"); - Assert.Equal("MyTable", simple.Name); - Assert.Equal("MySchema", simple.Schema.ToString()); + if (isPostgres) + { + var simple = ObjectName.Parse("my_schema.my_table", isPostgres); + Assert.Equal("my_table", simple.Name); + Assert.Equal("my_schema", simple.Schema.ToString()); + } + else + { + var simple = ObjectName.Parse("MySchema.MyTable", isPostgres); + Assert.Equal("MyTable", simple.Name); + Assert.Equal("MySchema", simple.Schema.ToString()); + } } [Fact] public void ParseNameEscaped() { - var simple = ObjectName.Parse("MySchema.[Select]"); - Assert.Equal("Select", simple.Name); - Assert.Equal("MySchema", simple.Schema.ToString()); - Assert.Equal("MySchema.[Select]", simple.ToString()); + if (isPostgres) + { + var simple = ObjectName.Parse("\"MySchema\".\"Select\"", isPostgres); + Assert.Equal("Select", simple.Name); + Assert.Equal("\"MySchema\"", simple.Schema.ToString()); + Assert.Equal("\"MySchema\".\"Select\"", simple.ToString()); + } + else + { + var simple = ObjectName.Parse("MySchema.[Select]", isPostgres); + Assert.Equal("Select", simple.Name); + Assert.Equal("MySchema", simple.Schema.ToString()); + Assert.Equal("MySchema.[Select]", simple.ToString()); + } } [Fact] public void ParseSchemaNameEscaped() { - var simple = ObjectName.Parse("[Select].MyTable"); - Assert.Equal("MyTable", simple.Name); - Assert.Equal("Select", simple.Schema.Name); - Assert.Equal("[Select].MyTable", simple.ToString()); + if (isPostgres) + { + var simple = ObjectName.Parse("\"Select\".mytable", isPostgres); + Assert.Equal("mytable", simple.Name); + Assert.Equal("Select", simple.Schema.Name); + Assert.Equal("\"Select\".mytable", simple.ToString()); + } + else + { + var simple = ObjectName.Parse("[Select].MyTable", isPostgres); + Assert.Equal("MyTable", simple.Name); + Assert.Equal("Select", simple.Schema.Name); + Assert.Equal("[Select].MyTable", simple.ToString()); + } } [Fact] public void ParseServerName() { - var simple = ObjectName.Parse("[FROM].[SELECT].[WHERE].[TOP]"); + var simple = ObjectName.Parse(isPostgres ? + "\"FROM\".\"SELECT\".\"WHERE\".\"TOP\"" : + "[FROM].[SELECT].[WHERE].[TOP]", + isPostgres); Assert.Equal("TOP", simple.Name); Assert.Equal("WHERE", simple.Schema.Name); Assert.Equal("SELECT", simple.Schema.Database!.Name); @@ -54,7 +97,10 @@ public void ParseServerName() [Fact] public void ParseServerNameSuperComplex() { - var simple = ObjectName.Parse("[FROM].[SELECT].[WHERE].[TOP.DISTINCT]"); + var simple = ObjectName.Parse(isPostgres ? + "\"FROM\".\"SELECT\".\"WHERE\".\"TOP.DISTINCT\"" : + "[FROM].[SELECT].[WHERE].[TOP.DISTINCT]", + isPostgres); Assert.Equal("TOP.DISTINCT", simple.Name); Assert.Equal("WHERE", simple.Schema.Name); Assert.Equal("SELECT", simple.Schema.Database!.Name); diff --git a/Signum.Test/Signum.Test.csproj b/Signum.Test/Signum.Test.csproj index 8d1ab8a05e..433c449bb8 100644 --- a/Signum.Test/Signum.Test.csproj +++ b/Signum.Test/Signum.Test.csproj @@ -2,7 +2,6 @@ netcoreapp3.1 - preview SignumTest true enable @@ -16,9 +15,9 @@ - - - + + + @@ -35,7 +34,7 @@ - + diff --git a/Signum.Test/appsettings.json b/Signum.Test/appsettings.json index c3634e2719..854ba1931c 100644 --- a/Signum.Test/appsettings.json +++ b/Signum.Test/appsettings.json @@ -1,5 +1,6 @@ {//This file is not copy to the bin directory. Use UserSecrets instead. "ConnectionStrings": { - "SignumTest": "Data Source=.\\SQLEXPRESS;Initial Catalog=SignumTest;Integrated Security=true" + //"SignumTest": "Data Source=.\\SQLEXPRESS;Initial Catalog=SignumTest;Integrated Security=true", + "SignumTest": "host=localhost;Username=postgres;Password=bnwfox7g;Database=SignumTest" } } diff --git a/Signum.Utilities/Date.cs b/Signum.Utilities/Date.cs new file mode 100644 index 0000000000..5d8d3275a4 --- /dev/null +++ b/Signum.Utilities/Date.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.Serialization; +using System.Text; + +//Thanks to supersonicclay +//From https://github.com/supersonicclay/csharp-date/blob/master/CSharpDate/Date.cs +namespace Signum.Utilities +{ + + [Serializable] + public struct Date : IComparable, IFormattable, ISerializable, IComparable, IEquatable + { + private DateTime _dt; + + public static readonly Date MaxValue = new Date(DateTime.MaxValue); + public static readonly Date MinValue = new Date(DateTime.MinValue); + + public Date(int year, int month, int day) + { + this._dt = new DateTime(year, month, day); + } + + public Date(DateTime dateTime) + { + this._dt = dateTime.AddTicks(-dateTime.Ticks % TimeSpan.TicksPerDay); + } + + private Date(SerializationInfo info, StreamingContext context) + { + this._dt = DateTime.FromFileTime(info.GetInt64("ticks")); + } + + public static TimeSpan operator -(Date d1, Date d2) + { + return d1._dt - d2._dt; + } + + public static Date operator -(Date d, TimeSpan t) + { + return new Date(d._dt - t); + } + + public static bool operator !=(Date d1, Date d2) + { + return d1._dt != d2._dt; + } + + public static Date operator +(Date d, TimeSpan t) + { + return new Date(d._dt + t); + } + + public static bool operator <(Date d1, Date d2) + { + return d1._dt < d2._dt; + } + + public static bool operator <=(Date d1, Date d2) + { + return d1._dt <= d2._dt; + } + + public static bool operator ==(Date d1, Date d2) + { + return d1._dt == d2._dt; + } + + public static bool operator >(Date d1, Date d2) + { + return d1._dt > d2._dt; + } + + public static bool operator >=(Date d1, Date d2) + { + return d1._dt >= d2._dt; + } + + public static implicit operator DateTime(Date d) + { + return d._dt; + } + + public static explicit operator Date(DateTime d) + { + return new Date(d); + } + + public int Day + { + get + { + return this._dt.Day; + } + } + + public DayOfWeek DayOfWeek + { + get + { + return this._dt.DayOfWeek; + } + } + + public int DayOfYear + { + get + { + return this._dt.DayOfYear; + } + } + + public int Month + { + get + { + return this._dt.Month; + } + } + + public static Date Today + { + get + { + return new Date(DateTime.Today); + } + } + + public int Year + { + get + { + return this._dt.Year; + } + } + + public long Ticks + { + get + { + return this._dt.Ticks; + } + } + + public Date AddDays(int value) + { + return new Date(this._dt.AddDays(value)); + } + + public Date AddMonths(int value) + { + return new Date(this._dt.AddMonths(value)); + } + + public Date AddYears(int value) + { + return new Date(this._dt.AddYears(value)); + } + + public static int Compare(Date d1, Date d2) + { + return d1.CompareTo(d2); + } + + public int CompareTo(Date value) + { + return this._dt.CompareTo(value._dt); + } + + public int CompareTo(object? value) + { + return this._dt.CompareTo(value); + } + + public static int DaysInMonth(int year, int month) + { + return DateTime.DaysInMonth(year, month); + } + + public bool Equals(Date value) + { + return this._dt.Equals(value._dt); + } + + public override bool Equals(object? value) + { + return value is Date && this._dt.Equals(((Date)value)._dt); + } + + public override int GetHashCode() + { + return this._dt.GetHashCode(); + } + + public static bool Equals(Date d1, Date d2) + { + return d1._dt.Equals(d2._dt); + } + + void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("ticks", this._dt.Ticks); + } + + public static bool IsLeapYear(int year) + { + return DateTime.IsLeapYear(year); + } + + public static Date Parse(string s) + { + return new Date(DateTime.Parse(s)); + } + + public static Date Parse(string s, IFormatProvider provider) + { + return new Date(DateTime.Parse(s, provider)); + } + + public static Date Parse(string s, IFormatProvider provider, DateTimeStyles style) + { + return new Date(DateTime.Parse(s, provider, style)); + } + + public static Date ParseExact(string s, string format, IFormatProvider provider) + { + return new Date(DateTime.ParseExact(s, format, provider)); + } + + public static Date ParseExact(string s, string format, IFormatProvider provider, DateTimeStyles style) + { + return new Date(DateTime.ParseExact(s, format, provider, style)); + } + + public static Date ParseExact(string s, string[] formats, IFormatProvider provider, DateTimeStyles style) + { + return new Date(DateTime.ParseExact(s, formats, provider, style)); + } + + public TimeSpan Subtract(Date value) + { + return this - value; + } + + public Date Subtract(TimeSpan value) + { + return this - value; + } + + public string ToLongString() + { + return this._dt.ToLongDateString(); + } + + public string ToShortString() + { + return this._dt.ToShortDateString(); + } + + public override string ToString() + { + return this.ToShortString(); + } + + public string ToString(IFormatProvider provider) + { + return this._dt.ToString(provider); + } + + public string ToString(string format) + { + if (format == "O" || format == "o" || format == "s") + { + return this.ToString("yyyy-MM-dd"); + } + + return this._dt.ToString(format); + } + + public string ToString(string? format, IFormatProvider? provider) + { + return this._dt.ToString(format, provider); + } + + public static bool TryParse(string s, out Date result) + { + DateTime d; + bool success = DateTime.TryParse(s, out d); + result = new Date(d); + return success; + } + + public static bool TryParse(string s, IFormatProvider provider, DateTimeStyles style, out Date result) + { + DateTime d; + bool success = DateTime.TryParse(s, provider, style, out d); + result = new Date(d); + return success; + } + + public static bool TryParseExact(string s, string format, IFormatProvider provider, DateTimeStyles style, out Date result) + { + DateTime d; + bool success = DateTime.TryParseExact(s, format, provider, style, out d); + result = new Date(d); + return success; + } + + public static bool TryParseExact(string s, string[] formats, IFormatProvider provider, DateTimeStyles style, out Date result) + { + DateTime d; + bool success = DateTime.TryParseExact(s, formats, provider, style, out d); + result = new Date(d); + return success; + } + } +} diff --git a/Signum.Utilities/ExpressionTrees/ExpressionCleaner.cs b/Signum.Utilities/ExpressionTrees/ExpressionCleaner.cs index 1ed4208912..6dfec3619d 100644 --- a/Signum.Utilities/ExpressionTrees/ExpressionCleaner.cs +++ b/Signum.Utilities/ExpressionTrees/ExpressionCleaner.cs @@ -313,6 +313,23 @@ protected override Expression VisitBinary(BinaryExpression b) return Expression.MakeBinary(b.NodeType, left, right, b.IsLiftedToNull, b.Method); } + // rel == 'a' is compiled as (int)rel == 123 + if(b.NodeType == ExpressionType.Equal || + b.NodeType == ExpressionType.NotEqual) + { + { + if (IsConvertCharToInt(b.Left) is Expression l && + IsConvertCharToIntOrConstant(b.Right) is Expression r) + return Expression.MakeBinary(b.NodeType, Visit(l), Visit(r)); + } + { + if (IsConvertCharToIntOrConstant(b.Left) is Expression l && + IsConvertCharToInt(b.Right) is Expression r) + return Expression.MakeBinary(b.NodeType, Visit(l), Visit(r)); + } + + } + if (b.Left.Type != typeof(bool)) return base.VisitBinary(b); @@ -344,6 +361,31 @@ protected override Expression VisitBinary(BinaryExpression b) return base.VisitBinary(b); } + static Expression? IsConvertCharToInt(Expression exp) + { + if (exp is UnaryExpression ue && ue.NodeType == ExpressionType.Convert && ue.Operand.Type == typeof(char)) + { + return ue.Operand; + } + + return null; + } + + static Expression? IsConvertCharToIntOrConstant(Expression exp) + { + var result = IsConvertCharToInt(exp); + if (result != null) + return result; + + if (exp is ConstantExpression ceInt && ceInt.Type == typeof(int)) + return Expression.Constant((char)(int)ceInt.Value, typeof(char)); + + if (exp is ConstantExpression ceChar && ceChar.Type == typeof(char)) + return ceChar; + + return null; + } + protected override Expression VisitConditional(ConditionalExpression c) { if (!shortCircuit) diff --git a/Signum.Utilities/Extensions/DateTimeExtensions.cs b/Signum.Utilities/Extensions/DateTimeExtensions.cs index e1ffd212c5..6b7ca22699 100644 --- a/Signum.Utilities/Extensions/DateTimeExtensions.cs +++ b/Signum.Utilities/Extensions/DateTimeExtensions.cs @@ -457,6 +457,11 @@ public static long ToUnixTimeMilliseconds(this DateTime dateTime) { return new DateTimeOffset(dateTime).ToUnixTimeMilliseconds(); } + + public static Date ToDate(this DateTime dt) + { + return new Date(dt); + } } [DescriptionOptions(DescriptionOptions.Members)] diff --git a/Signum.Utilities/SafeConsole.cs b/Signum.Utilities/SafeConsole.cs index 9efa2e72cf..d6fa15d50d 100644 --- a/Signum.Utilities/SafeConsole.cs +++ b/Signum.Utilities/SafeConsole.cs @@ -98,12 +98,25 @@ public static bool Ask(string question) return Ask(question, "yes", "no") == "yes"; } - public static string Ask(string question, params string[] answers) + public static string AskRetry(string question, params string[] answers) + { + retry: + var result = Ask(question, answers); + if (result == null) + goto retry; + + return result; + } + + public static string? Ask(string question, params string[] answers) { Console.Write(question + " ({0}) ".FormatWith(answers.ToString("/"))); do { var userAnswer = Console.ReadLine().ToLower(); + if (!userAnswer.HasText()) + return null; + var result = answers.FirstOrDefault(a => a.StartsWith(userAnswer, StringComparison.CurrentCultureIgnoreCase)); if (result != null) return result; @@ -112,6 +125,16 @@ public static string Ask(string question, params string[] answers) } while (true); } + public static string AskMultilineRetry(string question, params string[] answers) + { + retry: + var result = AskMultiLine(question, answers); + if (result == null) + goto retry; + + return result; + } + public static string? AskMultiLine(string question, params string[] answers) { Console.WriteLine(question); @@ -149,7 +172,11 @@ public static bool Ask(ref bool? rememberedAnswer, string question) string? answerString = null; - string result = Ask(ref answerString, question, "yes", "no"); + retry: + string? result = Ask(ref answerString, question, "yes", "no"); + + if (result == null) + goto retry; if (answerString.HasText()) rememberedAnswer = answerString == "yes"; @@ -157,7 +184,7 @@ public static bool Ask(ref bool? rememberedAnswer, string question) return result == "yes"; } - public static string Ask(ref string? rememberedAnswer, string question, params string[] answers) + public static string? Ask(ref string? rememberedAnswer, string question, params string[] answers) { if (rememberedAnswer != null) return rememberedAnswer; @@ -172,6 +199,9 @@ public static string Ask(ref string? rememberedAnswer, string question, params s if (remember) userAnswer = userAnswer.Replace("!", ""); + if (!userAnswer.HasText()) + return null; + var result = answers.FirstOrDefault(a => a.StartsWith(userAnswer, StringComparison.CurrentCultureIgnoreCase)); if (result != null) { diff --git a/Signum.Utilities/Signum.Utilities.csproj b/Signum.Utilities/Signum.Utilities.csproj index cb24bf3058..33ccc93116 100644 --- a/Signum.Utilities/Signum.Utilities.csproj +++ b/Signum.Utilities/Signum.Utilities.csproj @@ -2,7 +2,6 @@ netcoreapp3.1 - preview true enable true