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

stephenafamo / bob / 17002678275

16 Aug 2025 01:28AM UTC coverage: 41.658% (+0.06%) from 41.598%
17002678275

push

github

stephenafamo
Properly detect end of function in postgres query parser

0 of 1 new or added line in 1 file covered. (0.0%)

110 existing lines in 2 files now uncovered.

9556 of 22939 relevant lines covered (41.66%)

567.51 hits per line

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

50.78
/gen/bobgen-psql/driver/parser/source.go
1
package parser
2

3
import (
4
        "fmt"
5
        "reflect"
6
        "slices"
7
        "strconv"
8
        "strings"
9

10
        pg "github.com/pganalyze/pg_query_go/v6"
11
        "github.com/stephenafamo/bob/internal"
12
)
13

14
func (w *walker) getSource(node *pg.Node, info nodeInfo, sources ...queryResult) queryResult {
36✔
15
        cloned := slices.Clone(sources)
36✔
16

36✔
17
        switch stmt := node.Node.(type) {
36✔
18
        case *pg.Node_SelectStmt:
8✔
19
                return w.getSelectSource(stmt.SelectStmt, info, cloned...)
8✔
20

21
        case *pg.Node_InsertStmt:
12✔
22
                return w.getInsertSource(stmt.InsertStmt, info)
12✔
23

24
        case *pg.Node_UpdateStmt:
4✔
25
                return w.getUpdateSource(stmt.UpdateStmt, info, cloned...)
4✔
26

27
        case *pg.Node_DeleteStmt:
4✔
28
                return w.getDeleteSource(stmt.DeleteStmt, info, cloned...)
4✔
29

30
        case *pg.Node_RangeVar:
8✔
31
                if rangeInfo, ok := info.children["RangeVar"]; ok {
16✔
32
                        info = rangeInfo
8✔
33
                }
8✔
34
                return w.getTableSource(stmt.RangeVar, info, cloned...)
8✔
35

36
        case *pg.Node_RangeSubselect:
×
37
                if subSelInfo, ok := info.children["RangeSubselect"]; ok {
×
38
                        info = subSelInfo
×
39
                }
×
40
                sub := stmt.RangeSubselect
×
41
                source := w.getSource(sub.Subquery, info.children["Subquery"], cloned...)
×
42
                if sub.Alias == nil {
×
43
                        return source
×
44
                }
×
45
                source.name = sub.Alias.Aliasname
×
46
                if len(source.columns) != len(sub.Alias.Colnames) {
×
47
                        return source
×
48
                }
×
49

50
                colInfos := info.children["Alias"].children["Colnames"]
×
51
                for i := range sub.Alias.Colnames {
×
52
                        aliasName := w.names[colInfos.children[strconv.Itoa(i)].position()]
×
53
                        if aliasName != "" {
×
54
                                source.columns[i].name = aliasName
×
55
                        }
×
56
                }
57
                return source
×
58

59
        default:
×
60
                return queryResult{}
×
61
        }
62
}
63

64
func (w *walker) getTableSource(sub *pg.RangeVar, info nodeInfo, sources ...queryResult) queryResult {
36✔
65
        name := w.names[info.children["Relname"].position()]
36✔
66
        schema := ""
36✔
67
        if schemaInfo, ok := info.children["Schemaname"]; ok {
36✔
UNCOV
68
                schema = w.names[schemaInfo.position()]
×
UNCOV
69
        }
×
70

71
        source := queryResult{}
36✔
72
        for _, given := range sources {
36✔
UNCOV
73
                if given.schema != schema || given.name != name {
×
UNCOV
74
                        continue
×
75
                }
76
                // Found a source with the same schema and name
77
                source = given
×
78
        }
79

80
        if source.name == "" {
72✔
81
                for _, table := range w.db {
512✔
82
                        if table.Name != name {
916✔
83
                                continue
440✔
84
                        }
85

86
                        switch {
36✔
87
                        case table.Schema == schema: // schema matches
36✔
UNCOV
88
                        case table.Schema == "" && schema == w.sharedSchema: // schema is shared
×
UNCOV
89
                        default:
×
UNCOV
90
                                continue
×
91
                        }
92

93
                        source = queryResult{
36✔
94
                                schema:  table.Schema,
36✔
95
                                name:    table.Name,
36✔
96
                                columns: make([]col, len(table.Columns)),
36✔
97
                        }
36✔
98
                        for j, column := range table.Columns {
240✔
99
                                source.columns[j] = col{name: column.Name, nullable: column.Nullable}
204✔
100
                        }
204✔
101

102
                        break
36✔
103
                }
104
        }
105

106
        if sub.Alias == nil {
72✔
107
                return source
36✔
108
        }
36✔
109

UNCOV
110
        source.schema = "" // empty schema for aliased tables
×
UNCOV
111
        source.name = sub.Alias.Aliasname
×
112
        if len(source.columns) != len(sub.Alias.Colnames) {
×
UNCOV
113
                return source
×
UNCOV
114
        }
×
115

UNCOV
116
        colInfos := info.children["Alias"].children["Colnames"]
×
117
        for i := range sub.Alias.Colnames {
×
118
                aliasName := w.names[colInfos.children[strconv.Itoa(i)].position()]
×
119
                if aliasName != "" {
×
120
                        source.columns[i].name = aliasName
×
121
                }
×
122
        }
123

124
        return source
×
125
}
126

