@@ -2,20 +2,17 @@ package main
22
33import (
44 "bufio"
5- "context"
65 "flag"
76 "fmt"
87 "log"
98 "os"
109 "os/exec"
11- "path"
1210 "path/filepath"
1311 "runtime"
1412 "slices"
1513 "strings"
1614 "sync"
1715 "sync/atomic"
18- "time"
1916
2017 "github.com/Lslightly/qlstat/config"
2118 "github.com/Lslightly/qlstat/utils"
@@ -76,14 +73,90 @@ func main() {
7673 }
7774}
7875
79- type ErrorPair struct {
80- runErr string
81- decodeErr error
76+ // -- External args helper ------------------------------------------------
77+
78+ // buildGroupExternalArgs collects all --external=... options across all queries.
79+ // Since externals are defined at the QueryGroup level, all queries share them,
80+ // but we deduplicate to be safe.
81+ func buildGroupExternalArgs (queries []config.Query , extroot string ) []string {
82+ seen := make (map [string ]bool )
83+ var result []string
84+ for _ , q := range queries {
85+ for _ , ext := range q .ExternalOptions (extroot ) {
86+ if ! seen [ext ] {
87+ seen [ext ] = true
88+ result = append (result , ext )
89+ }
90+ }
91+ }
92+ return result
8293}
8394
84- type QueryStatus struct {
85- repo string
86- pair ErrorPair
95+ // -- Query execution -----------------------------------------------------
96+
97+ // runQueriesOnDB runs all queries on a single database using codeql database run-queries,
98+ // then decodes the resulting bqrs files to the target format.
99+ //
100+ // Command:
101+ //
102+ // codeql database run-queries --search-path=... --external=... -- <dbPath> <query1> <query2> ...
103+ //
104+ // Results are placed at <dbPath>/results/<queryPack>/<queryPathNoExt>.bqrs.
105+ // Decoded output goes to <dbPath>/results/<queryPack>/<queryPathNoExt>.<tgtFmt>.
106+ func runQueriesOnDB (cfg * config.Artifact , db config.DB , queries []config.Query , tgtFmt string ) {
107+ // --- Phase 1: run-queries ---
108+ extArgs := buildGroupExternalArgs (queries , db .ExtDir ())
109+
110+ queryPaths := make ([]string , len (queries ))
111+ for i , q := range queries {
112+ queryPaths [i ] = q .QueryPath (cfg .QueryRoot )
113+ }
114+
115+ logDir := filepath .Join (cfg .PassLogDir ("query" ), db .Name )
116+ utils .MkdirAll (logDir )
117+ outFile , errFile := utils .CreateOutAndErr (filepath .Join (logDir , "run-queries" ))
118+ defer outFile .Close ()
119+ defer errFile .Close ()
120+
121+ args := []string {
122+ "database" , "run-queries" ,
123+ "--search-path=" + filepath .Join (utils .ProjectRoot (), "qlsrc/lib" ),
124+ }
125+ args = append (args , extArgs ... )
126+ args = append (args , "--" )
127+ args = append (args , db .Path ())
128+ args = append (args , queryPaths ... )
129+
130+ cmd := exec .Command ("codeql" , args ... )
131+ cmd .Stdout = outFile
132+ cmd .Stderr = errFile
133+ outFile .WriteString (cmd .String () + "\n " )
134+ if err := cmd .Run (); err != nil {
135+ fmt .Fprintf (errFile , "codeql database run-queries failed: %v\n " , err )
136+ }
137+
138+ // --- Phase 2: decode bqrs to target format ---
139+ decodeLogDir := filepath .Join (cfg .PassLogDir ("decode" ), db .Name )
140+ utils .MkdirAll (decodeLogDir )
141+
142+ for _ , query := range queries {
143+ bqrsPath := filepath .Join (db .Path (), "results/lslightly/qlstat" , query .PathNoExt ()+ ".bqrs" )
144+ csvPath := filepath .Join (db .Path (), "results/lslightly/qlstat" , query .PathNoExt ()+ "." + tgtFmt )
145+
146+ decOut , decErr := utils .CreateOutAndErr (filepath .Join (decodeLogDir , query .Name ()))
147+ cmd := exec .Command ("codeql" , "bqrs" , "decode" ,
148+ "--format=" + tgtFmt ,
149+ bqrsPath ,
150+ "--output=" + csvPath )
151+ cmd .Stdout = decOut
152+ cmd .Stderr = decErr
153+ decOut .WriteString (cmd .String () + "\n " )
154+ if err := cmd .Run (); err != nil {
155+ fmt .Fprintf (decErr , "codeql bqrs decode failed: %v\n " , err )
156+ }
157+ decOut .Close ()
158+ decErr .Close ()
159+ }
87160}
88161
89162/*
@@ -93,67 +166,31 @@ dump stdout/stderr for ${db} in ${config.OutResultRoot}/${qScriptNameNoExt}/log/
93166*/
94167func queriesExec (cfg * config.Artifact , grp config.QueryGroup , tgtFmt string ) {
95168 dbs := cfg .ConvStrSliceToDBSlice (grp .QueryDBs )
96- bar := progressbar .Default (int64 (len (dbs ) * len (grp .Queries )))
97- for _ , qScript := range grp .Queries {
98- query := config .CreateQuery (qScript , grp .Externals , grp .ExternalFiles )
99-
100- queryLogDir := path .Join (cfg .PassLogDir ("query" ), query .PathNoExt ())
101- utils .MkdirAll (queryLogDir )
102- utils .Remkdir (query .DirPath (cfg .ResultRoot ))
103-
104- var dbWg sync.WaitGroup
105- for _ , db := range dbs {
106- dbWg .Add (1 )
107- go func (db config.DB , query config.Query ) {
108- defer dbWg .Done ()
109- queryForOneDB (cfg , db , query )
110- bar .Describe (fmt .Sprintf ("query: %-15s db: %-15s exts: %-15s" , query .Name (), db .Name , query .ExternalsSingleString ()))
111- bar .Add (1 )
112- }(db , query )
113- }
114- /*
115- currently multiple queries for one database is not supported
116- `codeql database run-queries` supports multiple queries.
117- But work to collect results from codeql-db/<db>/results is needed
118- */
119- dbWg .Wait ()
120- decodeFilesInDir (cfg , tgtFmt , query )
169+
170+ // Create Query objects for all queries in the group
171+ queries := make ([]config.Query , len (grp .Queries ))
172+ for i , qScript := range grp .Queries {
173+ queries [i ] = config .CreateQuery (qScript , grp .Externals , grp .ExternalFiles )
174+ }
175+
176+ bar := progressbar .Default (int64 (len (dbs )))
177+ var wg sync.WaitGroup
178+
179+ for _ , db := range dbs {
180+ wg .Add (1 )
181+ go func (db config.DB ) {
182+ defer wg .Done ()
183+ runQueriesOnDB (cfg , db , queries , tgtFmt )
184+ bar .Describe (fmt .Sprintf ("db: %-20s %d queries" , db .Name , len (queries )))
185+ bar .Add (1 )
186+ }(db )
121187 }
188+
189+ wg .Wait ()
122190 bar .Close ()
123191}
124192
125- /*
126- codeql query run -d=${config.InDBRoot}/${repo} ${config.QueryRoot}/${qScript} --output=${qResultDir}/${repo} --search-path=./qlsrc/lib --external=$pred/${config.dbRoot}/${repo}/ext/$pred.csv
127- */
128- func queryForOneDB (cfg * config.Artifact , db config.DB , query config.Query ) {
129- qResultDir := query .DirPath (cfg .ResultRoot )
130- repoOut , repoErr := utils .CreateOutAndErr (filepath .Join (query .DirPath (cfg .PassLogDir ("query" )), db .Name ))
131- defer repoOut .Close ()
132- defer repoErr .Close ()
133-
134- ctx , cancel := context .WithTimeout (context .TODO (), 10 * time .Minute )
135- defer cancel ()
136-
137- cmd := exec .CommandContext (ctx , "codeql" ,
138- append ([]string {
139- "query" ,
140- "run" ,
141- fmt .Sprintf ("-d=%s" , db .Path ()),
142- fmt .Sprintf ("--search-path=%s" , filepath .Join (cfg .QueryRoot , "lib" )),
143- query .QueryPath (cfg .QueryRoot ),
144- fmt .Sprintf ("--output=%s" , filepath .Join (qResultDir , db .Name )+ ".bqrs" ),
145- },
146- query .ExternalOptions (db .ExtDir ())... ,
147- )... ,
148- )
149- cmd .Stdout = repoOut
150- cmd .Stderr = repoErr
151- repoOut .WriteString (cmd .String () + "\n " )
152- // TODO report errors in query
153- _ = cmd .Run ()
154- repoOut .Sync ()
155- repoErr .Sync ()
156- }
193+ // -- Decoding ------------------------------------------------------------
157194
158195func checkDecodeTargetFmt (tgtFmt string ) bool {
159196 if slices .Contains (validFmts , tgtFmt ) {
@@ -164,116 +201,97 @@ func checkDecodeTargetFmt(tgtFmt string) bool {
164201}
165202
166203/*
167- codeql bqrs decode --format=${tgtFmt} ${path}/${fileBase }.bqrs --output=${path}/${fileBase }.${tgtFmt}
204+ codeql bqrs decode --format=${tgtFmt} ${dbPath}/results/lslightly/qlstat/${queryPathNoExt }.bqrs --output=${dbPath}/results/lslightly/qlstat/${queryPathNoExt }.${tgtFmt}
168205*/
169206func decodeResults (cfg * config.Artifact , tgtFmt string ) {
170207 checkDecodeTargetFmt (tgtFmt )
171208
172- // decode bqrs only in query result dir
173209 for grpi , grp := range cfg .QueryGrps {
210+ dbs := cfg .ConvStrSliceToDBSlice (grp .QueryDBs )
174211 queries := grp .Queries
175- bar := progressbar . Default ( int64 ( len ( queries )))
176- defer bar . Close ( )
212+
213+ bar := progressbar . Default ( int64 ( len ( queries ) * len ( dbs )) )
177214 for _ , qScript := range queries {
178- bar .Add (1 )
179215 query := config .CreateQuery (qScript , grp .Externals , grp .ExternalFiles )
180- bar .Describe (fmt .Sprintf ("Grp %d: Decoding %s dir" , grpi , query .Name ()))
181- decodeFilesInDir (cfg , tgtFmt , query )
182- }
183- }
184- }
216+ for _ , db := range dbs {
217+ bqrsPath := filepath .Join (db .Path (), "results/lslightly/qlstat" , query .PathNoExt ()+ ".bqrs" )
218+ csvPath := filepath .Join (db .Path (), "results/lslightly/qlstat" , query .PathNoExt ()+ "." + tgtFmt )
185219
186- func decodeFilesInDir (cfg * config. Artifact , tgtFmt string , query config. Query ) {
187- decodeLogDir := filepath . Join ( cfg . PassLogDir ( "decode" ), query . PathNoExt () )
188- utils .MkdirAll ( decodeLogDir )
220+ decodeLogDir := filepath . Join (cfg . PassLogDir ( "decode" ), db . Name )
221+ utils . MkdirAll ( decodeLogDir )
222+ decOut , decErr := utils .CreateOutAndErr ( filepath . Join ( decodeLogDir , query . Name ()) )
189223
190- resultRoot := query .DirPath (cfg .ResultRoot )
191- dirEntries , err := os .ReadDir (resultRoot )
192- if err != nil {
193- log .Fatal (err )
194- }
195- var wg sync.WaitGroup
196- for _ , de := range dirEntries {
197- if de .IsDir () || filepath .Ext (de .Name ()) != ".bqrs" {
198- continue
224+ cmd := exec .Command ("codeql" , "bqrs" , "decode" ,
225+ "--format=" + tgtFmt ,
226+ bqrsPath ,
227+ "--output=" + csvPath )
228+ cmd .Stdout = decOut
229+ cmd .Stderr = decErr
230+ decOut .WriteString (cmd .String () + "\n " )
231+ _ = cmd .Run ()
232+ decOut .Close ()
233+ decErr .Close ()
234+
235+ bar .Describe (fmt .Sprintf ("Grp %d: decoding %s/%s" , grpi , query .Name (), db .Name ))
236+ bar .Add (1 )
237+ }
199238 }
200- wg .Add (1 )
201- go func () {
202- defer wg .Done ()
203- outFile , errFile := utils .CreateOutAndErr (filepath .Join (decodeLogDir , de .Name ()))
204- defer outFile .Close ()
205- defer errFile .Close ()
206- tgtPath := filepath .Join (resultRoot , strings .TrimSuffix (de .Name (), filepath .Ext (de .Name ()))+ "." + tgtFmt )
207- cmd := exec .Command ("codeql" , "bqrs" , "decode" ,
208- "--format=" + tgtFmt ,
209- filepath .Join (resultRoot , de .Name ()),
210- "--output=" + tgtPath )
211- _ = cmd .Run ()
212- }()
239+ bar .Close ()
213240 }
214- wg .Wait ()
215241}
216242
243+ // -- CSV collection ------------------------------------------------------
244+
217245func collectCSVs (cfg * config.Artifact ) {
218246 for grpi , grp := range cfg .QueryGrps {
219247 bar := progressbar .Default (int64 (len (grp .Queries )))
220248 for _ , qScript := range grp .Queries {
221249 query := config .CreateQuery (qScript , grp .Externals , grp .ExternalFiles )
222250 bar .Describe (fmt .Sprintf ("Grp %d: Collecting for " + query .PathNoExt (), grpi ))
223- collectCSVsForQuery (cfg , query )
251+ collectCSVsForQuery (cfg , query , grp )
224252 bar .Add (1 )
225253 }
226254 }
227255}
228256
229- func collectCSVsForQuery (cfg * config.Artifact , query config.Query ) {
230- qResultDir := query .DirPath (cfg .ResultRoot )
231- qResultCSV := qResultDir + ".csv"
257+ func collectCSVsForQuery (cfg * config.Artifact , query config.Query , grp config.QueryGroup ) {
258+ qResultCSV := query .DirPath (cfg .ResultRoot ) + ".csv"
232259
233- // Create or truncate the output CSV file
234260 outFile := utils .CreateFile (qResultCSV )
235261 defer outFile .Close ()
236262
237- // Read all CSV files in the result directory
238- files , err := os .ReadDir (qResultDir )
239- if err != nil {
240- log .Fatalf ("Error reading directory %s: %v" , qResultDir , err )
241- }
242-
263+ dbs := cfg .ConvStrSliceToDBSlice (grp .QueryDBs )
243264 var ticket atomic.Int64
244265 var headerWritten atomic.Bool
245266 var expectedHeader string
246267 contentChan := make (chan string , cfg .ParallelCore )
247268 var hasFile bool
248269
249270 var wg sync.WaitGroup
250- for _ , file := range files {
251- if file .IsDir () || filepath .Ext (file .Name ()) != ".csv" {
271+ for _ , db := range dbs {
272+ csvPath := filepath .Join (db .Path (), "results/lslightly/qlstat" , query .PathNoExt ()+ ".csv" )
273+
274+ // Check if the csv file exists
275+ if _ , err := os .Stat (csvPath ); os .IsNotExist (err ) {
252276 continue
253277 }
254278 hasFile = true
255279
256280 wg .Add (1 )
257- go func (file os. DirEntry , qResultDir string ) {
281+ go func (csvPath , dbName string ) {
258282 defer wg .Done ()
259- // Get repo name from filename (without .csv extension)
260- repoName := strings .TrimSuffix (file .Name (), ".csv" )
261283
262- // For the first file, write header with repo_name column
263- fpath := filepath .Join (qResultDir , file .Name ())
264- // Open the CSV file
265- inFile , err := os .Open (fpath )
284+ inFile , err := os .Open (csvPath )
266285 if err != nil {
267- log .Fatalf ("Error opening file %s: %v" , fpath , err )
286+ log .Fatalf ("Error opening file %s: %v" , csvPath , err )
268287 }
269288 defer inFile .Close ()
270289
271290 scanner := bufio .NewScanner (inFile )
272291
273292 if ! scanner .Scan () {
274- log .Fatalf ("Error reading header from %s" , fpath )
293+ log .Fatalf ("Error reading header from %s" , csvPath )
275294 }
276- // Read header
277295 currentHeader := scanner .Text ()
278296
279297 firstFile := ticket .Add (1 ) == 1
@@ -284,7 +302,7 @@ func collectCSVsForQuery(cfg *config.Artifact, query config.Query) {
284302 for ! headerWritten .Load () {
285303 }
286304 if currentHeader != expectedHeader {
287- log .Fatalf ("Warning: Header mismatch in file %s. Expected: %s, Got: %s" , fpath , expectedHeader , currentHeader )
305+ log .Fatalf ("Warning: Header mismatch in file %s. Expected: %s, Got: %s" , csvPath , expectedHeader , currentHeader )
288306 }
289307 }
290308 lines := make ([]string , 0 )
@@ -293,10 +311,10 @@ func collectCSVsForQuery(cfg *config.Artifact, query config.Query) {
293311 if line == "" {
294312 break
295313 }
296- lines = append (lines , line + "," + repoName )
314+ lines = append (lines , line + "," + dbName )
297315 }
298316 contentChan <- strings .Join (lines , "\n " )
299- }(file , qResultDir )
317+ }(csvPath , db . Name )
300318 }
301319 go func () {
302320 wg .Wait ()
0 commit comments