diff --git a/Npgsql.EntityFramework/Npgsql.EntityFramework.csproj b/Npgsql.EntityFramework/Npgsql.EntityFramework.csproj index b0599a4c54..613022465d 100644 --- a/Npgsql.EntityFramework/Npgsql.EntityFramework.csproj +++ b/Npgsql.EntityFramework/Npgsql.EntityFramework.csproj @@ -99,6 +99,7 @@ + @@ -123,4 +124,4 @@ - + \ No newline at end of file diff --git a/Npgsql.EntityFramework/NpgsqlMigrationSqlGenerator.cs b/Npgsql.EntityFramework/NpgsqlMigrationSqlGenerator.cs new file mode 100644 index 0000000000..c19a567671 --- /dev/null +++ b/Npgsql.EntityFramework/NpgsqlMigrationSqlGenerator.cs @@ -0,0 +1,833 @@ +// NpgsqlMigrationSqlGenerator.cs +// +// Author: +// David Karlaš (david.karlas@gmail.com) +// +// Copyright (C) 2014 David Karlaš +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +using System; +using System.Collections.Generic; +using System.Data.Entity.Migrations.Model; +using System.Data.Entity.Migrations.Sql; +using System.Globalization; +using System.Text; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Spatial; + +namespace Npgsql +{ + public class NpgsqlMigrationSqlGenerator : MigrationSqlGenerator + { + List migrationStatments; + private List addedSchemas; + private List addedExtensions; + private Version serverVersion; + + public override IEnumerable Generate( + IEnumerable migrationOperations, string providerManifestToken) + { + migrationStatments = new List(); + addedSchemas = new List(); + addedExtensions = new List(); + serverVersion = new Version(providerManifestToken); + Convert(migrationOperations); + return migrationStatments; + } + + #region MigrationOperation to MigrationStatement converters + + #region General + + private void Convert(IEnumerable operations) + { + foreach (var migrationOperation in operations) + { + if (migrationOperation is AddColumnOperation) + { + Convert(migrationOperation as AddColumnOperation); + } + else if (migrationOperation is AlterColumnOperation) + { + Convert(migrationOperation as AlterColumnOperation); + } + else if (migrationOperation is CreateTableOperation) + { + Convert(migrationOperation as CreateTableOperation); + } + else if (migrationOperation is DropForeignKeyOperation) + { + Convert(migrationOperation as DropForeignKeyOperation); + } + else if (migrationOperation is DropTableOperation) + { + Convert(migrationOperation as DropTableOperation); + } + else if (migrationOperation is MoveTableOperation) + { + Convert(migrationOperation as MoveTableOperation); + } + else if (migrationOperation is RenameTableOperation) + { + Convert(migrationOperation as RenameTableOperation); + } + else if (migrationOperation is AddForeignKeyOperation) + { + Convert(migrationOperation as AddForeignKeyOperation); + } + else if (migrationOperation is DropIndexOperation) + { + Convert(migrationOperation as DropIndexOperation); + } + else if (migrationOperation is SqlOperation) + { + AddStatment((migrationOperation as SqlOperation).Sql, (migrationOperation as SqlOperation).SuppressTransaction); + } + else if (migrationOperation is AddPrimaryKeyOperation) + { + Convert(migrationOperation as AddPrimaryKeyOperation); + } + else if (migrationOperation is CreateIndexOperation) + { + Convert(migrationOperation as CreateIndexOperation); + } + else if (migrationOperation is DropColumnOperation) + { + Convert(migrationOperation as DropColumnOperation); + } + else if (migrationOperation is DropPrimaryKeyOperation) + { + Convert(migrationOperation as DropPrimaryKeyOperation); + } + else if (migrationOperation is HistoryOperation) + { + Convert(migrationOperation as HistoryOperation); + } + else if (migrationOperation is RenameColumnOperation) + { + Convert(migrationOperation as RenameColumnOperation); + } + else if (migrationOperation is UpdateDatabaseOperation) + { + Convert((migrationOperation as UpdateDatabaseOperation).Migrations as IEnumerable); + } + else + { + throw new NotImplementedException("Unhandled MigrationOperation " + migrationOperation.GetType().Name + " in " + GetType().Name); + } + } + } + + private void AddStatment(string sql, bool suppressTransacion = false) + { + migrationStatments.Add(new MigrationStatement() + { + Sql = sql, + SuppressTransaction = suppressTransacion, + BatchTerminator = ";" + + }); + } + + private void AddStatment(StringBuilder sql, bool suppressTransacion = false) + { + AddStatment(sql.ToString(), suppressTransacion); + } + + #endregion + + #region History + + private void Convert(HistoryOperation historyOperation) + { + foreach (var command in historyOperation.CommandTrees) + { + AddStatment(NpgsqlServices.Instance.CreateDbCommand(command).CommandText); + } + } + + #endregion + + #region Tables + + private void Convert(CreateTableOperation createTableOperation) + { + StringBuilder sql = new StringBuilder(); + CreateSchema(createTableOperation.Name); + sql.Append("CREATE TABLE "); + AppendTableName(createTableOperation.Name, sql); + sql.Append('('); + foreach (var column in createTableOperation.Columns) + { + AppendColumn(column, sql); + sql.Append(","); + } + sql.Remove(sql.Length - 1, 1); + if (createTableOperation.PrimaryKey != null) + { + sql.Append(","); + sql.Append("CONSTRAINT "); + sql.Append('"'); + sql.Append(createTableOperation.PrimaryKey.Name); + sql.Append('"'); + sql.Append(" PRIMARY KEY "); + sql.Append("("); + foreach (var column in createTableOperation.PrimaryKey.Columns) + { + sql.Append('"'); + sql.Append(column); + sql.Append("\","); + } + sql.Remove(sql.Length - 1, 1); + sql.Append(")"); + } + sql.Append(")"); + AddStatment(sql); + } + + private void Convert(DropTableOperation dropTableOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("DROP TABLE "); + AppendTableName(dropTableOperation.Name, sql); + AddStatment(sql); + } + + private void Convert(RenameTableOperation renameTableOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(renameTableOperation.Name, sql); + sql.Append(" RENAME TO "); + AppendTableName(renameTableOperation.NewName, sql); + AddStatment(sql); + } + + private void CreateSchema(string schemaName) + { + int dotIndex = schemaName.IndexOf('.'); + if (dotIndex != -1) + schemaName = schemaName.Remove(dotIndex); + if (addedSchemas.Contains(schemaName)) + return; + addedSchemas.Add(schemaName); + if (serverVersion.Major > 9 || (serverVersion.Major == 9 && serverVersion.Minor >= 3)) + { + AddStatment("CREATE SCHEMA IF NOT EXISTS " + schemaName); + } + else + { + //TODO: CREATE PROCEDURE that checks if schema already exists on servers < 9.3 + AddStatment("CREATE SCHEMA " + schemaName); + } + } + + //private void CreateExtension(string exensionName) + //{ + // //This is compatible only with server 9.1+ + // if (serverVersion.Major > 9 || (serverVersion.Major == 9 && serverVersion.Minor >= 1)) + // { + // if (addedExtensions.Contains(exensionName)) + // return; + // addedExtensions.Add(exensionName); + // AddStatment("CREATE EXTENSION IF NOT EXISTS \"" + exensionName + "\""); + // } + //} + + private void Convert(MoveTableOperation moveTableOperation) + { + StringBuilder sql = new StringBuilder(); + var newSchema = moveTableOperation.NewSchema ?? "dbo"; + CreateSchema(newSchema); + sql.Append("ALTER TABLE "); + AppendTableName(moveTableOperation.Name, sql); + sql.Append(" SET SCHEMA "); + sql.Append(newSchema); + AddStatment(sql); + } + + #endregion + + #region Columns + private void Convert(AddColumnOperation addColumnOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(addColumnOperation.Table, sql); + sql.Append(" ADD "); + AppendColumn(addColumnOperation.Column, sql); + AddStatment(sql); + } + + private void Convert(DropColumnOperation dropColumnOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(dropColumnOperation.Table, sql); + sql.Append(" DROP COLUMN \""); + sql.Append(dropColumnOperation.Name); + sql.Append('"'); + AddStatment(sql); + } + + private void Convert(AlterColumnOperation alterColumnOperation) + { + StringBuilder sql = new StringBuilder(); + + //TYPE + AppendAlterColumn(alterColumnOperation, sql); + sql.Append(" TYPE "); + AppendColumnType(alterColumnOperation.Column, sql, false); + AddStatment(sql); + sql.Clear(); + + //NOT NULL + AppendAlterColumn(alterColumnOperation, sql); + if (alterColumnOperation.Column.IsNullable != null && !alterColumnOperation.Column.IsNullable.Value) + sql.Append(" SET NOT NULL"); + else + sql.Append(" DROP NOT NULL"); + AddStatment(sql); + sql.Clear(); + + //DEFAULT + AppendAlterColumn(alterColumnOperation, sql); + if (alterColumnOperation.Column.DefaultValue != null) + { + sql.Append(" SET DEFAULT "); + AppendValue(alterColumnOperation.Column.DefaultValue, sql); + } + else if (!string.IsNullOrWhiteSpace(alterColumnOperation.Column.DefaultValueSql)) + { + sql.Append(" SET DEFAULT "); + sql.Append(alterColumnOperation.Column.DefaultValueSql); + } + else if (alterColumnOperation.Column.IsIdentity) + { + sql.Append(" SET DEFAULT "); + switch (alterColumnOperation.Column.Type) + { + case PrimitiveTypeKind.Byte: + case PrimitiveTypeKind.SByte: + case PrimitiveTypeKind.Int16: + case PrimitiveTypeKind.Int32: + case PrimitiveTypeKind.Int64: + //TODO: need function CREATE SEQUENCE IF NOT EXISTS and set to it... + //Until this is resolved changing IsIdentity from false to true + //on types int2, int4 and int8 won't switch to type serial2, serial4 and serial8 + throw new NotImplementedException("Not supporting creating sequence for integer types"); + case PrimitiveTypeKind.Guid: + //CreateExtension("uuid-ossp"); + //If uuid-ossp is not enabled migrations throw exception + AddStatment("select * from uuid_generate_v4()"); + sql.Append("uuid_generate_v4()"); + break; + default: + throw new NotImplementedException("Not supporting creating IsIdentity for " + alterColumnOperation.Column.Type); + } + } + else + { + sql.Append(" DROP DEFAULT"); + } + AddStatment(sql); + } + + private void AppendAlterColumn(AlterColumnOperation alterColumnOperation, StringBuilder sql) + { + sql.Append("ALTER TABLE "); + AppendTableName(alterColumnOperation.Table, sql); + sql.Append(" ALTER COLUMN \""); + sql.Append(alterColumnOperation.Column.Name); + sql.Append('"'); + } + + private void Convert(RenameColumnOperation renameColumnOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(renameColumnOperation.Table, sql); + sql.Append(" RENAME COLUMN \""); + sql.Append(renameColumnOperation.Name); + sql.Append("\" TO \""); + sql.Append(renameColumnOperation.NewName); + sql.Append('"'); + AddStatment(sql); + } + + #endregion + + #region Keys and indexes + + private void Convert(AddForeignKeyOperation addForeignKeyOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(addForeignKeyOperation.DependentTable, sql); + sql.Append(" ADD CONSTRAINT \""); + sql.Append(addForeignKeyOperation.Name); + sql.Append("\" FOREIGN KEY ("); + foreach (var column in addForeignKeyOperation.DependentColumns) + { + sql.Append('"'); + sql.Append(column); + sql.Append("\","); + } + sql.Remove(sql.Length - 1, 1); + sql.Append(") REFERENCES "); + AppendTableName(addForeignKeyOperation.PrincipalTable, sql); + sql.Append(" ("); + foreach (var column in addForeignKeyOperation.PrincipalColumns) + { + sql.Append('"'); + sql.Append(column); + sql.Append("\","); + } + sql.Remove(sql.Length - 1, 1); + sql.Append(")"); + + if (addForeignKeyOperation.CascadeDelete) + { + sql.Append(" ON DELETE CASCADE"); + } + AddStatment(sql); + } + + private void Convert(DropForeignKeyOperation dropForeignKeyOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(dropForeignKeyOperation.DependentTable, sql); + if (serverVersion.Major < 9) + sql.Append(" DROP CONSTRAINT \"");//TODO: http://piecesformthewhole.blogspot.com/2011/04/dropping-foreign-key-if-it-exists-in.html ? + else + sql.Append(" DROP CONSTRAINT IF EXISTS \""); + sql.Append(dropForeignKeyOperation.Name); + sql.Append('"'); + AddStatment(sql); + } + + private void Convert(CreateIndexOperation createIndexOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("CREATE "); + + if (createIndexOperation.IsUnique) + sql.Append("UNIQUE "); + + sql.Append("INDEX \""); + sql.Append(GetTableNameFromFullTableName(createIndexOperation.Table) + "_" + createIndexOperation.Name); + sql.Append("\" ON "); + AppendTableName(createIndexOperation.Table, sql); + sql.Append(" ("); + foreach (var column in createIndexOperation.Columns) + { + sql.Append('"'); + sql.Append(column); + sql.Append("\","); + } + sql.Remove(sql.Length - 1, 1); + sql.Append(")"); + AddStatment(sql); + } + + private string GetSchemaNameFromFullTableName(string tableFullName) + { + int dotIndex = tableFullName.IndexOf('.'); + if (dotIndex != -1) + return tableFullName.Remove(dotIndex); + else + return "dto";//TODO: Check always setting dto schema if no schema in table name is not bug + } + + /// + /// Removes schema prefix e.g. "dto.Blogs" returns "Blogs" and "Posts" returns "Posts" + /// + /// + /// + private string GetTableNameFromFullTableName(string tableName) + { + int dotIndex = tableName.IndexOf('.'); + if (dotIndex != -1) + return tableName.Substring(dotIndex + 1); + else + return tableName; + } + + private void Convert(DropIndexOperation dropIndexOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("DROP INDEX IF EXISTS "); + sql.Append(GetSchemaNameFromFullTableName(dropIndexOperation.Table)); + sql.Append(".\""); + sql.Append(GetTableNameFromFullTableName(dropIndexOperation.Table) + "_" + dropIndexOperation.Name); + sql.Append('"'); + AddStatment(sql); + } + + private void Convert(AddPrimaryKeyOperation addPrimaryKeyOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(addPrimaryKeyOperation.Table, sql); + sql.Append(" ADD CONSTRAINT \""); + sql.Append(addPrimaryKeyOperation.Name); + sql.Append("\" PRIMARY KEY "); + + sql.Append("("); + foreach (var column in addPrimaryKeyOperation.Columns) + { + sql.Append('"'); + sql.Append(column); + sql.Append("\","); + } + sql.Remove(sql.Length - 1, 1); + sql.Append(")"); + AddStatment(sql); + } + + private void Convert(DropPrimaryKeyOperation dropPrimaryKeyOperation) + { + StringBuilder sql = new StringBuilder(); + sql.Append("ALTER TABLE "); + AppendTableName(dropPrimaryKeyOperation.Table, sql); + sql.Append(" DROP CONSTRAINT \""); + sql.Append(dropPrimaryKeyOperation.Name); + sql.Append('"'); + AddStatment(sql); + } + + #endregion + + #endregion + + #region Misc functions + + private void AppendColumn(ColumnModel column, StringBuilder sql) + { + sql.Append('"'); + sql.Append(column.Name); + sql.Append("\" "); + AppendColumnType(column, sql, true); + + if (column.IsNullable != null && !column.IsNullable.Value) + sql.Append(" NOT NULL"); + + if (column.DefaultValue != null) + { + sql.Append(" DEFAULT "); + AppendValue(column.DefaultValue, sql); + } + else if (!string.IsNullOrWhiteSpace(column.DefaultValueSql)) + { + sql.Append(" DEFAULT "); + sql.Append(column.DefaultValueSql); + } + else if (column.IsIdentity) + { + switch (column.Type) + { + case PrimitiveTypeKind.Guid: + //CreateExtension("uuid-ossp"); + //If uuid-ossp is not enabled migrations throw exception + AddStatment("select * from uuid_generate_v4()"); + sql.Append(" DEFAULT uuid_generate_v4()"); + break; + case PrimitiveTypeKind.Byte: + case PrimitiveTypeKind.SByte: + case PrimitiveTypeKind.Int16: + case PrimitiveTypeKind.Int32: + case PrimitiveTypeKind.Int64: + //TODO: Add support for setting "SERIAL" + break; + } + } + else if (column.IsNullable != null + && !column.IsNullable.Value + && (column.StoreType == null || + (column.StoreType.IndexOf("rowversion", StringComparison.OrdinalIgnoreCase) == -1))) + { + sql.Append(" DEFAULT "); + AppendValue(column.ClrDefaultValue, sql); + } + } + + private void AppendColumnType(ColumnModel column, StringBuilder sql, bool setSerial) + { + switch (column.Type) + { + case PrimitiveTypeKind.Binary: + sql.Append("bytea"); + break; + case PrimitiveTypeKind.Boolean: + sql.Append("boolean"); + break; + case PrimitiveTypeKind.DateTime: + if (column.Precision != null) + sql.Append("timestamp(" + column.Precision + ")"); + else + sql.Append("timestamp"); + break; + case PrimitiveTypeKind.Decimal: + //TODO: Check if inside min/max + if (column.Precision == null && column.Scale == null) + { + sql.Append("numeric"); + } + else + { + sql.Append("numeric("); + sql.Append(column.Precision ?? 19); + sql.Append(','); + sql.Append(column.Scale ?? 4); + sql.Append(')'); + } + break; + case PrimitiveTypeKind.Double: + sql.Append("float8"); + break; + case PrimitiveTypeKind.Guid: + sql.Append("uuid"); + break; + case PrimitiveTypeKind.Single: + sql.Append("float4"); + break; + case PrimitiveTypeKind.Byte://postgres doesn't support sbyte :( + case PrimitiveTypeKind.SByte://postgres doesn't support sbyte :( + case PrimitiveTypeKind.Int16: + if (setSerial) + sql.Append(column.IsIdentity ? "serial2" : "int2"); + else + sql.Append("int2"); + break; + case PrimitiveTypeKind.Int32: + if (setSerial) + sql.Append(column.IsIdentity ? "serial4" : "int4"); + else + sql.Append("int4"); + break; + case PrimitiveTypeKind.Int64: + if (setSerial) + sql.Append(column.IsIdentity ? "serial8" : "int8"); + else + sql.Append("int8"); + break; + case PrimitiveTypeKind.String: + //Seems like bug in EF 6.0.2 to set MaxLength to 1 instead of 0 if not specified + //http://entityframework.codeplex.com/workitem/1784 TODO: Investigate and put back to + //if (column.MaxLength != null && column.MaxLength > 0) + if (column.MaxLength != null && column.MaxLength > 1) + { + sql.Append("varchar("); + sql.Append(column.MaxLength); + sql.Append(")"); + } + else + sql.Append("text"); + break; + case PrimitiveTypeKind.Time: + if (column.Precision != null) + { + sql.Append("interval("); + sql.Append(column.Precision); + sql.Append(')'); + } + else + { + sql.Append("interval"); + } + break; + case PrimitiveTypeKind.DateTimeOffset: + if (column.Precision != null) + { + sql.Append("timestamptz("); + sql.Append(column.Precision); + sql.Append(')'); + } + else + { + sql.Append("timestamptz"); + } + break; + case PrimitiveTypeKind.Geometry: + sql.Append("point"); + break; + //case PrimitiveTypeKind.Geography: + // break; + //case PrimitiveTypeKind.GeometryPoint: + // break; + //case PrimitiveTypeKind.GeometryLineString: + // break; + //case PrimitiveTypeKind.GeometryPolygon: + // break; + //case PrimitiveTypeKind.GeometryMultiPoint: + // break; + //case PrimitiveTypeKind.GeometryMultiLineString: + // break; + //case PrimitiveTypeKind.GeometryMultiPolygon: + // break; + //case PrimitiveTypeKind.GeometryCollection: + // break; + //case PrimitiveTypeKind.GeographyPoint: + // break; + //case PrimitiveTypeKind.GeographyLineString: + // break; + //case PrimitiveTypeKind.GeographyPolygon: + // break; + //case PrimitiveTypeKind.GeographyMultiPoint: + // break; + //case PrimitiveTypeKind.GeographyMultiLineString: + // break; + //case PrimitiveTypeKind.GeographyMultiPolygon: + // break; + //case PrimitiveTypeKind.GeographyCollection: + // break; + default: + throw new ArgumentException("Unhandled column type:" + column.Type); + } + } + + private void AppendTableName(string tableName, StringBuilder sql) + { + int dotIndex = tableName.IndexOf('.'); + if (dotIndex == -1) + { + sql.Append('"'); + sql.Append(tableName); + sql.Append('"'); + } + else + { + sql.Append('"'); + sql.Append(tableName.Remove(dotIndex)); + sql.Append("\".\""); + sql.Append(tableName.Substring(dotIndex + 1)); + sql.Append('"'); + } + } + + #endregion + + #region Value appenders + + private void AppendValue(byte[] values, StringBuilder sql) + { + if (values.Length == 0) + { + sql.Append("''"); + } + else + { + sql.Append("E'\\\\"); + foreach (var value in values) + sql.Append(value.ToString("X2")); + sql.Append("'"); + } + } + + private void AppendValue(bool value, StringBuilder sql) + { + sql.Append(value ? "TRUE" : "FALSE"); + } + + private void AppendValue(DateTime value, StringBuilder sql) + { + sql.Append("'"); + sql.Append(((NpgsqlTypes.NpgsqlTimeStamp)value).ToString()); + sql.Append("'"); + } + + private void AppendValue(DateTimeOffset value, StringBuilder sql) + { + sql.Append("'"); + sql.Append(((NpgsqlTypes.NpgsqlTimeStampTZ)value).ToString()); + sql.Append("'"); + } + + private void AppendValue(Guid value, StringBuilder sql) + { + sql.Append("'"); + sql.Append(value); + sql.Append("'"); + } + + private void AppendValue(string value, StringBuilder sql) + { + sql.Append("'"); + sql.Append(value); + sql.Append("'"); + } + + private void AppendValue(TimeSpan value, StringBuilder sql) + { + sql.Append("'"); + sql.Append(new NpgsqlTypes.NpgsqlInterval(value).ToString()); + sql.Append("'"); + } + + private void AppendValue(DbGeometry value, StringBuilder sql) + { + sql.Append("'"); + sql.Append(value); + sql.Append("'"); + } + + private void AppendValue(object value, StringBuilder sql) + { + if (value is byte[]) + { + AppendValue((byte[])value, sql); + } + else if (value is bool) + { + AppendValue((bool)value, sql); + } + else if (value is DateTime) + { + AppendValue((DateTime)value, sql); + } + else if (value is DateTimeOffset) + { + AppendValue((DateTimeOffset)value, sql); + } + else if (value is Guid) + { + AppendValue((Guid)value, sql); + } + else if (value is string) + { + AppendValue((string)value, sql); + } + else if (value is TimeSpan) + { + AppendValue((TimeSpan)value, sql); + } + else if (value is DbGeometry) + { + AppendValue((DbGeometry)value, sql); + } + else + { + sql.Append(string.Format(CultureInfo.InvariantCulture, "{0}", value)); + } + } + + #endregion + } +} diff --git a/Npgsql.EntityFramework/NpgsqlProviderManifest.Manifest.xml b/Npgsql.EntityFramework/NpgsqlProviderManifest.Manifest.xml index 32e2698820..78e4d75a45 100644 --- a/Npgsql.EntityFramework/NpgsqlProviderManifest.Manifest.xml +++ b/Npgsql.EntityFramework/NpgsqlProviderManifest.Manifest.xml @@ -52,6 +52,12 @@ + + + + + + diff --git a/Npgsql.EntityFramework/NpgsqlProviderManifest.cs b/Npgsql.EntityFramework/NpgsqlProviderManifest.cs index 6ff4223075..dbef2dec4e 100644 --- a/Npgsql.EntityFramework/NpgsqlProviderManifest.cs +++ b/Npgsql.EntityFramework/NpgsqlProviderManifest.cs @@ -140,6 +140,10 @@ public override TypeUsage GetEdmType(TypeUsage storeType) } return TypeUsage.CreateBinaryTypeUsage(primitiveType, false); } + case "rowversion": + { + return TypeUsage.CreateBinaryTypeUsage(primitiveType, true, 8); + } //TypeUsage.CreateBinaryTypeUsage //TypeUsage.CreateDateTimeTypeUsage //TypeUsage.CreateDecimalTypeUsage @@ -256,9 +260,9 @@ public override TypeUsage GetStoreType(TypeUsage edmType) } case PrimitiveTypeKind.Guid: return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["uuid"]); - // notably missing - // PrimitiveTypeKind.Byte: - // PrimitiveTypeKind.SByte: + case PrimitiveTypeKind.Byte: + case PrimitiveTypeKind.SByte: + return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["int2"]); } throw new NotSupportedException(); diff --git a/Npgsql.EntityFramework/NpgsqlServices.cs b/Npgsql.EntityFramework/NpgsqlServices.cs index 10fb892b7f..ec5c586160 100644 --- a/Npgsql.EntityFramework/NpgsqlServices.cs +++ b/Npgsql.EntityFramework/NpgsqlServices.cs @@ -6,6 +6,8 @@ using System.Data.Entity.Core.Common; using System.Data.Entity.Core.Common.CommandTrees; using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Migrations.Sql; +using System.Data.Entity.Infrastructure.DependencyResolution; #else using System.Data.Common.CommandTrees; using System.Data.Metadata.Edm; @@ -18,6 +20,14 @@ internal class NpgsqlServices : DbProviderServices { private static readonly NpgsqlServices _instance = new NpgsqlServices(); +#if ENTITIES6 + public NpgsqlServices() + { + AddDependencyResolver(new SingletonDependencyResolver>( + () => new NpgsqlMigrationSqlGenerator(), "Npgsql")); + } +#endif + public static NpgsqlServices Instance { get { return _instance; } @@ -28,7 +38,7 @@ protected override DbCommandDefinition CreateDbCommandDefinition(DbProviderManif return CreateCommandDefinition(CreateDbCommand(commandTree)); } - private DbCommand CreateDbCommand(DbCommandTree commandTree) + internal DbCommand CreateDbCommand(DbCommandTree commandTree) { if (commandTree == null) throw new ArgumentNullException("commandTree"); @@ -84,32 +94,72 @@ protected override string GetDbProviderManifestToken(DbConnection connection) { if (connection == null) throw new ArgumentNullException("connection"); - // TODO: used to use connection.ServerVersion - // that doesn't work with a closed connection. - bool openedConnection = false; - try + string serverVersion = ""; + UsingPostgresDBConnection((NpgsqlConnection)connection, conn => { - if (connection.State != System.Data.ConnectionState.Open) + serverVersion = conn.ServerVersion; + }); + return serverVersion; + } + + protected override DbProviderManifest GetDbProviderManifest(string versionHint) + { + if (versionHint == null) + throw new ArgumentNullException("versionHint"); + return new NpgsqlProviderManifest(versionHint); + } + +#if ENTITIES6 + protected override bool DbDatabaseExists(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) + { + bool exists = false; + UsingPostgresDBConnection((NpgsqlConnection)connection, conn => + { + using (NpgsqlCommand command = new NpgsqlCommand("select count(*) from pg_catalog.pg_database where datname = '" + connection.Database + "';", conn)) { - connection.Open(); - openedConnection = true; + exists = Convert.ToInt32(command.ExecuteScalar()) > 0; } - return connection.ServerVersion; - } - finally + }); + return exists; + } + + protected override void DbCreateDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) + { + UsingPostgresDBConnection((NpgsqlConnection)connection, conn => { - if (openedConnection) + using (NpgsqlCommand command = new NpgsqlCommand("CREATE DATABASE \"" + connection.Database + "\";", conn)) { - connection.Close(); + command.ExecuteNonQuery(); } - } + }); } - protected override DbProviderManifest GetDbProviderManifest(string versionHint) + protected override void DbDeleteDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection) { - if (versionHint == null) - throw new ArgumentNullException("versionHint"); - return new NpgsqlProviderManifest(versionHint); + UsingPostgresDBConnection((NpgsqlConnection)connection, conn => + { + //Close all connections in pool or exception "database used by another user appears" + NpgsqlConnection.ClearAllPools(); + using (NpgsqlCommand command = new NpgsqlCommand("DROP DATABASE \"" + connection.Database + "\";", conn)) + { + command.ExecuteNonQuery(); + } + }); + } +#endif + + private static void UsingPostgresDBConnection(NpgsqlConnection connection, Action action) + { + var connectionBuilder = new NpgsqlConnectionStringBuilder(connection.ConnectionString) + { + Database = "postgres" + }; + + using (var masterConnection = new NpgsqlConnection(connectionBuilder.ConnectionString)) + { + masterConnection.Open();//using's Dispose will close it even if exception... + action(masterConnection); + } } } } diff --git a/Npgsql.EntityFramework/SqlGenerators/SqlBaseGenerator.cs b/Npgsql.EntityFramework/SqlGenerators/SqlBaseGenerator.cs index 23c16349af..29b77a3a49 100644 --- a/Npgsql.EntityFramework/SqlGenerators/SqlBaseGenerator.cs +++ b/Npgsql.EntityFramework/SqlGenerators/SqlBaseGenerator.cs @@ -825,6 +825,8 @@ protected string GetDbType(EdmType edmType) { case PrimitiveTypeKind.Boolean: return "bool"; + case PrimitiveTypeKind.SByte: + case PrimitiveTypeKind.Byte: case PrimitiveTypeKind.Int16: return "int2"; case PrimitiveTypeKind.Int32: diff --git a/Npgsql/Npgsql/NpgsqlDataReader.cs b/Npgsql/Npgsql/NpgsqlDataReader.cs index a8147f48aa..208c1fdac7 100644 --- a/Npgsql/Npgsql/NpgsqlDataReader.cs +++ b/Npgsql/Npgsql/NpgsqlDataReader.cs @@ -403,11 +403,11 @@ public override Boolean GetBoolean(Int32 i) } /// - /// Gets the value of a column as Byte. Not implemented. + /// Gets the value of a column as Byte. /// public override Byte GetByte(Int32 i) { - throw new NotImplementedException(); + return (Byte)GetValue(i); } /// diff --git a/tests/App.config b/tests/App.config new file mode 100644 index 0000000000..015c9b4d35 --- /dev/null +++ b/tests/App.config @@ -0,0 +1,20 @@ + + + +
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/EntityFrameworkBasicTests.cs b/tests/EntityFrameworkBasicTests.cs new file mode 100644 index 0000000000..032403a9e0 --- /dev/null +++ b/tests/EntityFrameworkBasicTests.cs @@ -0,0 +1,221 @@ +#if NET40 +using Npgsql; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Data.Entity; +using System.Linq; +using System.Text; + +namespace NpgsqlTests +{ + [TestFixture] + public class EntityFrameworkBasicTests : TestBase + { + public EntityFrameworkBasicTests(string backendVersion) + : base(backendVersion) + { + } + + [TestFixtureSetUp] + public override void TestFixtureSetup() + { + base.TestFixtureSetup(); + using (var context = new BloggingContext(ConnectionStringEF)) + { + if (context.Database.Exists()) + context.Database.Delete();//We delete to be 100% schema is synced + context.Database.Create(); + } + } + + /// + /// Clean any previous entites before our test + /// + [SetUp] + protected override void SetUp() + { + base.SetUp(); + using (var context = new BloggingContext(ConnectionStringEF)) + { + context.Blogs.RemoveRange(context.Blogs); + context.Posts.RemoveRange(context.Posts); + context.SaveChanges(); + } + } + + public class Blog + { + public int BlogId { get; set; } + public string Name { get; set; } + + public virtual List Posts { get; set; } + } + + public class Post + { + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public byte Rating { get; set; } + + public int BlogId { get; set; } + public virtual Blog Blog { get; set; } + } + + public class BloggingContext : DbContext + { + public BloggingContext(string connection) + : base(new NpgsqlConnection(connection), true) + { + } + + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + } + + [Test] + public void InsertAndSelect() + { + using (var context = new BloggingContext(ConnectionStringEF)) + { + var blog = new Blog() + { + Name = "Some blog name" + }; + blog.Posts = new List(); + for (int i = 0; i < 5; i++) + blog.Posts.Add(new Post() + { + Content = "Some post content " + i, + Rating = (byte)i, + Title = "Some post Title " + i + }); + context.Blogs.Add(blog); + context.SaveChanges(); + } + + using (var context = new BloggingContext(ConnectionStringEF)) + { + var posts = from p in context.Posts + select p; + Assert.AreEqual(5, posts.Count()); + foreach (var post in posts) + { + StringAssert.StartsWith("Some post Title ", post.Title); + } + } + } + + [Test] + public void SelectWithWhere() + { + using (var context = new BloggingContext(ConnectionStringEF)) + { + var blog = new Blog() + { + Name = "Some blog name" + }; + blog.Posts = new List(); + for (int i = 0; i < 5; i++) + blog.Posts.Add(new Post() + { + Content = "Some post content " + i, + Rating = (byte)i, + Title = "Some post Title " + i + }); + context.Blogs.Add(blog); + context.SaveChanges(); + } + + using (var context = new BloggingContext(ConnectionStringEF)) + { + var posts = from p in context.Posts + where p.Rating < 3 + select p; + Assert.AreEqual(3, posts.Count()); + foreach (var post in posts) + { + Assert.Less(post.Rating, 3); + } + } + } + + [Test] + public void OrderBy() + { + using (var context = new BloggingContext(ConnectionStringEF)) + { + Random random = new Random(); + var blog = new Blog() + { + Name = "Some blog name" + }; + + blog.Posts = new List(); + for (int i = 0; i < 10; i++) + blog.Posts.Add(new Post() + { + Content = "Some post content " + i, + Rating = (byte)random.Next(0, 255), + Title = "Some post Title " + i + }); + context.Blogs.Add(blog); + context.SaveChanges(); + } + + using (var context = new BloggingContext(ConnectionStringEF)) + { + var posts = from p in context.Posts + orderby p.Rating + select p; + Assert.AreEqual(10, posts.Count()); + byte previousValue = 0; + foreach (var post in posts) + { + Assert.GreaterOrEqual(post.Rating, previousValue); + previousValue = post.Rating; + } + } + } + + [Test] + public void OrderByThenBy() + { + using (var context = new BloggingContext(ConnectionStringEF)) + { + Random random = new Random(); + var blog = new Blog() + { + Name = "Some blog name" + }; + + blog.Posts = new List(); + for (int i = 0; i < 10; i++) + blog.Posts.Add(new Post() + { + Content = "Some post content " + i, + Rating = (byte)random.Next(0, 255), + Title = "Some post Title " + (i % 3) + }); + context.Blogs.Add(blog); + context.SaveChanges(); + } + + using (var context = new BloggingContext(ConnectionStringEF)) + { + var posts = context.Posts.AsQueryable().OrderBy((p) => p.Title).ThenByDescending((p) => p.Rating); + Assert.AreEqual(10, posts.Count()); + foreach (var post in posts) + { + //TODO: Check outcome + Console.WriteLine(post.Title + " " + post.Rating); + } + } + } + + //Hunting season is open Happy hunting on OrderBy,GroupBy,Min,Max,Skip,Take,ThenBy... and all posible combinations + } +} +#endif \ No newline at end of file diff --git a/tests/EntityFrameworkMigrationTests.cs b/tests/EntityFrameworkMigrationTests.cs new file mode 100644 index 0000000000..c669d79db0 --- /dev/null +++ b/tests/EntityFrameworkMigrationTests.cs @@ -0,0 +1,772 @@ +// EntityFrameworkMigrationTests.cs +// +// Author: +// David Karlaš (david.karlas@gmail.com) +// +// Copyright (C) 2014 David Karlaš +// +// Permission to use, copy, modify, and distribute this software and its +// documentation for any purpose, without fee, and without a written +// agreement is hereby granted, provided that the above copyright notice +// and this paragraph and the following two paragraphs appear in all copies. +// +// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY +// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF +// THE POSSIBILITY OF SUCH DAMAGE. +// +// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS +// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +#if NET40 +using Npgsql; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data.Common; +using System.Data.Entity; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Migrations; +using System.Data.Entity.Migrations.History; +using System.Data.Entity.Migrations.Model; +using System.Data.Entity.Spatial; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; + +namespace NpgsqlTests +{ + [TestFixture] + public class EntityFrameworkMigrationTests : TestBase + { + public EntityFrameworkMigrationTests(string backendVersion) : base(backendVersion) { } + + #region Helper method + + /// + /// Helper function which I put behind var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + /// after filling operations list run test and then copy-paste content from D:\temp\printedCode.txt after var statments = new Np.. + /// Ofcourse check if generated code makes sense or UnitTests would be useless :D + /// + /// + private void PrintCode(IEnumerable statments) + { + + using (var SW = new System.IO.StreamWriter(@"D:\temp\printedCode.txt")) + { + SW.WriteLine("Assert.AreEqual(" + statments.Count() + ", statments.Count());"); + int i = 0; + foreach (var statement in statments) + SW.WriteLine("Assert.AreEqual(\"" + statement.Sql.Replace("\"", "\\\"") + "\", statments.ElementAt(" + i++ + ").Sql);"); + } + Assert.Fail(); + } + + #endregion + + #region Actual test against database + + [Test] + public void CreateBloggingContext() + { + using (var db = new BloggingContext(new NpgsqlConnection(ConnectionStringEF))) + { + if (!(db.Database.Connection is NpgsqlConnection)) + { + Assert.Fail( + "Connection type is \"" + db.Database.Connection.GetType().FullName + "\" should be \"" + typeof(NpgsqlConnection).FullName + "\"." + Environment.NewLine + + "Most likely wrong configuration in App.config file of Tests project."); + } + db.Database.Delete(); + var blog = new Blog { Name = "blogNameTest1" }; + db.Blogs.Add(blog); + db.SaveChanges(); + + var query = from b in db.Blogs + where b.Name == "blogNameTest1" + select b; + Assert.AreEqual(1, query.Count()); + + db.Database.Connection.Open(); + using (var command = db.Database.Connection.CreateCommand()) + { + command.CommandText = "select column_name, data_type, is_nullable, column_default from information_schema.columns where table_name = 'Blogs';"; + List expectedColumns = new List(new string[] { "Name", "BlogId" }); + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + Console.WriteLine((string)reader[0] + " " + (string)reader[1] + " " + (string)reader[2] + " " + (reader[3] ?? "").ToString()); + switch ((string)reader[0]) + { + case "Name": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("text", (string)reader[1]); + Assert.AreEqual("YES", (string)reader[2]); + Assert.IsNullOrEmpty(reader[3] as string); + break; + case "BlogId": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("integer", (string)reader[1]); + Assert.AreEqual("NO", (string)reader[2]); + Assert.AreEqual("nextval('dbo.\"Blogs_BlogId_seq\"'::regclass)", reader[3] as string); + break; + default: + Assert.Fail("Unknown column '" + (string)reader[0] + "' in Blogs table."); + break; + } + } + } + foreach (var columnName in expectedColumns) + { + Assert.Fail("Column '" + columnName + "' was not created in Blogs table."); + } + } + + using (var command = db.Database.Connection.CreateCommand()) + { + command.CommandText = "select column_name, data_type, is_nullable, column_default from information_schema.columns where table_name = 'Posts';"; + List expectedColumns = new List(new string[] { "PostId", "Title", "Content", "BlogId" }); + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + Console.WriteLine((string)reader[0] + " " + (string)reader[1] + " " + (string)reader[2] + " " + (reader[3] ?? "").ToString()); + switch ((string)reader[0]) + { + case "PostId": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("integer", (string)reader[1]); + Assert.AreEqual("NO", (string)reader[2]); + Assert.AreEqual("nextval('dbo.\"Posts_PostId_seq\"'::regclass)", (string)reader[3]); + break; + case "Title": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("text", (string)reader[1]); + Assert.AreEqual("YES", (string)reader[2]); + Assert.IsNullOrEmpty(reader[3] as string); + break; + case "Content": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("text", (string)reader[1]); + Assert.AreEqual("YES", (string)reader[2]); + Assert.IsNullOrEmpty(reader[3] as string); + break; + case "BlogId": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("integer", (string)reader[1]); + Assert.AreEqual("NO", (string)reader[2]); + Assert.AreEqual("0", (string)reader[3]); + break; + case "UniqueId": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("uuid", (string)reader[1]); + Assert.AreEqual("NO", (string)reader[2]); + Assert.AreEqual("'00000000-0000-0000-0000-000000000000'::uuid", reader[3] as string); + //Assert.AreEqual("uuid_generate_v4()", reader[3] as string); + break; + case "Rating": + expectedColumns.Remove((string)reader[0]); + Assert.AreEqual("smallint", (string)reader[1]); + Assert.AreEqual("YES", (string)reader[2]); + Assert.IsNullOrEmpty(reader[3] as string); + break; + default: + Assert.Fail("Unknown column '" + (string)reader[0] + "' in Post table."); + break; + } + } + } + foreach (var columnName in expectedColumns) + { + Assert.Fail("Column '" + columnName + "' was not created in Posts table."); + } + } + } + } + + + + public class Blog + { + public int BlogId { get; set; } + public string Name { get; set; } + + public virtual List Posts { get; set; } + } + + public class Post + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + //[DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid UniqueId { get; set; } + public byte? Rating { get; set; } + + public int BlogId { get; set; } + public virtual Blog Blog { get; set; } + } + + public class BloggingContext : DbContext + { + public BloggingContext(DbConnection connection) + : base(connection, true) + { + } + + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + } + + #endregion + + [Test] + public void DatabaseExistsCreateDelete() + { + using (var db = new BloggingContext(new NpgsqlConnection(ConnectionStringEF))) + { + if (db.Database.Exists()) + { + db.Database.Delete(); + Assert.IsFalse(db.Database.Exists()); + db.Database.Create(); + } + else + { + db.Database.Create(); + } + Assert.IsTrue(db.Database.Exists()); + db.Database.Delete(); + Assert.IsFalse(db.Database.Exists()); + } + } + + [Test] + public void TestAddColumnOperation() + { + var operations = new List(); + operations.Add(new AddColumnOperation("tableName", new ColumnModel(PrimitiveTypeKind.Double) + { + Name = "columnName" + })); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"tableName\" ADD \"columnName\" float8", statments.ElementAt(0).Sql); + } + + [Test] + public void TestAddColumnOperationDefaultValue() + { + var operations = new List(); + operations.Add(new AddColumnOperation("tableName", new ColumnModel(PrimitiveTypeKind.Single) + { + Name = "columnName", + DefaultValue = 4.4f + })); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"tableName\" ADD \"columnName\" float4 DEFAULT 4.4", statments.ElementAt(0).Sql); + } + + [Test] + public void TestAddColumnOperationDefaultValueSql() + { + var operations = new List(); + operations.Add(new AddColumnOperation("tableName", new ColumnModel(PrimitiveTypeKind.Single) + { + Name = "columnName", + DefaultValueSql = "4.6" + })); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"tableName\" ADD \"columnName\" float4 DEFAULT 4.6", statments.ElementAt(0).Sql); + } + + [Test] + public void TestAlterColumnOperation() + { + var operations = new List(); + operations.Add(new AlterColumnOperation("tableName", new ColumnModel(PrimitiveTypeKind.Double) + { + Name = "columnName" + }, false)); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(3, statments.Count()); + Assert.AreEqual("ALTER TABLE \"tableName\" ALTER COLUMN \"columnName\" TYPE float8", statments.ElementAt(0).Sql); + Assert.AreEqual("ALTER TABLE \"tableName\" ALTER COLUMN \"columnName\" DROP NOT NULL", statments.ElementAt(1).Sql); + Assert.AreEqual("ALTER TABLE \"tableName\" ALTER COLUMN \"columnName\" DROP DEFAULT", statments.ElementAt(2).Sql); + } + + [Test] + public void TestAlterColumnOperationDefaultAndNullable() + { + var operations = new List(); + operations.Add(new AlterColumnOperation("tableName", new ColumnModel(PrimitiveTypeKind.Double) + { + Name = "columnName", + DefaultValue = 2.3, + IsNullable = false + }, false)); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(3, statments.Count()); + Assert.AreEqual("ALTER TABLE \"tableName\" ALTER COLUMN \"columnName\" TYPE float8", statments.ElementAt(0).Sql); + Assert.AreEqual("ALTER TABLE \"tableName\" ALTER COLUMN \"columnName\" SET NOT NULL", statments.ElementAt(1).Sql); + Assert.AreEqual("ALTER TABLE \"tableName\" ALTER COLUMN \"columnName\" SET DEFAULT 2.3", statments.ElementAt(2).Sql); + } + + [Test] + public void TestCreateTableOperation() + { + var operations = new List(); + var operation = new CreateTableOperation("someSchema.someTable"); + + operation.Columns.Add( + new ColumnModel(PrimitiveTypeKind.String) + { + Name = "SomeString", + MaxLength = 233, + IsNullable = false + }); + + operation.Columns.Add( + new ColumnModel(PrimitiveTypeKind.String) + { + Name = "AnotherString", + IsNullable = true + }); + + operation.Columns.Add( + new ColumnModel(PrimitiveTypeKind.Binary) + { + Name = "SomeBytes" + }); + + operation.Columns.Add( + new ColumnModel(PrimitiveTypeKind.Int64) + { + Name = "SomeLong", + IsIdentity = true + }); + + operation.Columns.Add( + new ColumnModel(PrimitiveTypeKind.DateTime) + { + Name = "SomeDateTime" + }); + + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(2, statments.Count()); + if (BackendVersion.Major > 9 || (BackendVersion.Major == 9 && BackendVersion.Minor > 2)) + Assert.AreEqual("CREATE SCHEMA IF NOT EXISTS someSchema", statments.ElementAt(0).Sql); + else + Assert.AreEqual("CREATE SCHEMA someSchema", statments.ElementAt(0).Sql); + Assert.AreEqual("CREATE TABLE \"someSchema\".\"someTable\"(\"SomeString\" varchar(233) NOT NULL DEFAULT '',\"AnotherString\" text,\"SomeBytes\" bytea,\"SomeLong\" serial8,\"SomeDateTime\" timestamp)", statments.ElementAt(1).Sql); + } + + [Test] + public void TestDropColumnOperation() + { + var operations = new List(); + operations.Add(new DropColumnOperation("someTable", "someColumn")); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someTable\" DROP COLUMN \"someColumn\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestDropTableOperation() + { + var operations = new List(); + operations.Add(new DropTableOperation("someTable")); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("DROP TABLE \"someTable\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestRenameTableOperation() + { + var operations = new List(); + operations.Add(new RenameTableOperation("schema.someOldTableName", "someNewTablename")); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"schema\".\"someOldTableName\" RENAME TO \"someNewTablename\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestHistoryOperation() + { + var operations = new List(); + //TODO: fill operations + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + //TODO: check statments + } + + [Test] + public void TestDropIndexOperation() + { + var operations = new List(); + operations.Add(new DropIndexOperation() + { + Name = "someIndex", + Table = "someTable" + }); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("DROP INDEX IF EXISTS dto.\"someTable_someIndex\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestDropIndexOperationTableNameWithSchema() + { + var operations = new List(); + operations.Add(new DropIndexOperation() + { + Name = "someIndex", + Table = "someSchema.someTable" + }); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("DROP INDEX IF EXISTS someSchema.\"someTable_someIndex\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestCreateIndexOperation() + { + var operations = new List(); + var operation = new CreateIndexOperation(); + operation.Table = "someTable"; + operation.Name = "someIndex"; + operation.Columns.Add("column1"); + operation.Columns.Add("column2"); + operation.Columns.Add("column3"); + operation.IsUnique = false; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("CREATE INDEX \"someTable_someIndex\" ON \"someTable\" (\"column1\",\"column2\",\"column3\")", statments.ElementAt(0).Sql); + } + + [Test] + public void TestCreateIndexOperationUnique() + { + var operations = new List(); + var operation = new CreateIndexOperation(); + operation.Table = "someTable"; + operation.Name = "someIndex"; + operation.Columns.Add("column1"); + operation.Columns.Add("column2"); + operation.Columns.Add("column3"); + operation.IsUnique = true; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("CREATE UNIQUE INDEX \"someTable_someIndex\" ON \"someTable\" (\"column1\",\"column2\",\"column3\")", statments.ElementAt(0).Sql); + } + + [Test] + public void TestMoveTableOperation() + { + var operations = new List(); + operations.Add(new MoveTableOperation("someOldSchema.someTable", "someNewSchema")); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(2, statments.Count()); + if (BackendVersion.Major > 9 || (BackendVersion.Major == 9 && BackendVersion.Minor > 2)) + Assert.AreEqual("CREATE SCHEMA IF NOT EXISTS someNewSchema", statments.ElementAt(0).Sql); + else + Assert.AreEqual("CREATE SCHEMA someNewSchema", statments.ElementAt(0).Sql); + Assert.AreEqual("ALTER TABLE \"someOldSchema\".\"someTable\" SET SCHEMA someNewSchema", statments.ElementAt(1).Sql); + } + + [Test] + public void TestMoveTableOperationNewSchemaIsNull() + { + var operations = new List(); + operations.Add(new MoveTableOperation("someOldSchema.someTable", null)); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(2, statments.Count()); + if (BackendVersion.Major > 9 || (BackendVersion.Major == 9 && BackendVersion.Minor > 2)) + Assert.AreEqual("CREATE SCHEMA IF NOT EXISTS dbo", statments.ElementAt(0).Sql); + else + Assert.AreEqual("CREATE SCHEMA dbo", statments.ElementAt(0).Sql); + Assert.AreEqual("ALTER TABLE \"someOldSchema\".\"someTable\" SET SCHEMA dbo", statments.ElementAt(1).Sql); + } + + [Test] + public void TestAddPrimaryKeyOperation() + { + var operations = new List(); + var operation = new AddPrimaryKeyOperation(); + operation.Table = "someTable"; + operation.Name = "somePKName"; + operation.Columns.Add("column1"); + operation.Columns.Add("column2"); + operation.Columns.Add("column3"); + operation.IsClustered = false; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD CONSTRAINT \"somePKName\" PRIMARY KEY (\"column1\",\"column2\",\"column3\")", statments.ElementAt(0).Sql); + } + + [Test] + public void TestAddPrimaryKeyOperationClustered() + { + var operations = new List(); + var operation = new AddPrimaryKeyOperation(); + operation.Table = "someTable"; + operation.Name = "somePKName"; + operation.Columns.Add("column1"); + operation.Columns.Add("column2"); + operation.Columns.Add("column3"); + operation.IsClustered = true; + //TODO: PostgreSQL support something like IsClustered? + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD CONSTRAINT \"somePKName\" PRIMARY KEY (\"column1\",\"column2\",\"column3\")", statments.ElementAt(0).Sql); + } + + [Test] + public void TestDropPrimaryKeyOperation() + { + var operations = new List(); + var operation = new DropPrimaryKeyOperation(); + operation.Table = "someTable"; + operation.Name = "somePKName"; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someTable\" DROP CONSTRAINT \"somePKName\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestRenameColumnOperation() + { + var operations = new List(); + operations.Add(new RenameColumnOperation("someTable", "someOldColumnName", "someNewColumnName")); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someTable\" RENAME COLUMN \"someOldColumnName\" TO \"someNewColumnName\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestSqlOperation() + { + var operations = new List(); + operations.Add(new SqlOperation("SELECT someColumn FROM someTable")); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("SELECT someColumn FROM someTable", statments.ElementAt(0).Sql); + } + + [Test] + public void TestUpdateDatabaseOperation() + { + var operations = new List(); + //TODO: fill operations + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + //TODO: check statments + } + + [Test] + public void TestAddForeignKeyOperation() + { + var operations = new List(); + var operation = new AddForeignKeyOperation(); + operation.Name = "someFK"; + operation.PrincipalTable = "somePrincipalTable"; + operation.DependentTable = "someDependentTable"; + operation.DependentColumns.Add("column1"); + operation.DependentColumns.Add("column2"); + operation.DependentColumns.Add("column3"); + operation.CascadeDelete = false; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someDependentTable\" ADD CONSTRAINT \"someFK\" FOREIGN KEY (\"column1\",\"column2\",\"column3\") REFERENCES \"somePrincipalTable\" )", statments.ElementAt(0).Sql); + } + + [Test] + public void TestAddForeignKeyOperationCascadeDelete() + { + var operations = new List(); + var operation = new AddForeignKeyOperation(); + operation.Name = "someFK"; + operation.PrincipalTable = "somePrincipalTable"; + operation.DependentTable = "someDependentTable"; + operation.DependentColumns.Add("column1"); + operation.DependentColumns.Add("column2"); + operation.DependentColumns.Add("column3"); + operation.CascadeDelete = true; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someDependentTable\" ADD CONSTRAINT \"someFK\" FOREIGN KEY (\"column1\",\"column2\",\"column3\") REFERENCES \"somePrincipalTable\" ) ON DELETE CASCADE", statments.ElementAt(0).Sql); + } + + + [Test] + public void TestDropForeignKeyOperation() + { + var operations = new List(); + var operation = new DropForeignKeyOperation(); + operation.Name = "someFK"; + operation.DependentTable = "someTable"; + operations.Add(operation); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(1, statments.Count()); + if (BackendVersion.Major > 8) + Assert.AreEqual("ALTER TABLE \"someTable\" DROP CONSTRAINT IF EXISTS \"someFK\"", statments.ElementAt(0).Sql); + else + Assert.AreEqual("ALTER TABLE \"someTable\" DROP CONSTRAINT \"someFK\"", statments.ElementAt(0).Sql); + } + + [Test] + public void TestDefaultTypes() + { + var operations = new List(); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Binary) + { + Name = "someByteaColumn", + DefaultValue = new byte[6] { 1, 2, 127, 128, 254, 255 } + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Boolean) + { + Name = "someFalseBooleanColumn", + DefaultValue = false + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Boolean) + { + Name = "someTrueBooleanColumn", + DefaultValue = true + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Byte) + { + Name = "someByteColumn", + DefaultValue = 15 + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.DateTime) + { + Name = "someDateTimeColumn", + DefaultValue = new DateTime(2014, 1, 31, 5, 15, 23, 435) + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.DateTimeOffset) + { + Name = "someDateTimeOffsetColumn", + DefaultValue = new DateTimeOffset(new DateTime(2014, 1, 31, 5, 18, 43, 186), TimeSpan.FromHours(1)) + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Decimal) + { + Name = "someDecimalColumn", + DefaultValue = 23432423.534534m + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Double) + { + Name = "someDoubleColumn", + DefaultValue = 44.66 + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Guid) + { + Name = "someGuidColumn", + DefaultValue = new Guid("de303070-afb8-4ec1-bcb0-c637f3316501") + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Int16) + { + Name = "someInt16Column", + DefaultValue = 16 + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Int32) + { + Name = "someInt32Column", + DefaultValue = 32 + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Int64) + { + Name = "someInt64Column", + DefaultValue = 64 + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.SByte) + { + Name = "someSByteColumn", + DefaultValue = -24 + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Single) + { + Name = "someSingleColumn", + DefaultValue = 12.4f + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.String) + { + Name = "someStringColumn", + DefaultValue = "Hello EF" + }, false) + ); + operations.Add(new AddColumnOperation("someTable", + new ColumnModel(PrimitiveTypeKind.Time) + { + Name = "someColumn", + DefaultValue = new TimeSpan(937840050067)//1 day, 2 hours, 3 minutes, 4 seconds, 5 miliseconds, 6 microseconds, 700 nanoseconds + }, false) + ); + var statments = new NpgsqlMigrationSqlGenerator().Generate(operations, BackendVersion.ToString()); + Assert.AreEqual(16, statments.Count()); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someByteaColumn\" bytea DEFAULT E'\\\\01027F80FEFF'", statments.ElementAt(0).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someFalseBooleanColumn\" boolean DEFAULT FALSE", statments.ElementAt(1).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someTrueBooleanColumn\" boolean DEFAULT TRUE", statments.ElementAt(2).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someByteColumn\" int2 DEFAULT 15", statments.ElementAt(3).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someDateTimeColumn\" timestamp DEFAULT '2014-01-31 05:15:23.435'", statments.ElementAt(4).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someDateTimeOffsetColumn\" timestamptz DEFAULT '2014-01-31 05:18:43.186+01'", statments.ElementAt(5).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someDecimalColumn\" numeric DEFAULT 23432423.534534", statments.ElementAt(6).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someDoubleColumn\" float8 DEFAULT 44.66", statments.ElementAt(7).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someGuidColumn\" uuid DEFAULT 'de303070-afb8-4ec1-bcb0-c637f3316501'", statments.ElementAt(8).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someInt16Column\" int2 DEFAULT 16", statments.ElementAt(9).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someInt32Column\" int4 DEFAULT 32", statments.ElementAt(10).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someInt64Column\" int8 DEFAULT 64", statments.ElementAt(11).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someSByteColumn\" int2 DEFAULT -24", statments.ElementAt(12).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someSingleColumn\" float4 DEFAULT 12.4", statments.ElementAt(13).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someStringColumn\" text DEFAULT 'Hello EF'", statments.ElementAt(14).Sql); + Assert.AreEqual("ALTER TABLE \"someTable\" ADD \"someColumn\" interval DEFAULT '26:03:04.005007'", statments.ElementAt(15).Sql); + + } + } +} +#endif \ No newline at end of file diff --git a/tests/NpgsqlTests.csproj b/tests/NpgsqlTests.csproj index 1b380a8632..63fce33c07 100644 --- a/tests/NpgsqlTests.csproj +++ b/tests/NpgsqlTests.csproj @@ -81,6 +81,7 @@ true 4 v4.0 + NET35;NET40 bin\Release-net40\ @@ -90,6 +91,7 @@ true 4 v4.0 + NET35;NET40 bin\Debug-net45\ @@ -101,6 +103,7 @@ true 4 v4.5 + NET35;NET40;NET45 bin\Release-net45\ @@ -110,6 +113,7 @@ true 4 v4.5 + NET35;NET40;NET45 bin\Debug-net45-EF6\ @@ -121,6 +125,7 @@ true 4 v4.5 + NET35;NET40;NET45 bin\Release-net45-EF6\ @@ -130,12 +135,18 @@ true 4 v4.5 + NET35;NET40;NET45 + + ..\packages\EntityFramework.6.0.1\lib\net40\EntityFramework.dll + + + ..\packages\EntityFramework.6.0.1\lib\net40\EntityFramework.SqlServer.dll + ..\packages\NUnit.2.6.2\lib\nunit.framework.dll - 3.5 @@ -154,6 +165,8 @@ + + Code @@ -175,6 +188,7 @@ + Always @@ -214,6 +228,10 @@ + + {3ec85cba-5b79-11e3-8104-0022198ab089} + Npgsql.EntityFramework + {9D13B739-62B1-4190-B386-7A9547304EB3} Npgsql2012 diff --git a/tests/TestBase.cs b/tests/TestBase.cs index 0742a7415e..f02e401d38 100644 --- a/tests/TestBase.cs +++ b/tests/TestBase.cs @@ -69,6 +69,27 @@ protected TestBase(string backendVersion) protected virtual string ConnectionString { get { return _connectionString; } } private string _connectionString; + + /// + /// New ConectionString property crafted to change the database name from original TestBase.ConnectionString to append a "_ef" suffix. + /// i.e.: the TestBase.ConnectionString database is npgsql_tests. Entity Framework database will be npgsql_tests_ef. + /// + protected virtual string ConnectionStringEF + { + get + { + if (connectionStringEF == null) + { + //Reuse all strings just add _ef at end of database name for + var connectionSB = new NpgsqlConnectionStringBuilder(ConnectionString); + connectionSB.Database += "_ef"; + connectionStringEF = connectionSB.ConnectionString; + } + return connectionStringEF; + } + } + private string connectionStringEF; + /// /// Unless the NPGSQL_TEST_DB environment variable is defined, this is used as the connection string for the /// test database. @@ -84,7 +105,7 @@ protected TestBase(string backendVersion) #region Setup / Teardown [TestFixtureSetUp] - public void TestFixtureSetup() + public virtual void TestFixtureSetup() { var connStringEnvVar = "NPGSQL_TEST_DB_" + BackendVersion; _connectionString = Environment.GetEnvironmentVariable(connStringEnvVar); diff --git a/tests/packages.config b/tests/packages.config index 5c3ca54dd7..fbdacc1a3b 100644 --- a/tests/packages.config +++ b/tests/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file