127
func (w *walker) getSelectSource(stmt *pg.SelectStmt, info nodeInfo, sources ...queryResult) queryResult {
8✔
128
        if len(stmt.ValuesLists) > 0 {
8✔
UNCOV
129
                list, isList := stmt.ValuesLists[0].Node.(*pg.Node_List)
×
UNCOV
130
                if isList {
×
UNCOV
131
                        return w.getSourceFromList(
×
132
                                list.List,
×
133
                                info.children["ValuesLists"].children["0"].children["List"],
×
134
                                sources...,
×
UNCOV
135
                        )
×
UNCOV
136
                }
×
137
        }
138

139
        sources = w.addSourcesOfWithClause(stmt.WithClause, info.children["WithClause"], sources...)
8✔
140

8✔
141
        main := stmt
8✔
142
        mainInfo := info
8✔
143
        for main.Larg != nil {
8✔
UNCOV
144
                main = main.Larg
×
UNCOV
145
                mainInfo = mainInfo.children["Larg"]
×
UNCOV
146
        }
×
147

148
        if len(main.FromClause) == 0 {
8✔
UNCOV
149
                return w.getSourceFromTargets(main.TargetList, mainInfo.children["TargetList"].children, sources...)
×
UNCOV
150
        }
×
151

152
        from := main.FromClause[0]
8✔
153
        fromInfo := mainInfo.children["FromClause"].children["0"]
8✔
154
        sources = w.addSourcesOfFromItem(from, fromInfo, sources...)
8✔
155

8✔
156
        return w.getSourceFromTargets(main.TargetList, mainInfo.children["TargetList"].children, sources...)
8✔
157
}
158

159
func (w *walker) getInsertSource(stmt *pg.InsertStmt, info nodeInfo) queryResult {
12✔
160
        table := w.getTableSource(stmt.Relation, info.children["Relation"])
12✔
161
        return w.getSourceFromTargets(stmt.ReturningList, info.children["ReturningList"].children, table)
12✔
162
}
12✔
163

164
func (w *walker) getUpdateSource(stmt *pg.UpdateStmt, info nodeInfo, sources ...queryResult) queryResult {
4✔
165
        sources = w.addSourcesOfWithClause(stmt.WithClause, info.children["WithClause"], sources...)
4✔
166

4✔
167
        table := w.getTableSource(stmt.Relation, info.children["Relation"])
4✔
168
        sources = append(sources, table)
4✔
169

4✔
170
        if len(stmt.FromClause) == 0 {
8✔
171
                return w.getSourceFromTargets(
4✔
172
                        stmt.ReturningList,
4✔
173
                        info.children["ReturningList"].children,
4✔
174
                        sources...,
4✔
175
                )
4✔
176
        }
4✔
177

UNCOV
178
        from := stmt.FromClause[0]
×
UNCOV
179
        fromInfo := info.children["FromClause"].children["0"]
×
UNCOV
180
        sources = w.addSourcesOfFromItem(from, fromInfo, sources...)
×
UNCOV
181

×
UNCOV
182
        return w.getSourceFromTargets(
×
UNCOV
183
                stmt.ReturningList,
×
UNCOV
184
                info.children["ReturningList"].children,
×
UNCOV
185
                sources...,
×
UNCOV
186
        )
×
187
}
188

