• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gluesql / gluesql / 22275856923

22 Feb 2026 11:01AM UTC coverage: 98.17% (+0.09%) from 98.085%
22275856923

Pull #1883

github

web-flow
Merge 6026cc70f into 80a446e87
Pull Request #1883: Add patch coverage diff summary to PR coverage bot comment

21 of 29 new or added lines in 11 files covered. (72.41%)

67 existing lines in 24 files now uncovered.

42750 of 43547 relevant lines covered (98.17%)

66356.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.63
/core/src/plan/expr/nullability.rs
1
use crate::{
2
    ast::{Expr, Function},
3
    data::Value,
4
};
5

6
pub fn may_return_null(expr: &Expr) -> bool {
9,395✔
7
    match expr {
387✔
8
        Expr::Value(Value::Null)
9
        | Expr::Identifier(_)
10
        | Expr::CompoundIdentifier { .. }
11
        | Expr::ArrayIndex { .. }
12
        | Expr::Subquery(_)
13
        | Expr::Exists { .. }
14
        | Expr::InSubquery { .. }
15
        | Expr::Aggregate(_) => true,
113✔
16
        Expr::Literal(_)
17
        | Expr::Value(_)
18
        | Expr::TypedString { .. }
19
        | Expr::IsNull(_)
20
        | Expr::IsNotNull(_) => false,
7,763✔
21
        Expr::UnaryOp { expr: inner, .. }
377✔
22
        | Expr::Nested(inner)
566✔
23
        | Expr::Interval { expr: inner, .. } => may_return_null(inner),
944✔
24
        Expr::BinaryOp { left, right, .. }
376✔
25
        | Expr::Like {
26
            expr: left,
1✔
27
            pattern: right,
1✔
28
            ..
29
        }
30
        | Expr::ILike {
31
            expr: left,
1✔
32
            pattern: right,
1✔
33
            ..
34
        } => may_return_null(left) || may_return_null(right),
378✔
35
        Expr::Between {
36
            expr, low, high, ..
1✔
37
        } => may_return_null(expr) || may_return_null(low) || may_return_null(high),
1✔
38
        Expr::InList { expr, list, .. } => {
2✔
39
            may_return_null(expr) || list.iter().any(may_return_null)
2✔
40
        }
41
        Expr::Function(function) => function_may_return_null(function),
191✔
42
        Expr::Case {
43
            operand,
2✔
44
            when_then,
2✔
45
            else_result,
2✔
46
        } => {
47
            operand.as_deref().is_some_and(may_return_null)
2✔
48
                || when_then
2✔
49
                    .iter()
2✔
50
                    .any(|(when, then)| may_return_null(when) || may_return_null(then))
2✔
51
                || else_result.as_deref().is_none_or(may_return_null)
1✔
52
        }
53
        Expr::Array { elem } => elem.iter().any(may_return_null),
1✔
54
    }
55
}
9,395✔
56

