diff --git a/src/NHibernate.Test/Async/Tools/hbm2ddl/SchemaValidator/SchemaValidateTableWithSchemaFixture.cs b/src/NHibernate.Test/Async/Tools/hbm2ddl/SchemaValidator/SchemaValidateTableWithSchemaFixture.cs new file mode 100644 index 00000000000..3e719891bfe --- /dev/null +++ b/src/NHibernate.Test/Async/Tools/hbm2ddl/SchemaValidator/SchemaValidateTableWithSchemaFixture.cs @@ -0,0 +1,120 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using NHibernate.Dialect; +using NHibernate.Util; +using NUnit.Framework; + +namespace NHibernate.Test.Tools.hbm2ddl.SchemaValidator +{ + using System.Threading.Tasks; + [TestFixture] + public class SchemaValidateTableWithSchemaFixtureAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string[] Mappings => new[] { "Tools.hbm2ddl.SchemaValidator.VersionWithSchema.hbm.xml" }; + + protected override bool AppliesTo(Dialect.Dialect dialect) + { + switch (Dialect) + { + case MsSql2000Dialect _: + case PostgreSQLDialect _: + case SQLiteDialect _: + case MsSqlCeDialect _: + return true; + default: + // Firebird does not support schema. Its current dialect leave table name being schema prefixed, + // which causes SQL parse errors. It has a "create schema" command but instead creates a new + // database. + // MySql does not truly support schema. Its current dialect leave table name being schema prefixed, + // which is interpreted as a database name. It has a "create schema" command but instead creates + // a new database. + // Oracle tightly bounds schema to users. + return false; + } + } + + protected override void CreateSchema() + { + switch (Dialect) + { + case MsSql2000Dialect _: + case PostgreSQLDialect _: + // Must handle the schema manually: mapped database-objects are handled too late. + var cnx = Sfi.ConnectionProvider.GetConnection(); + try + { + using (var cmd = cnx.CreateCommand()) + { + cmd.CommandText = "create schema Test"; + cmd.ExecuteNonQuery(); + } + } + catch (Exception ex) + { + // Unfortunateley Assert.Warn and Console.WriteLine at this place seems to be ignored in Rider + // viewer. + Assert.Warn("Creating the schema failed, assuming it already exists. {0}", ex); + Console.WriteLine("Creating the schema failed, assuming it already exists."); + Console.WriteLine(ex); + } + finally + { + Sfi.ConnectionProvider.CloseConnection(cnx); + } + break; + } + base.CreateSchema(); + } + + protected override void DropSchema() + { + // SQL-Server does not need this call, but Postgres does not accept dropping a schema carrying objects. + base.DropSchema(); + + switch (Dialect) + { + case MsSql2000Dialect _: + case PostgreSQLDialect _: + var cnx = Sfi.ConnectionProvider.GetConnection(); + try + { + using (var cmd = cnx.CreateCommand()) + { + cmd.CommandText = "drop schema Test"; + cmd.ExecuteNonQuery(); + } + } + finally + { + Sfi.ConnectionProvider.CloseConnection(cnx); + } + break; + } + } + + [Test] + public async Task ShouldVerifyAsync() + { + var validator = new Tool.hbm2ddl.SchemaValidator(cfg); + try + { + await (validator.ValidateAsync()); + } + catch (SchemaValidationException sve) + { + Assert.Fail("Validation failed: {0}.\n{1}", StringHelper.CollectionToString(sve.ValidationErrors), sve); + } + } + } +} diff --git a/src/NHibernate.Test/Tools/hbm2ddl/SchemaValidator/SchemaValidateTableWithSchemaFixture.cs b/src/NHibernate.Test/Tools/hbm2ddl/SchemaValidator/SchemaValidateTableWithSchemaFixture.cs new file mode 100644 index 00000000000..87afcd89625 --- /dev/null +++ b/src/NHibernate.Test/Tools/hbm2ddl/SchemaValidator/SchemaValidateTableWithSchemaFixture.cs @@ -0,0 +1,109 @@ +using System; +using NHibernate.Dialect; +using NHibernate.Util; +using NUnit.Framework; + +namespace NHibernate.Test.Tools.hbm2ddl.SchemaValidator +{ + [TestFixture] + public class SchemaValidateTableWithSchemaFixture : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override string[] Mappings => new[] { "Tools.hbm2ddl.SchemaValidator.VersionWithSchema.hbm.xml" }; + + protected override bool AppliesTo(Dialect.Dialect dialect) + { + switch (Dialect) + { + case MsSql2000Dialect _: + case PostgreSQLDialect _: + case SQLiteDialect _: + case MsSqlCeDialect _: + return true; + default: + // Firebird does not support schema. Its current dialect leave table name being schema prefixed, + // which causes SQL parse errors. It has a "create schema" command but instead creates a new + // database. + // MySql does not truly support schema. Its current dialect leave table name being schema prefixed, + // which is interpreted as a database name. It has a "create schema" command but instead creates + // a new database. + // Oracle tightly bounds schema to users. + return false; + } + } + + protected override void CreateSchema() + { + switch (Dialect) + { + case MsSql2000Dialect _: + case PostgreSQLDialect _: + // Must handle the schema manually: mapped database-objects are handled too late. + var cnx = Sfi.ConnectionProvider.GetConnection(); + try + { + using (var cmd = cnx.CreateCommand()) + { + cmd.CommandText = "create schema Test"; + cmd.ExecuteNonQuery(); + } + } + catch (Exception ex) + { + // Unfortunateley Assert.Warn and Console.WriteLine at this place seems to be ignored in Rider + // viewer. + Assert.Warn("Creating the schema failed, assuming it already exists. {0}", ex); + Console.WriteLine("Creating the schema failed, assuming it already exists."); + Console.WriteLine(ex); + } + finally + { + Sfi.ConnectionProvider.CloseConnection(cnx); + } + break; + } + base.CreateSchema(); + } + + protected override void DropSchema() + { + // SQL-Server does not need this call, but Postgres does not accept dropping a schema carrying objects. + base.DropSchema(); + + switch (Dialect) + { + case MsSql2000Dialect _: + case PostgreSQLDialect _: + var cnx = Sfi.ConnectionProvider.GetConnection(); + try + { + using (var cmd = cnx.CreateCommand()) + { + cmd.CommandText = "drop schema Test"; + cmd.ExecuteNonQuery(); + } + } + finally + { + Sfi.ConnectionProvider.CloseConnection(cnx); + } + break; + } + } + + [Test] + public void ShouldVerify() + { + var validator = new Tool.hbm2ddl.SchemaValidator(cfg); + try + { + validator.Validate(); + } + catch (SchemaValidationException sve) + { + Assert.Fail("Validation failed: {0}.\n{1}", StringHelper.CollectionToString(sve.ValidationErrors), sve); + } + } + } +} diff --git a/src/NHibernate.Test/Tools/hbm2ddl/SchemaValidator/VersionWithSchema.hbm.xml b/src/NHibernate.Test/Tools/hbm2ddl/SchemaValidator/VersionWithSchema.hbm.xml new file mode 100644 index 00000000000..fe73b2f166e --- /dev/null +++ b/src/NHibernate.Test/Tools/hbm2ddl/SchemaValidator/VersionWithSchema.hbm.xml @@ -0,0 +1,17 @@ + + + + + + + uid_table + next_hi_value_column + + + + + + + diff --git a/src/NHibernate/Dialect/MsSqlCeDialect.cs b/src/NHibernate/Dialect/MsSqlCeDialect.cs index 88c1fa21be1..86407ba2530 100644 --- a/src/NHibernate/Dialect/MsSqlCeDialect.cs +++ b/src/NHibernate/Dialect/MsSqlCeDialect.cs @@ -263,7 +263,7 @@ public override bool SupportsLimitOffset public override IDataBaseSchema GetDataBaseSchema(DbConnection connection) { - return new MsSqlCeDataBaseSchema(connection); + return new MsSqlCeDataBaseSchema(connection, this); } public override SqlString GetLimitString(SqlString querySqlString, SqlString offset, SqlString limit) diff --git a/src/NHibernate/Dialect/SQLiteDialect.cs b/src/NHibernate/Dialect/SQLiteDialect.cs index 77d027987f7..802ef14eb6e 100644 --- a/src/NHibernate/Dialect/SQLiteDialect.cs +++ b/src/NHibernate/Dialect/SQLiteDialect.cs @@ -190,7 +190,7 @@ protected virtual void RegisterDefaultProperties() public override Schema.IDataBaseSchema GetDataBaseSchema(DbConnection connection) { - return new Schema.SQLiteDataBaseMetaData(connection); + return new Schema.SQLiteDataBaseMetaData(connection, this); } public override string AddColumnString diff --git a/src/NHibernate/Dialect/Schema/AbstractDataBaseSchema.cs b/src/NHibernate/Dialect/Schema/AbstractDataBaseSchema.cs index 5ae17dbd564..dc7caa8e925 100644 --- a/src/NHibernate/Dialect/Schema/AbstractDataBaseSchema.cs +++ b/src/NHibernate/Dialect/Schema/AbstractDataBaseSchema.cs @@ -15,20 +15,27 @@ namespace NHibernate.Dialect.Schema /// public abstract class AbstractDataBaseSchema : IDataBaseSchema { - private readonly DbConnection connection; + private readonly Dialect _dialect; - protected AbstractDataBaseSchema(DbConnection connection) - { - this.connection = connection; - } + protected AbstractDataBaseSchema(DbConnection connection) : this(connection, null) {} - protected DbConnection Connection + protected AbstractDataBaseSchema(DbConnection connection, Dialect dialect) { - get { return connection; } + Connection = connection; + _dialect = dialect; } + protected DbConnection Connection { get; } + public virtual bool IncludeDataTypesInReservedWords => true; + /// + /// Should be used for searching tables instead of using separately + /// the table, schema and catalog names? If , dialect must be provided + /// with . + /// + public virtual bool UseDialectQualifyInsteadOfTableName => false; + #region IDataBaseSchema Members public virtual bool StoresMixedCaseQuotedIdentifiers @@ -58,8 +65,36 @@ public virtual bool StoresLowerCaseIdentifiers public virtual DataTable GetTables(string catalog, string schemaPattern, string tableNamePattern, string[] types) { + if (UseDialectQualifyInsteadOfTableName) + { + var actualTablePattern = GetActualTableName(catalog, schemaPattern, tableNamePattern); + var tables = Connection.GetSchema("Tables", new[] { null, null, actualTablePattern }); + + // Caller may check the table name of yielded results, we need to patch them + foreach (DataRow tableRow in tables.Rows) + { + var tableName = Convert.ToString(tableRow[ColumnNameForTableName]); + if (tableName.Equals(actualTablePattern, StringComparison.InvariantCultureIgnoreCase)) + { + tableRow[ColumnNameForTableName] = tableNamePattern; + // Columns are looked-up according to the row table name, and schema and catalog data. + // We need to patch schema and catalog for being able to reconstruct the adequate table name. + if (!string.IsNullOrEmpty(catalog)) + { + tableRow["TABLE_CATALOG"] = catalog; + } + if (!string.IsNullOrEmpty(schemaPattern)) + { + tableRow["TABLE_SCHEMA"] = schemaPattern; + } + } + } + + return tables; + } + var restrictions = new[] { catalog, schemaPattern, tableNamePattern }; - return connection.GetSchema("Tables", restrictions); + return Connection.GetSchema("Tables", restrictions); } public virtual string ColumnNameForTableName @@ -72,32 +107,56 @@ public virtual string ColumnNameForTableName public virtual DataTable GetColumns(string catalog, string schemaPattern, string tableNamePattern, string columnNamePattern) { + if (UseDialectQualifyInsteadOfTableName) + { + var actualTablePattern = GetActualTableName(catalog, schemaPattern, tableNamePattern); + return Connection.GetSchema("Columns", new[] {null, null, actualTablePattern, columnNamePattern}); + } + var restrictions = new[] {catalog, schemaPattern, tableNamePattern, columnNamePattern}; - return connection.GetSchema("Columns", restrictions); + return Connection.GetSchema("Columns", restrictions); } public virtual DataTable GetIndexInfo(string catalog, string schemaPattern, string tableName) { + if (UseDialectQualifyInsteadOfTableName) + { + var actualTableName = GetActualTableName(catalog, schemaPattern, tableName); + return Connection.GetSchema("Indexes", new[] {null, null, actualTableName, null}); + } + var restrictions = new[] {catalog, schemaPattern, tableName, null}; - return connection.GetSchema("Indexes", restrictions); + return Connection.GetSchema("Indexes", restrictions); } public virtual DataTable GetIndexColumns(string catalog, string schemaPattern, string tableName, string indexName) { + if (UseDialectQualifyInsteadOfTableName) + { + var actualTableName = GetActualTableName(catalog, schemaPattern, tableName); + return Connection.GetSchema("IndexColumns", new[] {null, null, actualTableName, indexName, null}); + } + var restrictions = new[] {catalog, schemaPattern, tableName, indexName, null}; - return connection.GetSchema("IndexColumns", restrictions); + return Connection.GetSchema("IndexColumns", restrictions); } public virtual DataTable GetForeignKeys(string catalog, string schema, string table) { + if (UseDialectQualifyInsteadOfTableName) + { + var actualTableName = GetActualTableName(catalog, schema, table); + return Connection.GetSchema(ForeignKeysSchemaName, new[] {null, null, actualTableName, null}); + } + var restrictions = new[] {catalog, schema, table, null}; - return connection.GetSchema(ForeignKeysSchemaName, restrictions); + return Connection.GetSchema(ForeignKeysSchemaName, restrictions); } public virtual ISet GetReservedWords() { var result = new HashSet(StringComparer.OrdinalIgnoreCase); - DataTable dtReservedWords = connection.GetSchema(DbMetaDataCollectionNames.ReservedWords); + DataTable dtReservedWords = Connection.GetSchema(DbMetaDataCollectionNames.ReservedWords); foreach (DataRow row in dtReservedWords.Rows) { result.Add(row["ReservedWord"].ToString()); @@ -105,7 +164,7 @@ public virtual ISet GetReservedWords() if (IncludeDataTypesInReservedWords) { - DataTable dtTypes = connection.GetSchema(DbMetaDataCollectionNames.DataTypes); + DataTable dtTypes = Connection.GetSchema(DbMetaDataCollectionNames.DataTypes); foreach (DataRow row in dtTypes.Rows) { result.Add(row["TypeName"].ToString()); @@ -120,6 +179,16 @@ protected virtual string ForeignKeysSchemaName get { return "ForeignKeys"; } } + private string GetActualTableName(string catalog, string schemaPattern, string tableNamePattern) + { + if (_dialect == null) + throw new InvalidOperationException($"{this}: cannot qualify table name without the dialect"); + + // _dialect is supposed to concatenate catalog and schema with the table name as an + // unqualified name instead of actually qualifying it. + return _dialect.Qualify(catalog, schemaPattern, tableNamePattern); + } + #endregion } } diff --git a/src/NHibernate/Dialect/Schema/MsSqlCeMetaData.cs b/src/NHibernate/Dialect/Schema/MsSqlCeMetaData.cs index 12671a58d67..0269314eda4 100644 --- a/src/NHibernate/Dialect/Schema/MsSqlCeMetaData.cs +++ b/src/NHibernate/Dialect/Schema/MsSqlCeMetaData.cs @@ -6,7 +6,16 @@ namespace NHibernate.Dialect.Schema { public class MsSqlCeDataBaseSchema: AbstractDataBaseSchema { - public MsSqlCeDataBaseSchema(DbConnection connection) : base(connection) {} + // Since v5.2 + [Obsolete("Use overload with dialect argument.")] + public MsSqlCeDataBaseSchema(DbConnection connection) : this(connection, Dialect.GetDialect()) {} + + public MsSqlCeDataBaseSchema(DbConnection connection, Dialect dialect) : base(connection, dialect) + { + UseDialectQualifyInsteadOfTableName = dialect is MsSqlCeDialect; + } + + public override bool UseDialectQualifyInsteadOfTableName { get; } public override ITableMetadata GetTableMetadata(DataRow rs, bool extras) { diff --git a/src/NHibernate/Dialect/Schema/SQLiteMetaData.cs b/src/NHibernate/Dialect/Schema/SQLiteMetaData.cs index 8be4242acce..493e4aa7d07 100644 --- a/src/NHibernate/Dialect/Schema/SQLiteMetaData.cs +++ b/src/NHibernate/Dialect/Schema/SQLiteMetaData.cs @@ -6,7 +6,35 @@ namespace NHibernate.Dialect.Schema { public class SQLiteDataBaseMetaData : AbstractDataBaseSchema { - public SQLiteDataBaseMetaData(DbConnection connection) : base(connection) {} + // Since v5.2 + [Obsolete("Use overload with dialect argument.")] + public SQLiteDataBaseMetaData(DbConnection connection) : this(connection, Dialect.GetDialect()) {} + + public SQLiteDataBaseMetaData(DbConnection connection, Dialect dialect) : base(connection, dialect) + { + UseDialectQualifyInsteadOfTableName = dialect is SQLiteDialect; + } + + public override bool UseDialectQualifyInsteadOfTableName { get; } + + public override DataTable GetTables(string catalog, string schemaPattern, string tableNamePattern, string[] types) + { + var tables = base.GetTables(catalog, schemaPattern, tableNamePattern, types); + if (UseDialectQualifyInsteadOfTableName) + { + foreach (DataRow tableRow in tables.Rows) + { + var tableName = Convert.ToString(tableRow[ColumnNameForTableName]); + if (tableName.Equals(tableNamePattern, StringComparison.InvariantCultureIgnoreCase)) + { + // SQLite indicates "main" here by default, wrecking the other Get methods. + tableRow["TABLE_CATALOG"] = string.Empty; + } + } + } + + return tables; + } public override ITableMetadata GetTableMetadata(DataRow rs, bool extras) { @@ -93,4 +121,4 @@ public SQLiteForeignKeyMetaData(DataRow rs) : base(rs) Name = Convert.ToString(rs["CONSTRAINT_NAME"]); } } -} \ No newline at end of file +}