189
func (w *walker) getDeleteSource(stmt *pg.DeleteStmt, info nodeInfo, sources ...queryResult) queryResult {
4✔
190
        sources = w.addSourcesOfWithClause(stmt.WithClause, info.children["WithClause"], sources...)
4✔
191

4✔
192
        table := w.getTableSource(stmt.Relation, info.children["Relation"])
4✔
193
        sources = append(sources, table)
4✔
194

4✔
195
        if len(stmt.UsingClause) == 0 {
8✔
196
                return w.getSourceFromTargets(
4✔
197
                        stmt.ReturningList,
4✔
198
                        info.children["ReturningList"].children,
4✔
199
                        sources...,
4✔
200
                )
4✔
201
        }
4✔
202

UNCOV
203
        from := stmt.UsingClause[0]
×
UNCOV
204
        fromInfo := info.children["UsingClause"].children["0"]
×
UNCOV
205
        sources = w.addSourcesOfFromItem(from, fromInfo, sources...)
×
UNCOV
206

×
207
        return w.getSourceFromTargets(
×
208
                stmt.ReturningList,
×
209
                info.children["ReturningList"].children,
×
210
                sources...,
×
211
        )
×
212
}
213

214
func (w *walker) addSourcesOfWithClause(with *pg.WithClause, info nodeInfo, sources ...queryResult) []queryResult {
16✔
215
        if with == nil {
32✔
216
                return sources
16✔
217
        }
16✔
218

219
        cteInfos := info.children["Ctes"]
×
220
        for i, cte := range with.Ctes {
×
221
                cteNodeWrap, ok := cte.Node.(*pg.Node_CommonTableExpr)
×
222
                if !ok {
×
223
                        continue
×
224
                }
225

UNCOV
226
                cteNode := cteNodeWrap.CommonTableExpr
×
227
                cteInfo := cteInfos.children[strconv.Itoa(i)].children["CommonTableExpr"]
×
228
                var queryInfo nodeInfo
×
229
                for _, val := range cteInfo.children["Ctequery"].children {
×
230
                        queryInfo = val
×
231
                        break
×
232
                }
233

UNCOV
234
                stmtSource := w.getSource(cteNode.Ctequery, queryInfo, sources...)
×
235
                stmtSource.name = cteNode.Ctename
×
UNCOV
236
                stmtSource.mustBeQualified = true
×
UNCOV
237

×
238
                if len(cteNode.Aliascolnames) != len(stmtSource.columns) {
×
UNCOV
239
                        sources = append(sources, stmtSource)
×
UNCOV
240
                        continue
×
241
                }
242

UNCOV
243
                aliasInfos := cteInfo.children["Aliascolnames"]
×
UNCOV
244
                for j := range cteNode.Aliascolnames {
×
UNCOV
245
                        alias := w.names[aliasInfos.children[strconv.Itoa(j)].position()]
×
UNCOV
246
                        if alias != "" {
×
UNCOV
247
                                stmtSource.columns[j].name = alias
×
UNCOV
248
                        }
×
249
                }
250

251
                sources = append(sources, stmtSource)
×
252
        }
253

254
        return sources
×
255
}
256

