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

tstack / lnav / 24959179949-2999

26 Apr 2026 02:37PM UTC coverage: 69.227% (+0.09%) from 69.141%
24959179949-2999

push

github

tstack
[tests] fix paths

53969 of 77959 relevant lines covered (69.23%)

568944.78 hits per line

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

95.83
/src/sql.formatter.cc
1
/**
2
 * Copyright (c) 2025, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <algorithm>
31
#include <array>
32
#include <string>
33
#include <vector>
34

35
#include "sql.formatter.hh"
36

37
#include "base/attr_line.hh"
38
#include "base/intern_string.hh"
39
#include "base/lnav_log.hh"
40
#include "sql_help.hh"
41

42
static void
43
clear_left(std::string& str)
99✔
44
{
45
    if (str.empty() || str.back() == '\n') {
99✔
46
        return;
35✔
47
    }
48

49
    str.push_back('\n');
64✔
50
}
51

52
static void
53
clear_right(std::string& str)
106✔
54
{
55
    str.push_back('\n');
106✔
56
}
106✔
57

58
static void
59
add_indent(std::string& str, size_t indent)
14✔
60
{
61
    if (str.back() == '\n') {
14✔
62
        str.append(indent, ' ');
9✔
63
    }
64
}
14✔
65

66
static void
67
add_space(std::string& str, size_t indent)
354✔
68
{
69
    if (str.empty()) {
354✔
70
        return;
41✔
71
    }
72

73
    if (str.back() == '\n') {
313✔
74
        str.append(indent, ' ');
163✔
75
    } else if (str.back() != '.') {
150✔
76
        str.push_back(' ');
147✔
77
    }
78
}
79

80
namespace lnav {
81
namespace prql {
82

83
format_result
84
format(const attr_line_t& al, int cursor_offset)
5✔
85
{
86
    std::string retval;
5✔
87
    std::optional<int> cursor_retval;
5✔
88

89
    for (const auto& attr : al.al_attrs) {
67✔
90
        auto sf = al.to_string_fragment(attr);
62✔
91

92
        if (attr.sa_type == &sql::PRQL_STAGE_ATTR) {
62✔
93
            auto start_len = sf.length();
14✔
94
            sf = sf.trim("| \t\n");
14✔
95
            auto trimmed_size = start_len - sf.length();
14✔
96
            if (sf.empty()) {
14✔
97
                continue;
1✔
98
            }
99
            retval += sf;
13✔
100

101
            if (attr.sa_range.contains(cursor_offset)) {
13✔
102
                auto diff = attr.sa_range.lr_end - cursor_offset - trimmed_size;
2✔
103
                if (diff > 0 && diff < (ssize_t) retval.length()) {
2✔
104
                    cursor_retval = retval.length() - diff;
2✔
105
                } else {
106
                    cursor_retval = retval.length();
×
107
                }
108
            }
109
            retval.push_back('\n');
13✔
110
        }
111
    }
112

113
    return {retval, cursor_retval.value_or(retval.length())};
10✔
114
}
10✔
115

116
}  // namespace prql
117

118
namespace sql {
119

120
static bool
121
always_close_scope(std::vector<std::string>& scope_stack)
87✔
122
{
123
    return true;
87✔
124
}
125

126
static bool
127
never_close_scope(std::vector<std::string>& scope_stack)
2✔
128
{
129
    return false;
2✔
130
}
131

132
static bool
133
in_case_close_scope(std::vector<std::string>& scope_stack)
8✔
134
{
135
    if (scope_stack.back() == "CASE"_frag) {
8✔
136
        return false;
2✔
137
    }
138
    return true;
6✔
139
}
140

141
static bool
142
end_close_scope(std::vector<std::string>& scope_stack)
2✔
143
{
144
    if (scope_stack.empty()) {
2✔
145
        return false;
×
146
    }
147

148
    scope_stack.pop_back();
2✔
149
    if (scope_stack.empty()) {
2✔
150
        return false;
×
151
    }
152
    return scope_stack.back() == "CASE"_frag;
2✔
153
}
154

155
struct keyword_attrs {
156
    string_fragment ka_keyword;
157
    bool ka_clear_left{false};
158
    bool ka_clear_right{false};
159
    bool (*ka_close_scope_p)(std::vector<std::string>& scope_stack)
160
        = always_close_scope;
161
};
162

163
static constexpr std::array<keyword_attrs, 16> ATTRS_FOR_KW = {{
164
    {"CASE"_frag, true, false, never_close_scope},
165
    {"CREATE"_frag, true, false},
166
    {"ELSE"_frag, true, false, in_case_close_scope},
167
    {"END"_frag, true, false, end_close_scope},
168
    {"EXCEPT"_frag, true, false},
169
    {"FROM"_frag, true, true},
170
    {"HAVING"_frag, true, true},
171
    {"INTERSECT"_frag, true, false},
172
    {"LIMIT"_frag, true, true},
173
    {"SELECT"_frag, true, true},
174
    {"SET"_frag, true, true},
175
    {"UNION"_frag, true, false},
176
    {"VALUES"_frag, true, true},
177
    {"WHEN"_frag, true, false, in_case_close_scope},
178
    {"WHERE"_frag, true, true},
179
    {"WITH"_frag, true, true},
180
}};
181

182
constexpr auto ATTRS_FOR_KW_DEFAULT
183
    = keyword_attrs{""_frag, false, false, never_close_scope};
184

185
static const keyword_attrs&
186
get_keyword_attrs(const string_fragment& sf)
169✔
187
{
188
    auto iter = std::find_if(
169✔
189
        ATTRS_FOR_KW.begin(), ATTRS_FOR_KW.end(), [&sf](const auto& x) {
1,987✔
190
            return sf.iequal(x.ka_keyword);
1,987✔
191
        });
192
    if (iter == ATTRS_FOR_KW.end()) {
169✔
193
        return ATTRS_FOR_KW_DEFAULT;
70✔
194
    }
195

196
    return *iter;
99✔
197
}
198

199
static constexpr auto INDENT_SIZE = size_t{4};
200

201
static void
202
check_for_multi_word_clear(std::string& str,
87✔
203
                           std::vector<std::string>& scope_stack)
204
{
205
    struct clear_rules {
206
        const char* word;
207
        bool do_right;
208
        const char* padding{""};
209
    };
210

211
    static constexpr auto clear_words = std::array<clear_rules, 11>{
212
        {
213
            {" GROUP BY", true},
214
            {"INSERT INTO", true},
215
            {" ON CONFLICT", false},
216
            {" ORDER BY", true},
217
            {" LEFT JOIN", false},
218
            {" RIGHT JOIN", false},
219
            {" FULL JOIN", false},
220
            {" CROSS JOIN", false},
221
            {" INNER JOIN", false},
222
            {" PARTITION BY", false},
223
            {"REPLACE INTO", true},
224
        },
225
    };
226

227
    for (const auto& [words, do_right, padding] : clear_words) {
952✔
228
        if (endswith(str.c_str(), words)) {
878✔
229
            auto words_len = strlen(words);
13✔
230
            if (str[str.length() - words_len] == ' ') {
13✔
231
                str[str.length() - words_len] = '\n';
11✔
232
            }
233
            if (scope_stack.size() > 1) {
13✔
234
                if (do_right) {
11✔
235
                    scope_stack.pop_back();
6✔
236
                }
237
                str.insert(str.length() - words_len + 1,
11✔
238
                           (scope_stack.size() - 1) * INDENT_SIZE,
11✔
239
                           ' ');
240
                str.insert(str.length() - words_len + 1, padding);
11✔
241
            }
242
            if (do_right) {
13✔
243
                clear_right(str);
8✔
244
                scope_stack.emplace_back(words);
8✔
245
            }
246
            break;
13✔
247
        }
248
    }
249
}
87✔
250

251
format_result
252
format(const attr_line_t& al, int cursor_offset)
41✔
253
{
254
    string_attrs_t funcs;
41✔
255
    std::string retval;
41✔
256
    std::optional<int> cursor_retval;
41✔
257
    std::vector<bool> paren_indents;
41✔
258
    std::vector<std::string> scope_stack;
41✔
259

260
    scope_stack.emplace_back();
41✔
261
    for (const auto& attr : al.al_attrs) {
512✔
262
        if (!cursor_retval && cursor_offset < attr.sa_range.lr_start) {
471✔
263
            cursor_retval = retval.size();
1✔
264
        }
265
        if (find_string_attr(funcs, attr.sa_range.lr_start) != funcs.end()) {
471✔
266
            continue;
78✔
267
        }
268

269
        auto sf = al.to_string_fragment(attr);
393✔
270
        auto indent = (scope_stack.size() - 1) * INDENT_SIZE;
393✔
271
        if (attr.sa_type == &SQL_KEYWORD_ATTR) {
393✔
272
            const auto& ka = get_keyword_attrs(sf);
169✔
273
            const auto sf_upper = sf.to_string_with_case_style(
274
                string_fragment::case_style::upper);
169✔
275
            if (ka.ka_clear_left) {
169✔
276
                if (!paren_indents.empty()) {
99✔
277
                    paren_indents.back() = true;
14✔
278
                }
279
                if (ka.ka_close_scope_p(scope_stack)) {
99✔
280
                    if (scope_stack.size() > 1) {
95✔
281
                        scope_stack.pop_back();
60✔
282
                    }
283
                    indent = (scope_stack.size() - 1) * INDENT_SIZE;
95✔
284
                }
285
                clear_left(retval);
99✔
286
                if (ka.ka_keyword != "END"_frag) {  // XXX dumb special case
99✔
287
                    scope_stack.emplace_back(sf_upper);
97✔
288
                }
289
            }
290
            add_space(retval, indent);
169✔
291
            retval.append(sf_upper);
169✔
292
            if (ka.ka_clear_right) {
169✔
293
                clear_right(retval);
82✔
294
            } else {
295
                check_for_multi_word_clear(retval, scope_stack);
87✔
296
            }
297
        } else if (attr.sa_type == &SQL_COMMA_ATTR) {
393✔
298
            retval += sf;
15✔
299
            if (paren_indents.empty() || paren_indents.back()) {
15✔
300
                clear_right(retval);
12✔
301
            }
302
        } else if (attr.sa_type == &SQL_COMMENT_ATTR) {
209✔
303
            add_space(retval, indent);
×
304
            retval += sf;
×
305
            clear_right(retval);
×
306
        } else if (attr.sa_type == &SQL_PAREN_ATTR && sf.front() == '(') {
209✔
307
            paren_indents.push_back(false);
14✔
308
            while (!retval.empty() && isspace(retval.back())
38✔
309
                   && !endswith(retval, ",\n") && !endswith(retval, "VALUES\n"))
38✔
310
            {
311
                retval.pop_back();
5✔
312
            }
313
            if (endswith(retval, "ON")) {
14✔
314
                // force a clear
315
                paren_indents.back() = true;
1✔
316
            }
317
            add_space(retval, indent);
14✔
318
            retval += sf;
14✔
319
            if (scope_stack.back() == "CREATE") {
14✔
320
                paren_indents.back() = true;
1✔
321
            } else {
322
                scope_stack.emplace_back();
13✔
323
            }
324
            if (paren_indents.back()) {
14✔
325
                clear_right(retval);
2✔
326
            }
327
            if (endswith(retval, "OVER (")) {
14✔
328
                // clear might not happen
329
                paren_indents.back() = true;
1✔
330
            }
331
        } else if (attr.sa_type == &SQL_PAREN_ATTR && sf.front() == ')') {
195✔
332
            if (scope_stack.size() > 1) {
14✔
333
                scope_stack.pop_back();
14✔
334
            }
335
            if (!paren_indents.empty()) {
14✔
336
                if (paren_indents.back()) {
14✔
337
                    retval.push_back('\n');
9✔
338
                }
339
                paren_indents.pop_back();
14✔
340
            }
341
            add_indent(retval, indent > 0 ? indent - INDENT_SIZE : 0);
14✔
342
            retval += sf;
14✔
343
        } else if (attr.sa_type == &SQL_FUNCTION_ATTR) {
181✔
344
            funcs.emplace_back(attr);
12✔
345
            add_space(retval, indent);
12✔
346
            retval += sf;
12✔
347
        } else if (attr.sa_type == &SQL_GARBAGE_ATTR && sf.front() == '.') {
169✔
348
            retval.push_back('.');
3✔
349
        } else if (attr.sa_type == &SQL_GARBAGE_ATTR && sf.front() == ';') {
166✔
350
            retval.push_back(';');
2✔
351
            clear_right(retval);
2✔
352
        } else {
353
            if (retval.empty() || retval.back() != '(') {
164✔
354
                add_space(retval, indent);
159✔
355
            }
356
            retval += sf;
164✔
357
        }
358

359
        if (attr.sa_range.contains(cursor_offset)) {
393✔
360
            auto diff = attr.sa_range.lr_end - cursor_offset;
6✔
361
            if (retval.back() == '\n') {
6✔
362
                diff += 1;
1✔
363
            }
364
            if (diff < (ssize_t) retval.length()) {
6✔
365
                cursor_retval = retval.length() - diff;
6✔
366
            } else {
367
                cursor_retval = retval.length();
×
368
            }
369
        }
370

371
        ensure(!scope_stack.empty());
393✔
372
    }
373

374
    return {retval, cursor_retval.value_or(retval.length())};
82✔
375
}
82✔
376

377
}  // namespace sql
378

379
namespace db {
380

381
format_result
382
format(const attr_line_t& al, int cursor_offset)
46✔
383
{
384
    if (lnav::sql::is_prql(al.to_string_fragment())) {
46✔
385
        return prql::format(al, cursor_offset);
5✔
386
    }
387

388
    return sql::format(al, cursor_offset);
41✔
389
}
390

391
}  // namespace db
392

393
}  // namespace lnav
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