diff --git a/Qsi.Tests/Vendor/PostgreSql/Driver/PostgreSqlRepositoryProvider.cs b/Qsi.Tests/Vendor/PostgreSql/Driver/PostgreSqlRepositoryProvider.cs index 4541cd2b..1dee064c 100644 --- a/Qsi.Tests/Vendor/PostgreSql/Driver/PostgreSqlRepositoryProvider.cs +++ b/Qsi.Tests/Vendor/PostgreSql/Driver/PostgreSqlRepositoryProvider.cs @@ -7,6 +7,7 @@ using Qsi.Data.Object; using Qsi.Data.Object.Function; using Qsi.Engines; +using Qsi.PostgreSql.Data; using Qsi.Utilities; namespace Qsi.Tests.PostgreSql.Driver; @@ -76,7 +77,7 @@ from information_schema.TABLES } sql = $@" -select COLUMN_NAME +select COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT from information_schema.COLUMNS where TABLE_CATALOG = '{table.Identifier[0]}' and TABLE_SCHEMA = '{table.Identifier[1].Value}' and TABLE_NAME = '{table.Identifier[2].Value}' order by ORDINAL_POSITION"; @@ -87,6 +88,8 @@ from information_schema.COLUMNS { var column = table.NewColumn(); column.Name = new QsiIdentifier(reader.GetString(0), false); + column.IsNullable = reader.GetString(1) == "YES"; + column.Default = reader.IsDBNull(2) ? null : reader.GetString(2); } } diff --git a/Qsi.Tests/Vendor/PostgreSql/PostgreSqlTest.cs b/Qsi.Tests/Vendor/PostgreSql/PostgreSqlTest.cs index e2909558..7f3cd4e7 100644 --- a/Qsi.Tests/Vendor/PostgreSql/PostgreSqlTest.cs +++ b/Qsi.Tests/Vendor/PostgreSql/PostgreSqlTest.cs @@ -274,6 +274,38 @@ public async Task Test_DML(string query, string[] expectedQueries, int expectedR Assert.Pass(); } + /// + /// Insert Action 시 Not Null 제약조건 있는 테이블에 값 대입하는 상황에 관한 에러 처리를 확인하는 테스트를 수행합니다. + /// + [Test] + public async Task Test_InsertNotNull() + { + const string CreateTableQuery = @"create table if not exists test_not_null ( +col1 VARCHAR NOT NULL, +col2 VARCHAR +)"; + + var command = Connection.CreateCommand(); + command.CommandText = CreateTableQuery; + await command.ExecuteNonQueryAsync(); + + string[] queries = + { + "INSERT INTO test_not_null VALUES (null, 'test')", + "INSERT INTO test_not_null (col2) VALUES ('test')" + }; + + const string errorMessage = "QSI-0021: The column 'col1' has a Not Null constraint."; + + foreach (var query in queries) + { + Assert.ThrowsAsync(async () => + { + await Engine.Execute(new QsiScript(query, QsiScriptType.Insert), null); + }, errorMessage); + } + } + /// /// Parameterized query에 대하여 테스트를 수행합니다. /// diff --git a/Qsi/Analyzers/Action/QsiActionAnalyzer.cs b/Qsi/Analyzers/Action/QsiActionAnalyzer.cs index 0085dc47..5b7c2027 100644 --- a/Qsi/Analyzers/Action/QsiActionAnalyzer.cs +++ b/Qsi/Analyzers/Action/QsiActionAnalyzer.cs @@ -466,6 +466,10 @@ protected virtual async ValueTask ExecuteDataInsertAction( } ColumnTarget[] columnTargets = await ResolveColumnTargetsFromDataInsertActionAsync(context, table, action); + var columnWithInvalidDefault = ResolveNotNullableColumnWithInvalidDefault(table.Columns, columnTargets); + + if (columnWithInvalidDefault is not null) + throw new QsiException(QsiError.NotNullConstraints, columnWithInvalidDefault.Name.Value); var dataContext = new TableDataInsertContext(context, table) { @@ -617,6 +621,14 @@ protected virtual SetColumnTarget ResolveSetColumnTarget( ); } + protected virtual QsiTableColumn ResolveNotNullableColumnWithInvalidDefault(IEnumerable columns, IEnumerable columnTargets) + { + HashSet targetNames = columnTargets.Select(ct => ct.DeclaredName.SubIdentifier(0).ToString()).ToHashSet(); + + return columns + .FirstOrDefault(x => !targetNames.Contains(x.Name.Value) && !x.IsNullable && x.Default is null); + } + private async ValueTask ProcessQueryValues(TableDataInsertContext context, IQsiTableDirectivesNode directives, IQsiTableNode valueTable) { var engine = context.Engine; @@ -745,6 +757,9 @@ private void PopulateInsertRow(TableDataInsertContext context, DataValueSelector item = pivot.SourceColumn is not null ? valueSelector(pivot) : ResolveDefaultColumnValue(pivot); + + if (item.Value is null && target.Table.Columns[pivot.DestinationOrder].IsNullable == false) + throw new QsiException(QsiError.NotNullConstraints, pivot.DestinationColumn.Name.Value); } target.InsertRows.Add(targetRow); diff --git a/Qsi/Data/Table/QsiTableColumn.cs b/Qsi/Data/Table/QsiTableColumn.cs index 24fe30bb..398610fb 100644 --- a/Qsi/Data/Table/QsiTableColumn.cs +++ b/Qsi/Data/Table/QsiTableColumn.cs @@ -35,6 +35,8 @@ public bool IsExpression set => _isExpression = value; } + public bool IsNullable { get; set; } = true; + internal QsiQualifiedIdentifier ImplicitTableWildcardTarget { get; set; } internal bool _isExpression; @@ -52,6 +54,7 @@ internal QsiTableColumn CloneInternal() column.ImplicitTableWildcardTarget = ImplicitTableWildcardTarget; column._isExpression = _isExpression; column.HasIndex = HasIndex; + column.IsNullable = IsNullable; return column; } diff --git a/Qsi/QsiError.cs b/Qsi/QsiError.cs index a8a6b7fd..62984b14 100644 --- a/Qsi/QsiError.cs +++ b/Qsi/QsiError.cs @@ -34,7 +34,8 @@ public enum QsiError ParameterNotFound, InvalidNestedExplain, SubqueryReturnsMoreThanRow, - UnableResolveFunction + UnableResolveFunction, + NotNullConstraints } internal static class SR @@ -72,6 +73,7 @@ internal static class SR public const string InvalidNestedExplain = "Invalid nested explain for '{0}'"; public const string SubqueryReturnsMoreThanRow = "Subquery returns more than {0} row"; public const string UnableResolveFunction = "Unable to resolve function '{0}'"; + public const string NotNullConstraints = "The column '{0}' has a Not Null constraint."; public static string GetResource(QsiError error) { @@ -110,6 +112,7 @@ public static string GetResource(QsiError error) QsiError.InvalidNestedExplain => InvalidNestedExplain, QsiError.SubqueryReturnsMoreThanRow => SubqueryReturnsMoreThanRow, QsiError.UnableResolveFunction => UnableResolveFunction, + QsiError.NotNullConstraints => NotNullConstraints, _ => null }; }