diff --git a/sqlparser/lib/src/analysis/types/data.dart b/sqlparser/lib/src/analysis/types/data.dart index 4d1e079a..78164a6c 100644 --- a/sqlparser/lib/src/analysis/types/data.dart +++ b/sqlparser/lib/src/analysis/types/data.dart @@ -35,7 +35,8 @@ class ResolvedType { ResolvedType get withoutNullabilityInfo { return nullable == null ? this - : ResolvedType(type: type, hint: hint, isArray: isArray); + : ResolvedType( + type: type, hint: hint, isArray: isArray, nullable: null); } ResolvedType withNullable(bool nullable) { diff --git a/sqlparser/lib/src/analysis/types/graph/type_graph.dart b/sqlparser/lib/src/analysis/types/graph/type_graph.dart index 187d55ae..287ee02a 100644 --- a/sqlparser/lib/src/analysis/types/graph/type_graph.dart +++ b/sqlparser/lib/src/analysis/types/graph/type_graph.dart @@ -41,6 +41,14 @@ class TypeGraph { bool knowsType(Typeable? t) => _knownTypes.containsKey(variables.normalize(t)); + bool knowsNullability(Typeable? t) { + final normalized = variables.normalize(t); + final knownType = _knownTypes[normalized]; + + return (knownType != null && knownType.nullable != null) || + _knownNullability.containsKey(normalized); + } + void addRelation(TypeRelation relation) { _relations.add(relation); } diff --git a/sqlparser/lib/src/analysis/types/resolving_visitor.dart b/sqlparser/lib/src/analysis/types/resolving_visitor.dart index 07a3d5a4..7d91a169 100644 --- a/sqlparser/lib/src/analysis/types/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types/resolving_visitor.dart @@ -323,7 +323,7 @@ class TypeResolver extends RecursiveVisitor { break; case TokenType.doublePipe: // string concatenation. - session._checkAndResolve(e, _textType, arg); + session._checkAndResolve(e, _textType.withoutNullabilityInfo, arg); session._addRelation(NullableIfSomeOtherIs(e, [e.left, e.right])); const childExpectation = ExactTypeExpectation.laxly(_textType); visit(e.left, childExpectation); @@ -502,7 +502,7 @@ class TypeResolver extends RecursiveVisitor { case 'trim': case 'upper': nullableIfChildIs(); - return _textType; + return _textType.withoutNullabilityInfo; case 'group_concat': return _textType.withNullable(true); case 'date': @@ -668,8 +668,15 @@ class TypeResolver extends RecursiveVisitor { void _lazyCopy(Typeable to, Typeable? from, {bool makeNullable = false}) { if (session.graph.knowsType(from)) { var type = session.typeOf(from)!; - if (makeNullable) type = type.withNullable(true); - session._markTypeResolved(to, type); + if (makeNullable) { + type = type.withNullable(true); + session._markTypeResolved(to, type); + } else if (session.graph.knowsNullability(from)) { + session._markTypeResolved(to, type); + } else { + session._markTypeResolved(to, type); + session._addRelation(NullableIfSomeOtherIs(to, [from!])); + } } else { session._addRelation(CopyTypeFrom(to, from, makeNullable: makeNullable)); } diff --git a/sqlparser/test/analysis/types2/misc_cases_test.dart b/sqlparser/test/analysis/types2/misc_cases_test.dart index 76d1075a..6e45c379 100644 --- a/sqlparser/test/analysis/types2/misc_cases_test.dart +++ b/sqlparser/test/analysis/types2/misc_cases_test.dart @@ -83,7 +83,7 @@ WITH RECURSIVE SELECT x+1 FROM cnt LIMIT 1000000 ) - SELECT x FROM cnt; + SELECT x FROM cnt; '''); final expressions = content.root.allDescendants.whereType(); @@ -92,4 +92,20 @@ WITH RECURSIVE everyElement(isNotNull), ); }); + + test('concatenation is nullable when any part is', () { + // https://github.com/simolus3/moor/issues/1719 + + final engine = SqlEngine() + ..registerTableFromSql('CREATE TABLE foobar (foo TEXT, bar TEXT);'); + + final content = + engine.analyze("SELECT foo, bar, foo || ' ' || bar FROM foobar;"); + + final columns = (content.root as SelectStatement).resolvedColumns!; + expect( + columns.map(content.typeOf), + everyElement(isA() + .having((e) => e.type?.nullable, 'type.nullable', isTrue))); + }); }