257
func (w *walker) addSourcesOfFromItem(from *pg.Node, fromInfo nodeInfo, sources ...queryResult) []queryResult {
8✔
258
        var joinedNodes []joinedInfo
8✔
259

8✔
260
        for {
16✔
261
                join := from.GetJoinExpr()
8✔
262
                if join == nil {
16✔
263
                        break
8✔
264
                }
UNCOV
265
                joinInfo := fromInfo.children["JoinExpr"]
×
UNCOV
266

×
UNCOV
267
                // Update the main FROM
×
UNCOV
268
                from = join.Larg
×
UNCOV
269
                fromInfo = joinInfo.children["Larg"]
×
UNCOV
270

×
UNCOV
271
                infoKey := strings.TrimPrefix(
×
UNCOV
272
                        reflect.TypeOf(join.Rarg.Node).Elem().Name(), "Node_")
×
UNCOV
273

×
UNCOV
274
                joined := joinedInfo{
×
UNCOV
275
                        node:     join.Rarg,
×
UNCOV
276
                        info:     joinInfo.children["Rarg"].children[infoKey],
×
UNCOV
277
                        joinType: join.Jointype,
×
UNCOV
278
                }
×
UNCOV
279

×
UNCOV
280
                joinedNodes = append(joinedNodes, joined)
×
281
        }
282

283
        joinedNodes = append(joinedNodes, joinedInfo{
8✔
284
                node: from,
8✔
285
                info: fromInfo,
8✔
286
        })
8✔
287

8✔
288
        // Loop join in reverse order
8✔
289
        joinSources := make([]queryResult, 0, len(joinedNodes))
8✔
290
        for _, j := range slices.Backward(joinedNodes) {
16✔
291
                joinSource := w.getSource(
8✔
292
                        j.node,
8✔
293
                        j.info,
8✔
294
                        sources...,
8✔
295
                )
8✔
296
                // Do not qualify joined tables by default
8✔
297
                joinSource.mustBeQualified = false
8✔
298

8✔
299
                var right, left bool
8✔
300

8✔
301
                switch j.joinType {
8✔
302
                case pg.JoinType_JOIN_RIGHT:
×
303
                        right = true
×
304

UNCOV
305
                case pg.JoinType_JOIN_LEFT:
×
UNCOV
306
                        left = true
×
307

UNCOV
308
                case pg.JoinType_JOIN_FULL:
×
UNCOV
309
                        right = true
×
UNCOV
310
                        left = true
×
311
                }
312

313
                if right {
8✔
UNCOV
314
                        for i := range joinSources {
×
315
                                for j := range joinSources[i].columns {
×
316
                                        joinSources[i].columns[j].nullable = true
×
UNCOV
317
                                }
×
318
                        }
319
                }
320
                if left {
8✔
UNCOV
321
                        for i := range joinSource.columns {
×
UNCOV
322
                                joinSource.columns[i].nullable = true
×
UNCOV
323
                        }
×
324
                }
325

326
                joinSources = append(joinSources, joinSource)
8✔
327
        }
328

329
        return append(sources, joinSources...)
8✔
330
}
331

332
func (w *walker) getSourceFromTargets(targets []*pg.Node, infos map[string]nodeInfo, sources ...queryResult) queryResult {
28✔
333
        if len(targets) != len(infos) {
28✔
334
                return queryResult{}
×
335
        }
×
336

337
        source := queryResult{
28✔
338
                columns: make([]col, 0, len(targets)),
28✔
339
        }
28✔
340

28✔
341
        var prefix string
28✔
342

28✔
343
        for i, target := range targets {
40✔
344
                targetInfo := infos[strconv.Itoa(i)]
12✔
345
                pos := targetInfo.position()
12✔
346

12✔
347
                if newPrefix, found := w.getPrefixAnnotation(pos[0]); found {
12✔
348
                        prefix = newPrefix
×
349
                }
×
350

351
                if w.names[pos] == "*" {
24✔
352
                        if w.getConfigComment(pos[1]) != "" {
12✔
UNCOV
353
                                w.errors = append(w.errors, fmt.Errorf("no comments after STAR column"))
×
354
                        }
×
355

356
                        source.columns = append(
12✔
357
                                source.columns,
12✔
358
                                w.getStarColumns(target, targetInfo, prefix, sources...)...,
12✔
359
                        )
12✔
360

12✔
361
                        continue
12✔
362
                }
363

364
                column := col{
×
365
                        pos:  pos,
×
366
                        name: w.names[pos],
×
367
                }
×
368

×
369
                if nullable := w.nullability[pos]; nullable != nil {
×
370
                        column.nullable = nullable.IsNull(w.names, w.nullability, sources)
×
371
                }
×
372

UNCOV
373
                resTarget := target.GetResTarget()
×
UNCOV
374
                if resTarget != nil && resTarget.Name != "" {
×
UNCOV
375
                        column.name = resTarget.GetName()
×
UNCOV
376
                }
×
377

378
                column.name = prefix + column.name
×
379
                source.columns = append(source.columns, column)
×
380

×
381
                if prefix != "" {
×
382
                        valInfo := targetInfo.children["ResTarget"].children["Val"]
×
383
                        w.editRules = append(
×
384
                                w.editRules,
×
385
                                internal.Replace(
×
386
                                        int(valInfo.end), int(targetInfo.end)-1,
×
387
                                        fmt.Sprintf(" AS %q", column.name),
×
388
                                ),
×
389
                        )
×
390
                }
×
391
        }
392

393
        return source
28✔
394
}
395

