sqlite3 to postgresql - migration - #296
Merged
Merged
Conversation
Member
Author
|
@AnHeuermann and @sjoelund maybe you can have a quick look at this before I activate it so we can run all jobs on all the library testing computers. |
Towards a network based SQL database for the library testing, issue OpenModelica#295. sqlite2postgres.py copies the per-machine sqlite3 databases into PostgreSQL. The PostgreSQL layout is a 1:1 mirror of the sqlite3 one - one table per branch with the same column names, only the types adapted - so that the test scripts can push new results to the network database with the statements they already use. The two machines are merged into the same tables, ripper1 first and ripper2 with --skip-existing, so that the row already in the database wins whenever a key collides. The keys the migration adds - (date, libname, model) for a branch table - are what makes a shared database possible at all: they hold across all 160 million migrated rows, and they stop a run that is pushed twice from storing its results twice. The script needs nothing but the standard library and the psql client, streams the rows through COPY in batches, and commits each batch together with its progress row in [migration_progress], so a migration that is interrupted continues where it stopped instead of duplicating rows. --index creates the keys and the index the reports need, --verify compares the row counts of both sides. --catch-up copies the runs a machine wrote after the migration read its database, which a test still using its sqlite3 file keeps doing until the jobs are switched over: a wasm-jit run of 19527 models finished right after the migration and was missing from the shared database until it was caught up. It picks the runs by date rather than by continuing from the rowid it stopped at, because VACUUM renumbers the rowids of these tables and clean-empty-omcversion-dates.py runs one after every test, and because two machines write into the same table for the branches they both test. Reading a whole database costs two to three minutes, and the keys make repeating it harmless. COPY is used in its text format rather than CSV because an empty CSV field reads back as NULL, which would silently turn the empty libversion strings of the old data into NULLs. For the same reason only the key columns are NOT NULL: the sqlite3 tables declare every column NOT NULL, but tables created by an older test.py kept their laxer declaration and the historical rows do not hold up. doc/README.md documents what is actually in the database: the per-branch tables, omcversion and libversion, what every column means, how finalphase maps to the phase names, how a test run writes its results, and how to run the migration. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
…iles Second part of issue OpenModelica#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>
adrpo
force-pushed
the
sqlite3-to-postgresql
branch
from
August 11, 2026 15:35
ea8596a to
4b7ec32
Compare
Member
Author
|
I will merge this to check on the actual computers if this works properly. We'll sort the issues afterwards. |
adrpo
added a commit
that referenced
this pull request
Aug 11, 2026
…299) Every build of the library testing has failed since #296 with org.jenkinsci.plugins.workflow.steps.MissingContextVariableException: Required context class hudson.FilePath is missing before a single stage ran. The password of the results database is a secret file, and Jenkins writes such a file into the workspace of a node; #296 bound it in the environment of the pipeline, which has agent none and therefore no workspace at all. It is now bound where a node exists: in the environment of the two report stages, beside the IDA_EMAIL credential they already bind that way, and around the test run and the cleanup in runRegressiontest, the way withSccache already binds its key. LIBTEST_DB stays in the environment of the pipeline; it is a string and needs nothing. Nothing about how the scripts read the password changes: libpq still finds it through PGPASSFILE. --- Generated by Claude Code.
adrpo
added a commit
that referenced
this pull request
Aug 11, 2026
The image the report stages run in was still built on build-deps:v1.16.3, which is the dependency image of OpenModelica 1.16. It now uses ubuntu-22.04, the one the OpenModelica repository builds against in .CI/common.groovy and every cmake Jenkinsfile, so the reports run on the same Ubuntu and the same Python as everything else. That also gets psycopg2-binary a wheel to install rather than a source build, which needs pg_config from libpq-dev; the results database needs psycopg2 since #296. Two things the change makes explicit rather than assume of the base image: apt is told -y, because an image that does not assume it turns the build into a prompt that never gets answered, and python3-pip is installed, because the line after it needs pip and the base may not carry it. --- Generated by Claude Code.
adrpo
added a commit
that referenced
this pull request
Aug 11, 2026
The results of a job went into a per-machine sqlite3 file that had to be downloaded before the run and published back afterwards, and since #296 they go into the shared database instead. Which of the two was a tick box on the job, defaulting to the shared one. Nobody should be able to send a day of testing to a file nobody reads by unticking a box, and keeping the old path around only leaves two ways of doing the same thing for someone to wonder about later. Both are gone: - the "postgres" parameter, and the branches it guarded; - copying ~/TEST_LIBS_BACKUP/<machine>-sqlite3.db into the workspace before a run, copying it back after, and the sshPublisher that uploaded it; - fetching a machine's sqlite3.db in the two report stages, which now read the shared database directly; - keeping a week of dated backups of those files on every node; - dbPrefix and sshConfig, which existed only to name and publish that file, from runRegressiontest and its twenty-three callers. The scripts themselves are untouched and still write a sqlite3 file by default: that is what a developer running test.py gets, and what the GitHub checks use. It is the pipeline that no longer has a second way of doing this. --- Generated by Claude Code.
adrpo
added a commit
that referenced
this pull request
Aug 11, 2026
The results of a job went into a per-machine sqlite3 file that had to be downloaded before the run and published back afterwards, and since #296 they go into the shared database instead. Which of the two was a tick box on the job, defaulting to the shared one. Nobody should be able to send a day of testing to a file nobody reads by unticking a box, and keeping the old path around only leaves two ways of doing the same thing for someone to wonder about later. Both are gone: - the "postgres" parameter, and the branches it guarded; - copying ~/TEST_LIBS_BACKUP/<machine>-sqlite3.db into the workspace before a run, copying it back after, and the sshPublisher that uploaded it; - fetching a machine's sqlite3.db in the two report stages, which now read the shared database directly; - keeping a week of dated backups of those files on every node; - dbPrefix and sshConfig, which existed only to name and publish that file, from runRegressiontest and its twenty-three callers. The scripts themselves are untouched and still write a sqlite3 file by default: that is what a developer running test.py gets, and what the GitHub checks use. It is the pipeline that no longer has a second way of doing this. --- Generated by Claude Code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Towards #295: the results move from a sqlite3 file per test machine to one shared PostgreSQL database, and the machines coordinate through it instead of overwriting each other.
Today every job downloads its machine's
sqlite3.db, tests, and copies the whole file back. Two machines running the same job both write a full copy, and whichever finishes last wins - the other one's results are gone. That is the thing this PR removes.What is in the database
omdbon openmodelica.org now holds all 160,247,359 rows of both machines: ripper1's 34 tables and ripper2's 54. 50 GB, or 20 GB on disk after the ZFS lz4 the tablespace sits on.The layout is a 1:1 mirror of the sqlite3 one - one table per branch, same column names, only
integer/realbecomingbigint/double precision- so the test scripts push new results with the statements they already use.What the mirror adds is a key per table, which is what makes a shared database possible at all:
<branch>(date, libname, model)omcversion(date, branch)libversion(date, branch, libname, confighash)They hold across all 160 million migrated rows, and they stop a run that is pushed twice from being stored twice.
Job claiming
Before testing a library,
test.pyclaims it injob_claim, 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. The others print
and move on to the next library. The winner refreshes
heartbeatevery minute and setsstate='done'when the results are written, so a machine that dies parks its jobs for 30 minutes rather than forever.Using it
resultsdb.pyputs both backends behind one interface. Every script takes--db, which defaults to theLIBTEST_DBenvironment variable:The password comes from
PGPASSWORDor~/.pgpass, never from the URL or a command line. The scripts write the same statements for either backend, with?as the placeholder, and ask the connection where the dialects genuinely differ:quote(),tableExists(),groupConcat(),countIf(),likeNoCase(),insertIgnore().The Jenkinsfile gets a
postgresparameter, on by default, which points the pipeline atomdband drops both thewgetofsqlite3.dband thesshPublisherthat copied it back. Untick it and the old behaviour is exactly what it was. The password is a Jenkins secret file credential,omdb-pgpass, bound toPGPASSFILEat pipeline level.Migrating and catching up
ripper1 goes in first, then ripper2 with
--skip-existing, so ripper1 wins whenever a key collides. On the current data the priority never actually fires - not one(branch, date)is shared between the two machines, not even for the four branches both of them test - so the merge is a plain union. The rule matters for re-runs and for two machines pushing later on.The script needs nothing but the standard library and the
psqlclient. It streams throughCOPYin batches and commits each batch together with its progress row inmigration_progress, so an interrupted migration continues where it stopped instead of duplicating rows. Run: ~50 minutes per machine at 48k rows/s, on the server itself.--catch-upcopies the runs a machine wrote after the migration read its database, which any job still using its sqlite3 file keeps doing until it is switched over. It picks the runs by date rather than continuing from the rowid it stopped at, becauseVACUUMrenumbers the rowids of these tables andclean-empty-omcversion-dates.pyruns one after every test - and because two machines write into the same table for the branches they both test. Two to three minutes per machine, and repeating it is harmless.How it was checked
--verifyon both machines: 0 of 34 and 0 of 54 tables incomplete; every table's row count matches its source, and the shared tables are clean unions (libversion310901 + 452246 = 763147,omcversion11271 + 8776 = 20047).\Nand unicode in model names all come back byte-identical. This is whyCOPYuses its text format and not CSV - an empty CSV field reads back as NULL, which would have silently turned the emptylibversionstrings of the old data into NULLs.resultsdb.pyself-test, both backends, 15 checks: two machines racing for one claim, the loser refused, a released claim taken over.master, whose table holds 43 million of them (92 libraries, 17 regression rows), and onheavy_tests,conversion(252 regression rows) andbasemodelica_jl_master. The two only render the doubles with a different number of digits, and the report parses them back withfloat().That last test earned its keep: it caught a
%-versus-+precedence slip in the ported regression query that would have thrownTypeErroron the firstall-reports.pyrun.Before enabling it
pip3 install psycopg2-binary. It is inrequirements.txtand in.CI/build-dep, but the non-docker node stages runtest.pywith the node's python.test.pyhas not been run end-to-end against PostgreSQL here, because that needs omc and the libraries. The claim path and the insert path are verified against synthetic and migrated data, not a live run. Worth tickingpostgreson one small job first -heavy_testsorgenerateSymbolicJacobian- and checking thatjob_claimfills in and the new rows land.--catch-uponce more right after the switch, to sweep up whatever the last sqlite-era jobs wrote in the meantime.Still open in #295: #78 (build each FMU once and simulate it with several tools) and dockerising the remaining Jenkins jobs.
Generated by Claude Code.