Skip to content

Commit ea8596a

Browse files
committed
Let the testing use a shared PostgreSQL database instead of sqlite3 files
Second part of issue #295: the test machines can now write their results to one network database and coordinate through it, instead of each copying a sqlite3 file in, testing, and copying it back - the step that made two machines overwrite each other depending on which one finished last. resultsdb.py holds both backends behind one interface. The scripts write the same statements for either, with "?" as the placeholder, and ask the connection where the dialects genuinely differ: quoting a branch name, testing whether a table exists, concatenating a group, counting a condition, matching a branch without case, skipping a row that is already there. Every script takes --db, which defaults to the LIBTEST_DB environment variable, so Jenkins sets the database once for the whole pipeline rather than on forty invocations. A machine claims a job in [job_claim] before testing a library, keyed by exactly the question the run already asks: which library, in which version, against which compiler and configuration. Only one machine can win the claim, and the others skip that library and move on. The winner refreshes a heartbeat every minute and marks the claim done when the results are written, so a machine that dies parks its jobs for STALE_CLAIM_MINUTES rather than forever. A local sqlite3 file has a single writer, so there claim() always says yes. Two PostgreSQL specifics were needed for the reports: GROUP_CONCAT relies on sqlite keeping the order of the subquery that feeds it, so the ported query orders inside the aggregate, and COUNT(x or null) becomes COUNT(*) FILTER. Running the report queries against ripper1's sqlite file and against the migrated database gives the same rows, down to the phase counts, the per-phase sums and the regression rows: checked on master, whose table holds 43 million of them, and on heavy_tests, conversion and basemodelica_jl_master. The two only render the doubles with a different number of digits, and the report parses them back with float(). The Jenkinsfile gets a "postgres" parameter, on by default, which selects the database and drops the download and the publishing of sqlite3.db. Unticking it restores the old behaviour unchanged. The password comes from a Jenkins secret file credential, omdb-pgpass, bound to PGPASSFILE, so it never reaches a command line or the build log. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
1 parent ec1c7f1 commit ea8596a

12 files changed

Lines changed: 631 additions & 125 deletions