57
fn function_may_return_null(function: &Function) -> bool {
191✔
58
    use Function::*;
59

60
    match function {
2✔
61
        Coalesce(exprs) => exprs.iter().all(may_return_null),
5✔
62
        IfNull { expr, then } => may_return_null(expr) && may_return_null(then),
3✔
63
        NullIf { .. } | Custom { .. } => true,
3✔
64
        Now() | CurrentDate() | CurrentTime() | CurrentTimestamp() | Pi() | GenerateUuid()
65
        | Rand(_) => false,
8✔
66
        Cast { expr, .. }
95✔
67
        | Abs(expr)
1✔
68
        | Initcap(expr)
1✔
69
        | Lower(expr)
1✔
70
        | Upper(expr)
1✔
71
        | Asin(expr)
1✔
72
        | Acos(expr)
1✔
73
        | Atan(expr)
1✔
74
        | Ceil(expr)
1✔
75
        | Round(expr)
1✔
76
        | Trunc(expr)
1✔
77
        | Floor(expr)
1✔
78
        | Exp(expr)
1✔
79
        | Ln(expr)
1✔
80
        | Log2(expr)
1✔
81
        | Log10(expr)
1✔
82
        | Sin(expr)
1✔
83
        | Cos(expr)
1✔
84
        | Tan(expr)
1✔
85
        | Sqrt(expr)
1✔
86
        | Radians(expr)
1✔
87
        | Degrees(expr)
1✔
88
        | LastDay(expr)
1✔
89
        | Reverse(expr)
1✔
90
        | Sign(expr)
1✔
91
        | IsEmpty(expr)
1✔
92
        | Length(expr)
1✔
93
        | Entries(expr)
1✔
94
        | Keys(expr)
1✔
95
        | Values(expr)
1✔
96
        | Ascii(expr)
1✔
97
        | Chr(expr)
1✔
98
        | Md5(expr)
1✔
99
        | Hex(expr)
1✔
100
        | GetX(expr)
1✔
101
        | GetY(expr)
1✔
102
        | Dedup(expr)
1✔
103
        | Extract { expr, .. }
1✔
104
        | Sort { expr, order: None } => may_return_null(expr),
133✔
105
        AddMonth { expr, size }
1✔
106
        | Left { expr, size }
1✔
107
        | Right { expr, size }
1✔
108
        | Repeat { expr, num: size }
1✔
109
        | Skip { expr, size }
1✔
110
        | Take { expr, size }
1✔
111
        | Append { expr, value: size }
1✔
112
        | Prepend { expr, value: size }
1✔
113
        | Div {
114
            dividend: expr,
1✔
115
            divisor: size,
1✔
116
        }
117
        | Mod {
118
            dividend: expr,
1✔
119
            divisor: size,
1✔
120
        }
121
        | Gcd {
122
            left: expr,
1✔
123
            right: size,
1✔
124
        }
125
        | Lcm {
126
            left: expr,
1✔
127
            right: size,
1✔
128
        }
129
        | Power { expr, power: size }
1✔
130
        | Format { expr, format: size }
1✔
131
        | ToDate { expr, format: size }
1✔
132
        | ToTimestamp { expr, format: size }
1✔
133
        | ToTime { expr, format: size }
1✔
134
        | Position {
135
            from_expr: size,
2✔
136
            sub_expr: expr,
2✔
137
        }
138
        | Point { x: expr, y: size }
3✔
139
        | CalcDistance {
140
            geometry1: expr,
1✔
141
            geometry2: size,
1✔
142
        }
143
        | Sort {
144
            expr,
1✔
145
            order: Some(size),
1✔
146
        } => may_return_null(expr) || may_return_null(size),
24✔
147
        Log { antilog, base } => may_return_null(antilog) || may_return_null(base),
1✔
148
        Concat(exprs) | Greatest(exprs) => exprs.iter().any(may_return_null),
2✔
149
        ConcatWs { separator, exprs } => {
1✔
150
            may_return_null(separator) || exprs.iter().any(may_return_null)
1✔
151
        }
152
        Replace { expr, old, new } => {
1✔
153
            may_return_null(expr) || may_return_null(old) || may_return_null(new)
1✔
154
        }
155
        Lpad { expr, size, fill } | Rpad { expr, size, fill } => {
1✔
156
            may_return_null(expr)
2✔
157
                || may_return_null(size)
2✔
158
                || fill.as_ref().is_some_and(may_return_null)
2✔
159
        }
160
        Trim {
161
            expr, filter_chars, ..
1✔
162
        } => may_return_null(expr) || filter_chars.as_ref().is_some_and(may_return_null),
1✔
163
        Ltrim { expr, chars } | Rtrim { expr, chars } => {
1✔
164
            may_return_null(expr) || chars.as_ref().is_some_and(may_return_null)
2✔
165
        }
166
        Slice {
167
            expr,
1✔
168
            start,
1✔
169
            length,
1✔
170
        } => may_return_null(expr) || may_return_null(start) || may_return_null(length),
1✔
171
        Substr { expr, start, count } => {
1✔
172
            may_return_null(expr)
1✔
173
                || may_return_null(start)
1✔
174
                || count.as_ref().is_some_and(may_return_null)
1✔
175
        }
176
        Unwrap { expr, selector } => may_return_null(expr) || may_return_null(selector),
1✔
177
        FindIdx {
178
            from_expr,
1✔
179
            sub_expr,
1✔
180
            start,
1✔
181
        } => {
182
            may_return_null(from_expr)
1✔
183
                || may_return_null(sub_expr)
1✔
184
                || start.as_ref().is_some_and(may_return_null)
1✔
185
        }
186
        Splice {
187
            list_data,
1✔
188
            begin_index,
1✔
189
            end_index,
1✔
190
            values,
1✔
191
        } => {
192
            may_return_null(list_data)
1✔
193
                || may_return_null(begin_index)
1✔
194
                || may_return_null(end_index)
1✔
195
                || values.as_ref().is_some_and(may_return_null)
1✔
196
        }
197
    }
198
}
191✔
199