UNCOV
396
func (w *walker) getSourceFromList(target *pg.List, info nodeInfo, sources ...queryResult) queryResult {
×
UNCOV
397
        result := queryResult{
×
UNCOV
398
                columns: make([]col, len(target.Items)),
×
UNCOV
399
        }
×
400

×
401
        itemsInfo := info.children["Items"]
×
UNCOV
402
        for i := range target.Items {
×
UNCOV
403
                pos := itemsInfo.children[strconv.Itoa(i)].position()
×
UNCOV
404
                result.columns[i] = col{
×
405
                        pos:  pos,
×
406
                        name: fmt.Sprintf("column%d", i+1),
×
UNCOV
407
                }
×
UNCOV
408

×
UNCOV
409
                if nullable, ok := w.nullability[pos]; ok {
×
UNCOV
410
                        result.columns[i].nullable = nullable.IsNull(w.names, w.nullability, sources)
×
UNCOV
411
                }
×
412
        }
413

UNCOV
414
        return result
×
415
}
416

417
func (w *walker) getStarColumns(target *pg.Node, info nodeInfo, prefix string, sources ...queryResult) []col {
12✔
418
        if target == nil {
12✔
UNCOV
419
                return nil
×
UNCOV
420
        }
×
421

422
        fields := target.GetResTarget().GetVal().GetColumnRef().GetFields()
12✔
423
        if len(fields) == 0 {
12✔
424
                return nil
×
UNCOV
425
        }
×
426

427
        var schema, table, column string
12✔
428

12✔
429
        fieldsInfo := info.
12✔
430
                children["ResTarget"].
12✔
431
                children["Val"].
12✔
432
                children["ColumnRef"].
12✔
433
                children["Fields"]
12✔
434

12✔
435
        for i := range fields {
32✔
436
                name := w.names[fieldsInfo.children[strconv.Itoa(i)].position()]
20✔
437
                switch {
20✔
438
                case i == len(fields)-1:
12✔
439
                        column = name
12✔
440
                case i == len(fields)-2:
8✔
441
                        table = name
8✔
UNCOV
442
                case i == len(fields)-3:
×
443
                        schema = name
×
444
                }
445
        }
446

447
        if column != "*" {
12✔
UNCOV
448
                panic("getStarColumns when column != '*'")
×
449
        }
450

451
        var columns []col
12✔
452

12✔
453
        w.editRules = append(
12✔
454
                w.editRules,
12✔
455
                internal.Delete(int(fieldsInfo.start), int(fieldsInfo.end)-1),
12✔
456
        )
12✔
457

12✔
458
        buf := &strings.Builder{}
12✔
459
        var i int
12✔
460
        for _, source := range sources {
24✔
461
                if source.mustBeQualified {
12✔
UNCOV
462
                        continue
×
463
                }
464

465
                if table != "" && source.name != table {
12✔
UNCOV
466
                        continue
×
467
                }
468

469
                if schema != "" && source.schema != schema {
12✔
UNCOV
470
                        continue
×
471
                }
472

473
                columns = append(columns, source.columns...)
12✔
474

12✔
475
                if i > 0 {
12✔
UNCOV
476
                        buf.WriteString(", ")
×
UNCOV
477
                }
×
478
                expandQuotedSource(buf, source, prefix)
12✔
479
                i++
12✔
480
        }
481
        w.editRules = append(
12✔
482
                w.editRules,
12✔
483
                internal.Insert(int(fieldsInfo.start), buf.String()),
12✔
484
        )
12✔
485

12✔
486
        for i := range columns {
72✔
487
                columns[i].name = prefix + columns[i].name
60✔
488
        }
60✔
489

490
        return columns
12✔
491
}
492

493
func expandQuotedSource(buf *strings.Builder, source queryResult, prefix string) {
12✔
494
        for i, col := range source.columns {
72✔
495
                if i > 0 {
108✔
496
                        buf.WriteString(", ")
48✔
497
                }
48✔
498
                if source.schema != "" {
60✔
UNCOV
499
                        fmt.Fprintf(buf, "%q.%q.%q AS %q", source.schema, source.name, col.name, prefix+col.name)
×
500
                } else {
60✔
501
                        fmt.Fprintf(buf, "%q.%q AS %q", source.name, col.name, prefix+col.name)
60✔
502
                }
60✔
503
        }
504
}
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

© 2025 Coveralls, Inc