.CI/Jenkinsfile

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ pipeline {
33
parameters {
44
booleanParam(name: 'OLDLIBS', defaultValue: false, description: 'Also test some outdated libraries')
55

6+
booleanParam(name: 'postgres', defaultValue: true, description: 'Store the results in the shared PostgreSQL database (omdb on openmodelica.org) rather than in the per-machine sqlite3 file. Machines coordinate through it, so two of them no longer overwrite each other. Untick to go back to the sqlite3 files.')
7+
68
booleanParam(name: 'v1_26', defaultValue: false, description: 'maintenance/v1.26 branch (ryzen-5950x-1)')
79
booleanParam(name: 'v1_27', defaultValue: false, description: 'maintenance/v1.27 branch (ryzen-5950x-1)')
810
booleanParam(name: 'master', defaultValue: false, description: 'master branch (ryzen-5950x-1)')
@@ -40,6 +42,12 @@ pipeline {
4042
}
4143
environment {
4244
LC_ALL = 'C.UTF-8'
45+
// Where the results go. The scripts take it from here instead of a --db
46+
// option on every single invocation.
47+
LIBTEST_DB = "${params.postgres ? 'postgresql://om@openmodelica.org/omdb' : 'sqlite3.db'}"
48+
// A secret file holding one pgpass line; libpq reads the password from it,
49+
// so it never reaches a command line or the build log.
50+
PGPASSFILE = credentials('omdb-pgpass')
4351
}
4452
stages {
4553
stage('test') { parallel {
@@ -506,7 +514,12 @@ pipeline {
506514
cd OpenModelica
507515
git fetch
508516
'''
509-
sh 'wget -q https://libraries.openmodelica.org/sqlite3/ripper1/sqlite3.db'
517+
// The reports read the shared database directly when it is in use.
518+
script {
519+
if (!params.postgres) {
520+
sh 'wget -q https://libraries.openmodelica.org/sqlite3/ripper1/sqlite3.db'
521+
}
522+
}
510523
sh './clean-empty-omcversion-dates.py'
511524

512525
sh "./all-reports.py --email --omcgitdir=OpenModelica ${env.GITBRANCHES} conversion heavy_tests"
@@ -572,7 +585,11 @@ pipeline {
572585
cd OpenModelica
573586
git fetch
574587
'''
575-
sh 'wget -q https://libraries.openmodelica.org/sqlite3/ripper2/sqlite3.db'
588+
script {
589+
if (!params.postgres) {
590+
sh 'wget -q https://libraries.openmodelica.org/sqlite3/ripper2/sqlite3.db'
591+
}
592+
}
576593
sh './clean-empty-omcversion-dates.py'
577594

578595
sh "./all-reports.py --email --omcgitdir=OpenModelica ${env.GITBRANCHES_FMI} ${env.GITBRANCHES_NEWINST} ${env.GITBRANCHES_DAE} ${env.GITBRANCHES_NEWBACKEND_DAE} ${env.GITBRANCHES_CPP} gbode cvode ida"
@@ -1010,14 +1027,17 @@ def runRegressiontest(branch, name, extraFlags, omsHash, dbPrefix, sshConfig, om
10101027

10111028
sh "test -d '${libraryPath}/.openmodelica/libraries/Modelica trunk'"
10121029

1013-
sh """
1014-
if ! test -f ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db; then
1015-
wget -O ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db.tmp -q https://libraries.openmodelica.org/sqlite3/${dbPrefix}/sqlite3.db
1016-
mv ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db.tmp ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db
1017-
fi
1018-
cp ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db OpenModelicaLibraryTesting/sqlite3.db
1019-
test -s OpenModelicaLibraryTesting/sqlite3.db
1020-
"""
1030+
// The shared database needs none of this: the results go straight into it.
1031+
if (!params.postgres) {
1032+
sh """
1033+
if ! test -f ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db; then
1034+
wget -O ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db.tmp -q https://libraries.openmodelica.org/sqlite3/${dbPrefix}/sqlite3.db
1035+
mv ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db.tmp ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db
1036+
fi
1037+
cp ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db OpenModelicaLibraryTesting/sqlite3.db
1038+
test -s OpenModelicaLibraryTesting/sqlite3.db
1039+
"""
1040+
}
10211041

10221042
sh 'date'
10231043

@@ -1040,11 +1060,16 @@ def runRegressiontest(branch, name, extraFlags, omsHash, dbPrefix, sshConfig, om
10401060
stdbuf -oL -eL time ./test.py --ompython_omhome=/usr ${FMI_TESTING_FLAG} --extraflags='${extraFlags}' --extrasimflags='${extrasimflags}' ${testFlags} --branch="${name}" --output="libraries.openmodelica.org:/var/www/libraries.openmodelica.org/branches/${name}/" --libraries='${libraryPath}/.openmodelica/libraries/' --jobs=${jobs} ${libs_config_file} ${params.OLDLIBS ? "configs/conf-old.json configs/conf-nonstandard.json" : ""} || (killall omc ; false) || exit 1
10411061
""")
10421062
sh 'date'
1043-
sh "rm -f OpenModelicaLibraryTesting/${dbPrefix}-sqlite3.db.tmp"
1044-
sh "ln OpenModelicaLibraryTesting/sqlite3.db OpenModelicaLibraryTesting/${dbPrefix}-sqlite3.db.tmp"
10451063
sh "cd OpenModelicaLibraryTesting/ && ./clean-empty-omcversion-dates.py"
1046-
sh "cp OpenModelicaLibraryTesting/sqlite3.db ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db"
1047-
sh "rm -f ~/TEST_LIBS_BACKUP/${dbPrefix}-`date +sqlite3.%Y%m%d.db`"
10481064

1049-
sshPublisher(publishers: [sshPublisherDesc(configName: sshConfig, transfers: [sshTransfer(removePrefix: 'OpenModelicaLibraryTesting', sourceFiles: 'OpenModelicaLibraryTesting/sqlite3.db')])], failOnError: true)
1065+
// Copying the file back is what made two machines overwrite each other's
1066+
// results, so it only happens while a job still writes its own sqlite3 file.
1067+
if (!params.postgres) {
1068+
sh "rm -f OpenModelicaLibraryTesting/${dbPrefix}-sqlite3.db.tmp"
1069+
sh "ln OpenModelicaLibraryTesting/sqlite3.db OpenModelicaLibraryTesting/${dbPrefix}-sqlite3.db.tmp"
1070+
sh "cp OpenModelicaLibraryTesting/sqlite3.db ~/TEST_LIBS_BACKUP/${dbPrefix}-sqlite3.db"
1071+
sh "rm -f ~/TEST_LIBS_BACKUP/${dbPrefix}-`date +sqlite3.%Y%m%d.db`"
1072+
1073+
sshPublisher(publishers: [sshPublisherDesc(configName: sshConfig, transfers: [sshTransfer(removePrefix: 'OpenModelicaLibraryTesting', sourceFiles: 'OpenModelicaLibraryTesting/sqlite3.db')])], failOnError: true)
1074+
}
10501075
}

.CI/build-dep/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
FROM docker.openmodelica.org/build-deps:v1.16.3
22

33
RUN apt-get update && apt-get install libxml2 libxslt1.1 libxml2-dev libxslt1-dev
4-
RUN pip3 install matplotlib FMPy
4+
RUN pip3 install matplotlib FMPy psycopg2-binary

all-plots.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import sys, argparse, subprocess, os
55
import simplejson as json
6-
import shared
6+
import shared, resultsdb
77
import re, time, math
88
from omcommon import friendlyStr
99

@@ -18,18 +18,19 @@
1818
parser = argparse.ArgumentParser(description='OpenModelica model testing report generation tool')
1919
parser.add_argument('branches', nargs='*')
2020
parser.add_argument('--historypath', default="history")
21+
resultsdb.addArgument(parser)
2122
args = parser.parse_args()
2223

2324
branches = [branch.split("/")[-1] for branch in args.branches]
2425
fnameprefix = args.historypath
2526

2627
libs = {}
2728

28-
import cgi, sqlite3, time, datetime
29+
import cgi, time, datetime
2930
from omcommon import friendlyStr, multiple_replace
3031

31-
conn = sqlite3.connect('sqlite3.db')
32-
cursor = conn.cursor()
32+
db = resultsdb.connect(args.db)
33+
cursor = db.cursor()
3334

3435
def dateStr(dint):
3536
return str(datetime.datetime.fromtimestamp(dint).strftime('%Y-%m-%d %H:%M:%S'))
@@ -98,8 +99,7 @@ def plotLibrary(branch, libname, xs, total, frontend,backend,simcode,template,co
9899

99100
for branch in branches:
100101
try:
101-
cursor.execute("SELECT name FROM [sqlite_master] WHERE type='table' AND name=?", (branch,))
102-
one = cursor.fetchone()
102+
one = (branch,) if db.tableExists(branch) else None
103103
if one == None:
104104
print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch)
105105
# ignore this table and continue
@@ -112,13 +112,13 @@ def plotLibrary(branch, libname, xs, total, frontend,backend,simcode,template,co
112112
# ignore this table and continue
113113
continue
114114

115-
cursor.execute('''CREATE INDEX IF NOT EXISTS [idx_%s_date] ON [%s](date)''' % (branch,branch))
115+
db.createDateIndex(branch)
116116
libs = {}
117-
for (date,libname,total,frontend,backend,simcode,template,compile,simulate,verify) in cursor.execute("""SELECT date,libname,COUNT(finalphase),COUNT(finalphase>=1 or null),COUNT(finalphase>=2 or null),COUNT(finalphase>=3 or null),COUNT(finalphase>=4 or null),COUNT(finalphase>=5 or null),COUNT(finalphase>=6 or null),COUNT(finalphase>=7 or null)
118-
FROM [%s]
117+
for (date,libname,total,frontend,backend,simcode,template,compile,simulate,verify) in cursor.execute("""SELECT date,libname,COUNT(finalphase),%s
118+
FROM %s
119119
GROUP BY date,libname
120120
ORDER BY libname,date ASC
121-
""" % (branch)):
121+
""" % (",".join(db.countIf("finalphase>=%d" % i) for i in range(1,8)), db.quote(branch))):
122122
if libname not in libs:
123123
libs[libname] = ([],[],[],[],[],[],[],[],[])
124124
libs[libname][0].append(datetime.datetime.fromtimestamp(date))

all-reports.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import codecs
66
import sys, argparse, subprocess, os, time
77
import simplejson as json
8-
import shared
8+
import shared, resultsdb
99
import re
1010
from omcommon import friendlyStr
1111

@@ -17,6 +17,7 @@
1717
parser.add_argument('--githuburltesting', default="https://github.com/OpenModelica/OpenModelicaLibraryTesting/commit")
1818
parser.add_argument('--omcgitdir', default="../OpenModelica/OpenModelica")
1919
parser.add_argument('--email', default=False, action='store_true')
20+
resultsdb.addArgument(parser)
2021
args = parser.parse_args()
2122

2223
os.environ['TZ'] = 'Europe/Stockholm'
@@ -44,11 +45,11 @@
4445

4546
libs = {}
4647

47-
import cgi, sqlite3, time, datetime
48+
import cgi, time, datetime
4849
from omcommon import friendlyStr, multiple_replace
4950

50-
conn = sqlite3.connect('sqlite3.db')
51-
cursor = conn.cursor()
51+
db = resultsdb.connect(args.db)
52+
cursor = db.cursor()
5253

5354
def dateStr(dint):
5455
return str(datetime.datetime.fromtimestamp(dint).strftime('%Y-%m-%d %H:%M:%S'))
@@ -70,8 +71,7 @@ def modelLink(libname, modelname, extension, text):
7071
emails_to_send = {}
7172
for branch in branches:
7273
try:
73-
cursor.execute("SELECT name FROM [sqlite_master] WHERE type='table' AND name=?", (branch,))
74-
one = cursor.fetchone()
74+
one = (branch,) if db.tableExists(branch) else None
7575
if one == None:
7676
print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch)
7777
# ignore this table and continue
@@ -86,8 +86,8 @@ def modelLink(libname, modelname, extension, text):
8686
missing_branches.append(branch)
8787
continue
8888

89-
cursor.execute('''CREATE INDEX IF NOT EXISTS [idx_%s_date] ON [%s](date)''' % (branch,branch))
90-
cursor.execute("SELECT date,omcversion FROM [omcversion] WHERE branch LIKE ? COLLATE NOCASE ORDER BY date ASC", (branch,))
89+
db.createDateIndex(branch)
90+
cursor.execute("SELECT date,omcversion FROM omcversion WHERE %s ORDER BY date ASC" % db.likeNoCase("branch"), (branch,))
9191
entries = cursor.fetchall()
9292
n = len(entries)
9393
urlToOpen = "%s/%s/00_history.html" % (historyurl, branch)
@@ -152,11 +152,11 @@ def modelLink(libname, modelname, extension, text):
152152
gitloglibrarytesting = "<tr><td>could not get the git log for OpenModelicaLibraryTesting</td></tr>"
153153

154154
tpl = tpl.replace("#OMCGITLOG#",gitlog).replace("#NUMCOMMITS#",str(gitlog.count("<tr>"))).replace("#3rdParty#",thirdPartyChanged).replace("#OMCLIBRARYTESTINGGITLOG#",gitloglibrarytesting)
155-
libnames = [libname for (libname,) in cursor.execute("""SELECT libname FROM [%s] WHERE date=? GROUP BY libname""" % branch, (d2,))]
155+
libnames = [libname for (libname,) in cursor.execute("""SELECT libname FROM %s WHERE date=? GROUP BY libname""" % db.quote(branch), (d2,))]
156156
startdates = {}
157157
# Get previous date of each library run and group them together for fast queries later
158158
for libname in libnames:
159-
ds = cursor.execute("""SELECT date FROM [%s] WHERE date<? AND libname=? ORDER BY date DESC LIMIT 1""" % branch, (d2,libname)).fetchall()
159+
ds = cursor.execute("""SELECT date FROM %s WHERE date<? AND libname=? ORDER BY date DESC LIMIT 1""" % db.quote(branch), (d2,libname)).fetchall()
160160
if len(ds)==0:
161161
continue
162162
((d1lib,),) = ds
@@ -166,10 +166,14 @@ def modelLink(libname, modelname, extension, text):
166166
regressions = []
167167
for d1lib in startdates.keys():
168168
# Order by date so we can select and know which is the older and which is the newer value... for finalphase, and the execution times
169-
# Note: GROUP_CONCAT returns both values as a string... So you need to split it later
170-
query = """SELECT model,libname,GROUP_CONCAT(finalphase),GROUP_CONCAT(frontend),GROUP_CONCAT(backend),GROUP_CONCAT(simcode),GROUP_CONCAT(templates),GROUP_CONCAT(compile),GROUP_CONCAT(simulate) FROM
171-
(SELECT model,libname,finalphase,frontend,backend,simcode,templates,compile,simulate FROM [%s] WHERE date IN (?,?) AND libname IN (%s) ORDER BY date)
172-
GROUP BY model,libname HAVING
169+
# Note: the group concatenation returns both values as a string... So you need to split it
170+
# later. The order is the one of the dates, which PostgreSQL only guarantees when the
171+
# aggregate says so, hence the date column in the inner query.
172+
concat = ",".join(db.groupConcat(c, "date") for c in
173+
["finalphase","frontend","backend","simcode","templates","compile","simulate"])
174+
query = ("""SELECT model,libname,%s FROM
175+
(SELECT model,libname,date,finalphase,frontend,backend,simcode,templates,compile,simulate FROM %%s WHERE date IN (?,?) AND libname IN (%%s) ORDER BY date) AS phases
176+
GROUP BY model,libname HAVING""" % concat + """
173177
(MIN(finalphase) <> MAX(finalphase)) OR
174178
((MIN(finalphase) >= ?) AND
175179
(MAX(frontend) > ?*MIN(frontend) AND MAX(frontend) > ?) OR
@@ -179,7 +183,7 @@ def modelLink(libname, modelname, extension, text):
179183
(MAX(compile) > ?*MIN(compile) AND MAX(compile) > ?) OR
180184
(MAX(simulate) > ?*MIN(simulate) AND MAX(simulate) > ?)
181185
)
182-
""" % (branch,",".join(["'%s'" % libname for libname in startdates[d1lib]]))
186+
""") % (db.quote(branch),",".join(["'%s'" % libname for libname in startdates[d1lib]]))
183187
cursor.execute(query, (d1lib,d2,timeMinPhase,timeRel,timeAbs,timeRel,timeAbs,timeRel,timeAbs,timeRel,timeAbs,timeRel,2*timeAbs,timeRel,timeAbs))
184188
regressions += cursor.fetchall()
185189
regressions = sorted(regressions, key = lambda x: (x[1],x[0]))
@@ -228,10 +232,10 @@ def modelLink(libname, modelname, extension, text):
228232

229233
libstrs = []
230234
for libname in sorted(list(libs)):
231-
cursor.execute("SELECT libversion,confighash FROM [libversion] WHERE branch LIKE ? COLLATE NOCASE AND date<=? AND libname=? ORDER BY date DESC LIMIT 1", (branch,d1,libname))
235+
cursor.execute("SELECT libversion,confighash FROM libversion WHERE %s AND date<=? AND libname=? ORDER BY date DESC LIMIT 1" % db.likeNoCase("branch"), (branch,d1,libname))
232236
(lv1,lh1) = cursor.fetchone()
233237
lv1 = lv1.strip()
234-
cursor.execute("SELECT libversion,confighash FROM [libversion] WHERE branch LIKE ? COLLATE NOCASE AND date<=? AND libname=? ORDER BY date DESC LIMIT 1", (branch,d2,libname))
238+
cursor.execute("SELECT libversion,confighash FROM libversion WHERE %s AND date<=? AND libname=? ORDER BY date DESC LIMIT 1" % db.likeNoCase("branch"), (branch,d2,libname))
235239
(lv2,lh2) = cursor.fetchone()
236240
lv2 = lv2.strip()
237241
if lv1 != lv2:

clean-dates.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
#!/usr/bin/env python3
22

3-
import argparse, sqlite3, sys
3+
import argparse, sys
4+
import resultsdb
45
from datetime import datetime
56

67
parser = argparse.ArgumentParser(description='OpenModelica library testing tool')
78
parser.add_argument('startDate')
89
parser.add_argument('stopDate')
10+
resultsdb.addArgument(parser)
911

1012
args = parser.parse_args()
1113

@@ -29,12 +31,12 @@
2931
sys.stdout.write("Please respond with 'yes' or 'no'")
3032
sys.exit(1)
3133

32-
conn = sqlite3.connect('sqlite3.db')
33-
cursor = conn.cursor()
34+
db = resultsdb.connect(args.db)
35+
cursor = db.cursor()
3436

35-
tables = [tbl for (tbl,) in cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")]
37+
tables = db.tables()
3638
for tbl in tables:
37-
cursor.execute("DELETE FROM [%s] WHERE date<? AND date>?" % tbl, (stopTime.timestamp(),startTime.timestamp()))
38-
conn.commit()
39-
conn.execute("VACUUM")
40-
conn.close()
39+
cursor.execute("DELETE FROM %s WHERE date<? AND date>?" % db.quote(tbl), (stopTime.timestamp(),startTime.timestamp()))
40+
db.commit()
41+
db.vacuum()
42+
db.close()

clean-empty-omcversion-dates.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
#!/usr/bin/env python3
22

3-
import argparse, sqlite3, sys
3+
import argparse, sys
4+
import resultsdb
45
from datetime import datetime
56

67
parser = argparse.ArgumentParser(description='OpenModelica library testing tool')
8+
resultsdb.addArgument(parser)
79

810
args = parser.parse_args()
911

10-
conn = sqlite3.connect('sqlite3.db')
11-
cursor = conn.cursor()
12+
db = resultsdb.connect(args.db)
13+
cursor = db.cursor()
1214

1315
entries = cursor.execute("SELECT date,branch FROM omcversion").fetchall()
1416
dropped=0
@@ -20,18 +22,22 @@
2022
branchDates[branch] = set()
2123
branchDates[branch].add(date)
2224
for branch in branches:
23-
data=cursor.execute("SELECT DISTINCT date FROM [%s]" % branch).fetchall()
25+
# The shared database holds the branches of every machine, including ones
26+
# this one never created a result table for.
27+
if not db.tableExists(branch):
28+
continue
29+
data=cursor.execute("SELECT DISTINCT date FROM %s" % db.quote(branch)).fetchall()
2430
for (date,) in data:
2531
try:
2632
branchDates[branch].remove(date)
2733
except KeyError:
2834
pass
2935
for date in branchDates[branch]:
3036
print("Dropping empty omcversion entry (%d,%s)" % (date,branch))
31-
cursor.execute("DELETE FROM [omcversion] WHERE date=? AND branch=?", (date,branch))
37+
cursor.execute("DELETE FROM omcversion WHERE date=? AND branch=?", (date,branch))
3238
dropped += 1
3339

34-
conn.commit()
40+
db.commit()
3541
if dropped>0:
36-
conn.execute("VACUUM")
37-
conn.close()
42+
db.vacuum()
43+
db.close()

0 commit comments

Comments
 (0)