Navicat Premium 18 on Linux: a hands-on review with Postgres, SQLite, Oracle and MySQL
Disclosure
Disclosure: Navicat gave me free Premium access for this review and offered a one-year Premium subscription (about US$1,000) if I published. Navicat did not see or approve this text, and told me the review did not have to be positive.
I tested Navicat Premium 18 (AppImage) on Ubuntu against four engines: a local Postgres 16 container, a SQLite file, an Oracle Autonomous Database on Oracle Cloud's Always Free tier (Oracle AI Database 26ai), and a MySQL HeatWave database on the same cloud. Everything used invented finance-style data.
I include the SQL and the setup commands, so you can repeat each test.
The short version
Worked well: Postgres to Postgres transfer and synchronization, with the exact SQL shown before anything ran. Oracle to Oracle transfer, structure sync and data sync also worked. On MySQL HeatWave, Data Transfer, Structure Synchronization, Data Synchronization and Data Masking all finished with 0 errors.
Needed manual checks:
- Postgres to SQLite lost auto-numbering and defaults.
- Postgres to MySQL kept the keys and the foreign key but turned
SERIALinto a plainintand dropped thenow()default. - Postgres to Oracle mapped timestamps to
DATE. The customers transfer failed when Navicat's generatedTO_DATEexpression received fractional seconds its format did not allow, and trimming the source timestamps to whole seconds got those rows through.
Rough edges on Linux: connecting to an Autonomous Database took a wallet, Oracle Instant Client and a libaio link, and the Net Service Name dropdown stayed empty. The SSH tunnel to MySQL HeatWave showed a key passphrase prompt although my key has none.
AI features: they work with a model you bring, and the results depend on it. I used a free Nvidia model through OpenRouter.
Not tested: real or large data, other platforms and stronger AI models.
What I tested and how
When and where: November 2026, with Navicat Premium 18 as an AppImage on Ubuntu.
Data: a customers table (8 rows, with name, email, phone, national ID and county) and a transactions table (200 rows, with amounts in KES). Larger copies, customers_ai (about 1,000 generated rows) and transactions_ai (200 rows), were used for the AI and transfer tests. Everything was fake. Nothing real went through Navicat or through any AI provider.
AI model: a free Nvidia Nemotron model reached through OpenRouter. Frontier models such as Claude from Anthropic, Kimi from Moonshot and GPT from OpenAI typically need API keys and may incur usage charges, which is why I chose a free one. It is not a model Navicat bundles, so anything below about AI output reflects that choice.
Data Masking
Data Masking lives under Tools and worked on Postgres, but partial masking keeps fragments of the original, so choose techniques by risk. I picked a technique for each column from a dropdown. Most of my choices produced partial masking: asterisks replace most characters while the format stays realistic.
| Column | What the output looked like | What still leaks |
|---|---|---|
| full_name | A**** W******* | initial letters and name length |
| a*****w@e**********m | first and last character of the local part, last character of the domain | |
| phone | +25******0001 | country prefix and last 4 digits |
| national_id | 1******* on one copy, NULL on another | one digit when partial, nothing when nulled |
| county , customer_id , created_at left unmasked on purpose | not applicable |
I left customer_id alone because masking a key would break the link to transactions. Nulling the national ID was the safest result, but it only works on columns that allow NULL, so it would fail on full_name. Partial masking is fine for stopping casual viewing in test data. For anything that must resist re-identification, use a substitution technique or null the column.
Queries I used for the masking test
The masked output went into a copy of customers called customers_ai, so I could compare it with the original. I first created the copy with AS TABLE, which copies the columns and rows but not the keys or defaults, then dropped it and used LIKE ... INCLUDING ALL, which copies the structure with its constraints, defaults and indexes but no rows.
CREATE TABLE customers_ai AS TABLE customers ;
DROP TABLE customers_ai ;
CREATE TABLE customers_ai ( LIKE customers INCLUDING ALL ) ;
To see what each technique did, I joined the original and masked tables side by side:
SELECT o . customer_id , o . full_name AS original_name , m . full_name AS masked_name , o . email AS original_email , m . email AS masked_email FROM customers o JOIN customers_ai m USING ( customer_id ) ;
For the AI tests I wanted realistic names again, so I replaced the masked names in customers_ai by cycling through a list of 20 invented Kenyan names:
UPDATE customers_ai ai SET full_name = np . full_name FROM ( SELECT customer_id , row_number ( ) OVER ( ORDER BY customer_id ) AS rn FROM customers_ai ) t JOIN ( SELECT full_name , row_number ( ) OVER ( ) AS rn , count (*) OVER () AS total FROM (VALUES ('Wanjiku Kamau'), ('Otieno Odhiambo'), ('Njeri Mwangi'), ('Mwende Mutiso'), ('Akinyi Odongo'), ('Muthoni Karuki'), ('Kibet Rotich'), ('Atieno Achieng'), ('Maina Njoroge'), ('Wekesa Barasa'), ('Nafula Wanjala'), ('Kamau Ndungu'), ('Achieng Ouko'), ('Mutuku Kilonzo'), ('Wambui Githinji'), ('Odhiambo Onyango'), ('Nduta Kimani'), ('Korir Ruto'), ('Auma Otieno'), ('Chepkorir Langat') ) AS v(full_name) ) np ON np.rn = ((t.rn - 1) % np.total) + 1 WHERE ai.customer_id = t.customer_id;
SELECT customer_id, full_name FROM customers_ai ORDER BY customer_id LIMIT 10;
The VALUES list has no explicit numbering, so Postgres does not guarantee the order in which the names are cycled. Give each name an explicit number if you need a reproducible result.
Postgres to Postgres: generation, transfer and synchronization
Between two Postgres databases (my_project_db as the source and my_target_db as the target), Navicat copied and synced my data correctly and showed the exact SQL before running anything.
Data Generation: produced about 1,000 rows, and Navicat reported 0.00186 seconds for it. Data Transfer: I copied customers, customers_ai, transactions and the two sequences behind the SERIAL columns into the second database. Row counts matched, with 200 rows in transactions on both sides. Data Synchronization: after I changed one county in the source, the comparison found exactly one row to update and nothing to insert or delete. It listed customers (8 rows) and transactions (200 rows) as identical. After running it, the changed value appeared in the target. Structure Synchronization: it flagged the risk_score column I had added and showed the ALTER TABLE script before applying it.
Two cautions from the previews:
- The data sync script updated every column of the changed row, not only the county. On a busy table, that could overwrite someone else's recent edit to another column.
- Sync copies whatever the source holds, including masked values. Mask before you sync to a test environment, not after.
A Recompare button and a per-table tick box made it easy to check and limit what would run.
Queries I used on these tests
The first two statements built the _ai copy of transactions. The next two checked the transfer on both sides. The ALTER TABLE is the schema change I used for Structure Synchronization, and the last query reads the county of the row I changed for Data Synchronization.
CREATE TABLE transactions_ai ( LIKE transactions INCLUDING ALL ) ;
INSERT INTO transactions_ai SELECT * FROM transactions ;
SELECT count (*) FROM transactions;
SELECT round(sum(amount_kes), 2) FROM transactions;
ALTER TABLE customers_ai ADD COLUMN risk_score INT;
SELECT county FROM customers_ai WHERE customer_id = 1002;
Postgres to SQLite
The data and keys survived a move to a different engine, but auto-increment and column defaults did not, so check them by hand after any cross-engine transfer. I transferred the tables from Postgres into a new SQLite file with Data Transfer, leaving the sequences unticked because SQLite has none.
| Check | Result |
|---|---|
| Row counts | Matched |
| Sum of amount_kes over 200 rows | 5121876.44 in both engines |
| Primary key on customer_id | Kept |
| NOT NULL on customer_id and full_name | Kept |
| Foreign key from transactions to customers | Kept |
| Auto-numbering on customer_id | Lost |
| Default on created_at | Lost |
| Types | int4 became integer(32) , varchar(n) became text(n) , timestamps became text(6) |
The lost auto-numbering showed up as soon as I inserted a row without an id: SQLite rejected it with a NOT NULL constraint failure. I suspect the cause is that the column arrived as integer(32) and not as a plain INTEGER PRIMARY KEY, which is the form SQLite fills in itself, but I did not confirm that.
Queries I used on the SQLite copy
SELECT round ( sum ( amount_kes ) , 2 ) FROM transactions ;
SELECT count (*) FROM transactions;
PRAGMA table_info(customers);
PRAGMA foreign_key_list(transactions);
SELECT sql FROM sqlite_master WHERE name = 'customers';
SELECT * FROM customers LIMIT 3;
SELECT customer_id FROM customers ORDER BY customer_id DESC LIMIT 1;
When I asked the AI to correct the failed insert, it declined to guess. Its reply was a comment saying it could not safely correct the statement, because customers.customer_id is NOT NULL and the insert supplied no value or default, and it asked for the table schema or a valid customer_id.
Postgres to Oracle Autonomous Database
Oracle was the hardest test: getting connected took real work on Linux, and the transfer failed on timestamps until I changed the source data.
Connecting from Linux
An Autonomous Database needs a downloaded wallet and an Oracle client library, so the setup is more than a hostname and password. These are the commands that got me connected, in the order that worked. Replace <your_db> with your database name, and run steps 4 to 6 in one terminal so the IC variable stays set.
Step 1. Check whether an Instant Client is already on the machine.
find ~ /opt /usr/lib -name 'libclntsh.so*' 2 > /dev/null
Step 2. Unzip the Instant Client. I used the 23.26 Basic zip (about 137 MB) from Oracle's download page.
mkdir -p ~/oracle
unzip -q ~/Downloads/instantclient-basic-linux.x64-23.26.4.0.0.zip -d ~/oracle
ls -l ~/oracle/instantclient_23_26/libclntsh.so*
Step 3. Unzip the wallet you downloaded from the Oracle Cloud console.
mkdir -p ~/network/admin
unzip ~/Downloads/Wallet_<your_db>.zip -d ~/network/admin
Step 4. Point the wallet's sqlnet.ora at a real path, and copy the wallet into the Instant Client.
IC=~/oracle/instantclient_23_26
sed -i "s|DIRECTORY=\"[^\"]*\"|DIRECTORY=\"$HOME/network/admin\"|" ~/network/admin/sqlnet.ora
mkdir -p $IC/network/admin
cp ~/network/admin/* $IC/network/admin/
sed -i "s|DIRECTORY=\"[^\"]*\"|DIRECTORY=\"$IC/network/admin\"|" $IC/network/admin/sqlnet.ora
cat -A ~/network/admin/sqlnet.ora
cat -A $IC/network/admin/sqlnet.ora
Each file should hold two lines: the WALLET_LOCATION line with your path, and SSL_SERVER_DN_MATCH=yes. One of my copies lost the second line, so if yours is missing it, add it back:
printf '\nSSL_SERVER_DN_MATCH=yes\n' >> ~/network/admin/sqlnet.ora
Step 5. Ask Linux which libraries the client cannot find.
ldd $IC/libclntsh.so.23.1 | grep "not found"
Mine listed libnnz.so, libaio.so.1 and libclntshcore.so.23.1. The first and last live in the Instant Client folder itself, and libaio needs a link because Ubuntu names it libaio.so.1t64.
Step 6. Register the folder, link libaio, and check again.
echo $IC | sudo tee /etc/ld.so.conf.d/oracle-instantclient.conf
sudo ldconfig
sudo ln -s /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1
ldd $IC/libclntsh.so.23.1 | grep "not found"
The last command should print nothing. A link placed inside the Instant Client folder did not work for libaio, because ldconfig ignores it there.
Step 7. List the service names in the wallet.
grep -oE '^[A-Za-z0-9_]+ *=' $IC/network/admin/tnsnames.ora
You should see five aliases ending in _high, _medium, _low, _tp and _tpurgent.
Step 8. Set up Navicat. In Tools > Options > Environment, set the OCI library to /home/<you>/oracle/instantclient_23_26/libclntsh.so. Leave "Use bundled OCI library" unticked, then quit Navicat completely and reopen it. Create an Oracle connection with Connection Type TNS, type the Net Service Name by hand (I used the _low alias), enter your user, and type the password yourself. Click Test Connection.
Two things confused me. The Net Service Name dropdown stayed empty even though tnsnames.ora was valid, but typing the alias by hand worked, and Navicat then filled in the full Service Name itself. And with no alias set, the error was ORA-12547 (lost contact), which says nothing about a missing service name. A later ORA-01017 simply meant the password I typed did not match the one stored in the database, so check that before blaming the wallet.
Also worth knowing for a trial: Always Free databases stop after 7 days of inactivity and need a manual Start in the Oracle console. Oracle's documentation also says a database that is stopped and stays inactive for 90 days, cumulative, may be reclaimed and permanently deleted, so start it again before then if you want to keep it.
Creating the test user
I ran this in Database Actions as ADMIN. Choose your own password, 12 to 30 characters with an upper-case letter, a lower-case letter and a digit, and type it without the angle brackets. My first attempt kept the brackets as part of the password, and every login then failed with ORA-01017.
CREATE USER navicat_test IDENTIFIED BY "<your password>" ;
GRANT DWROLE TO navicat_test ;
ALTER USER navicat_test QUOTA UNLIMITED ON DATA ;
To change the password later:
ALTER USER navicat_test IDENTIFIED BY "<new password>" ;
To check the account and the session from ADMIN:
SELECT username , account_status , authentication_type FROM dba_users WHERE username = 'NAVICAT_TEST' ;
SELECT granted_role FROM dba_role_privs WHERE grantee = 'NAVICAT_TEST' ;
SELECT SYS_CONTEXT ( 'USERENV' , 'SESSION_USER' ) FROM dual ;
After I edited a
Comments
No comments yet. Start the discussion.