200
#[cfg(test)]
201
mod tests {
202
    use {
203
        super::may_return_null,
204
        crate::{
205
            parse_sql::parse_expr,
206
            translate::{NO_PARAMS, translate_expr},
207
        },
208
    };
209

210
    fn test(sql: &str, expected: bool) {
113✔
211
        let expr = parse_expr(sql).and_then(|parsed| translate_expr(&parsed, NO_PARAMS));
113✔
212
        let actual = expr.map(|expr| may_return_null(&expr));
113✔
213

214
        assert_eq!(actual, Ok(expected), "{sql} nullability mismatch");
113✔
215
    }
112✔
216

217
    #[test]
218
    fn expression_cases() {
1✔
219
        if std::env::var_os("GLUESQL_COVERAGE_BOT_MISS").is_some() {
1✔
NEW
220
            std::hint::black_box(1_u8);
×
221
        }
1✔
222
        test("NULL", true);
1✔
223
        test("id", true);
1✔
224
        test("Foo.id", true);
1✔
225
        test("COALESCE(NULL, id)", true);
1✔
226
        test("IFNULL(id, NULL)", true);
1✔
227
        test("CASE 1 WHEN 1 THEN id ELSE 0 END", true);
1✔
228
        test("1 IN (SELECT 1)", true);
1✔
229
        test("id IN (1, 2, 3)", true);
1✔
230
        test("ARRAY[1, id]", true);
1✔
231
        test("POSITION('a' IN id)", true);
1✔
232

233
        test("1", false);
1✔
234
        test("'A'", false);
1✔
235
        test("INTERVAL 1 DAY", false);
1✔
236
        test("NOT TRUE", false);
1✔
237
        test("1 BETWEEN 0 AND 2", false);
1✔
238
        test("'A' LIKE 'A%'", false);
1✔
239
        test("'A' ILIKE 'A%'", false);
1✔
240
        test("1 IN (1, 2, 3)", false);
1✔
241
        test("('A' IS NULL)", false);
1✔
242
        test("CASE 1 WHEN 1 THEN 2 ELSE 3 END", false);
1✔
243
    }
1✔
244

245
    #[test]
246
    fn function_branch_cases() {
1✔
247
        test("ABS(1)", false);
1✔
248
        test("ADD_MONTH(DATE '2020-01-01', 1)", false);
1✔
249
        test("LOWER('ABC')", false);
1✔
250
        test("INITCAP('abc')", false);
1✔
251
        test("UPPER('abc')", false);
1✔
252
        test("LEFT('abc', 1)", false);
1✔
253
        test("RIGHT('abc', 1)", false);
1✔
254
        test("ASIN(0)", false);
1✔
255
        test("ACOS(0)", false);
1✔
256
        test("ATAN(0)", false);
1✔
257
        test("LPAD('abc', 5, 'x')", false);
1✔
258
        test("RPAD('abc', 5, 'x')", false);
1✔
259
        test("REPLACE('abc', 'b', 'c')", false);
1✔
260
        test("CAST(1 AS INT)", false);
1✔
261
        test("CEIL(1.1)", false);
1✔
262
        test("COALESCE(1, 2)", false);
1✔
263
        test("CONCAT('a', 'b')", false);
1✔
264
        test("CONCAT_WS('-', 'a', 'b')", false);
1✔
265
        test("CUSTOM_FUNC()", true);
1✔
266
        test("IFNULL(1, 2)", false);
1✔
267
        test("NULLIF(1, 2)", true);
1✔
268
        test("RAND()", false);
1✔
269
        test("ROUND(1.2)", false);
1✔
270
        test("TRUNC(1.2)", false);
1✔
271
        test("FLOOR(1.2)", false);
1✔
272
        test("TRIM('  value  ')", false);
1✔
273
        test("EXP(1)", false);
1✔
274
        test("EXTRACT(YEAR FROM DATE '2020-01-01')", false);
1✔
275
        test("LN(1)", false);
1✔
276
        test("LOG(2, 10)", false);
1✔
277
        test("LOG2(2)", false);
1✔
278
        test("LOG10(10)", false);
1✔
279
        test("DIV(4, 2)", false);
1✔
280
        test("MOD(4, 2)", false);
1✔
281
        test("GCD(4, 2)", false);
1✔
282
        test("LCM(4, 2)", false);
1✔
283
        test("SIN(1)", false);
1✔
284
        test("COS(1)", false);
1✔
285
        test("TAN(1)", false);
1✔
286
        test("SQRT(4)", false);
1✔
287
        test("POWER(2, 3)", false);
1✔
288
        test("RADIANS(180)", false);
1✔
289
        test("DEGREES(3.14)", false);
1✔
290
        test("NOW()", false);
1✔
291
        test("CURRENT_DATE()", false);
1✔
292
        test("CURRENT_TIME()", false);
1✔
293
        test("CURRENT_TIMESTAMP()", false);
1✔
294
        test("PI()", false);
1✔
295
        test("LAST_DAY(DATE '2020-01-01')", false);
1✔
296
        test("LTRIM('  abc')", false);
1✔
297
        test("RTRIM('abc  ')", false);
1✔
298
        test("REVERSE('abc')", false);
1✔
299
        test("REPEAT('a', 2)", false);
1✔
300
        test("SIGN(1)", false);
1✔
301
        test("SUBSTR('abc', 1, 1)", false);
1✔
302
        test(r#"UNWRAP('{"a":1}', 'a')"#, false);
1✔
303
        test("GENERATE_UUID()", false);
1✔
304
        test("GREATEST(1, 2)", false);
1✔
305
        test("FORMAT('value', '%s')", false);
1✔
306
        test("TO_DATE('2020-01-01', '%Y-%m-%d')", false);
1✔
307
        test(
1✔
308
            "TO_TIMESTAMP('2020-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S')",
1✔
309
            false,
310
        );
311
        test("TO_TIME('12:34:56', '%H:%M:%S')", false);
1✔
312
        test("POSITION('a' IN 'abc')", false);
1✔
313
        test("FIND_IDX('abc', 'a', 1)", false);
1✔
314
        test("ASCII('A')", false);
1✔
315
        test("CHR(65)", false);
1✔
316
        test("MD5('abc')", false);
1✔
317
        test("HEX('abc')", false);
1✔
318
        test("APPEND('[1,2]', '3')", false);
1✔
319
        test("SORT('[1,2]')", false);
1✔
320
        test("SORT('[1,2]', 'ASC')", false);
1✔
321
        test("SLICE('[1,2,3]', 1, 1)", false);
1✔
322
        test("PREPEND('[2,3]', '1')", false);
1✔
323
        test("SKIP('[1,2,3]', 1)", false);
1✔
324
        test("TAKE('[1,2,3]', 2)", false);
1✔
325
        test("GET_X('POINT(1 2)')", false);
1✔
326
        test("GET_Y('POINT(1 2)')", false);
1✔
327
        test("POINT(1, 2)", false);
1✔
328
        test("CALC_DISTANCE(POINT(0, 0), POINT(1, 1))", false);
1✔
329
        test("IS_EMPTY('[]')", false);
1✔
330
        test("LENGTH('[1,2]')", false);
1✔
331
        test(r#"ENTRIES('{"a":1}')"#, false);
1✔
332
        test(r#"KEYS('{"a":1}')"#, false);
1✔
333
        test(r#"VALUES('{"a":1}')"#, false);
1✔
334
        test("COALESCE(NULL)", true);
1✔
335
        test("IFNULL(NULL, NULL)", true);
1✔
336
        test("NULLIF(1, 1)", true);
1✔
337
        test("RAND(1)", false);
1✔
338
        test("COALESCE(NULL, NULL)", true);
1✔
339
        test("COALESCE()", true);
1✔
340
        test("SPLICE('[1,2,3]', 1, 2, '[9]')", false);
1✔
341
        test("DEDUP('[1,1,2]')", false);
1✔
342
    }
1✔
343

344
    #[test]
345
    #[should_panic(expected = "nullability mismatch")]
346
    fn invalid_expression_panics() {
1✔
347
        test("INVALID SQL", false);
1✔
348
    }
1✔
349
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc