Documentation
The complete reference for BeQuery — every feature, every configuration, every edge case. Designed to get you from nothing to a running clone in 5 minutes, and stay useful when you need to go deep.
Getting started
BeQuery clones your production MySQL database into a safe Postgres copy. You run analytics queries against the clone — production never sees the load.
Add a database
Go to Databases → Add Database. Pick a connection method (most common is SSH Tunnel for hosted MySQL behind a firewall). Fill in credentials. Click Test Connection.
Pick tables
In the Tables tab, tick the tables you want to clone. Or enable Include all tables — every sync auto-picks up new tables from the source.
Set a schedule
In Schedule, choose hourly / daily / weekly + the time of day. BeQuery runs the sync on that cadence automatically.
Run your first sync
Click Sync Now. The first sync is a full clone (it creates the tables). Every sync after is incremental and only moves changed rows.
Query your data
Open Query Editor or Tables. Everything in your MySQL DB is now queryable via Postgres SQL, with zero risk to production.
Connection methods
Every way your database might be exposed is supported. Pick the one that matches your hosting:
Direct Connection
Standard MySQL over the internet. Works if the DB accepts public connections and you can whitelist our IP.
SSH Tunnel
Most common. We connect to your SSH server with a key, then forward to MySQL on localhost. No need to expose MySQL to the internet. Upload the SSH private key with the Upload button — any format (id_rsa, .pem, .key, .ppk).
SSL / TLS
TLS-encrypted MySQL connection with CA / client cert / client key. All certificates are encrypted at rest.
SOCKS5 Proxy
Route through a SOCKS5 proxy with a static IP. Your DB whitelists the proxy, not us.
VPN / WireGuard
If MySQL is on a VPN network, use direct host/port as seen from inside the VPN. Optional: paste WireGuard config for reference.
Credentials are encrypted with AES-256-GCM before hitting Postgres. Test Connection pre-loads your saved password so you don't retype it.
Sync modes
Incremental (default)
Every table with a primary key is synced incrementally after the first clone. The sync COPYs source rows into a staging table, then MERGEs into the target:
- New rows in source →
INSERT - Changed rows →
UPDATE(only columns that actually differ) - Rows removed from source →
DELETEfrom target
The target table stays online throughout — queries running in the Query Editor during a sync will succeed. Indexes on the target are preserved.
Full
First sync of a table is always full (drops + recreates). You can force a full resync any time from the Full Resync button — use this when you changed source schema (added / removed columns) or the clone looks corrupted.
Include all tables
Toggle in the Tables tab of a config. When on: each sync queries MySQL for the live table list, so new tables in the source are automatically cloned, and tables removed from the source are automatically dropped from the clone.
Auto-drop on deselect
When you untick a table and save the config, the clone drops that table at the next sync. The Tables browser always reflects the current selection — no orphans.
Fast-path optimization
For tables with a timestamp column that tracks modification time (updated_at, date_upd, modified, last_modified, modified_at, date_modification), the incremental sync fetches only rows changed since the last sync.
On a 100M-row table where 50K rows change per day, the fast-path fetches 50K rows (not 100M). Nightly resyncs go from hours to minutes.
The Tables tab shows a lightning bolt ⚡ badge on every table where fast-path is active. The header card "Fast-path ready: X / Y" tells you how many tables are optimized.
Tables without a detected timestamp column fall back to the standard incremental (scan all source rows for MERGE). Still faster than a full re-clone because only diffs are written to Postgres.
Data preservation
BeQuery clones values exactly — no silent data transformation. That means a few unusual type mappings vs a naive ORM:
MySQL DATE, DATETIME, TIMESTAMP, TIME→PG TEXTPostgres's strict date types reject values MySQL accepts (0000-00-00, year 10000, TIME > 24h). Storing as TEXT preserves every literal. Cast in queries when you need date math: col::timestamp.
MySQL INT UNSIGNED→PG BIGINTMySQL unsigned int holds up to 4.29B; Postgres INTEGER max is 2.1B. Widening preserves the full range.
MySQL BIGINT UNSIGNED→PG NUMERIC(20,0)Postgres BIGINT max 9.2×10^18, MySQL bigint unsigned max 1.8×10^19. NUMERIC holds arbitrary precision.
MySQL TINYINT (any display width)→PG SMALLINTTINYINT(1) is conventionally boolean but technically holds 2, 7, -1 etc. Mapping to BOOLEAN breaks; SMALLINT preserves any value.
MySQL BIT(n)→PG BIGINTMySQL BIT is integer-as-bitstring (up to 64 bits fits a BIGINT). Postgres BIT is a different semantic.
MySQL JSON→PG JSONBBinary-packed JSON, indexed and queryable.
MySQL GEOMETRY, POINT, POLYGON, ...→PG BYTEARaw WKB bytes preserved. Query with PostGIS if installed.
Strings with NUL bytes (\0) or lone UTF-16 surrogate halves get cleaned because Postgres TEXT literally cannot store them. Everything else is byte-exact.
Tables browser
Tables is your data explorer. Three view modes:
List
Master-detail. Sidebar on the left with all tables, filterable and sortable (name, rows, size, last sync). Click a table → see the first 50 rows on the right, paginated. Refresh button pulls the latest data.
Grid
Card grid — one card per table showing the headline number of rows, size, column count, PK indicator, fast-path badge. Click a card → opens in List mode with the data pre-loaded.
Table
Single dense DataTable with every table as a row. Full metadata: rows, size, columns, primary keys, fast-path column (if any), last sync time. Click a column header to sort. Click a row to browse its data.
Stats header
Above every view: total tables, total rows across the DB, storage on disk, and the fast-path readiness count.
Query Editor
SQL editor with syntax highlighting against the cloned Postgres schema. Run SELECT / WITH / EXPLAIN against your cloned data without any risk to the source MySQL.
- Read-only — INSERT, UPDATE, DELETE, DROP and other DML/DDL statements are refused.
- Result limit — queries are capped at 10,000 rows. Use LIMIT for exact slices.
- Statement timeout — 30 seconds per query, to protect the cluster.
- History — every query you run is saved in Query History (success or error, duration, row count).
- Saved queries — promote a query to Saved Queries to reuse across teammates.
The Cloud plan meters each query by rows read from the first row. The Settings → Cloud Usage card shows live cost and running totals for the current billing period.
MySQL → PostgreSQL
Your source databases are MySQL (PrestaShop, WooCommerce, Magento, custom PHP apps), but the clones live in PostgreSQL. The query editor accepts PostgreSQL syntax. Two tools bridge the gap:
Ask AI — natural language to SQL (Sparkles button)
Describe what you want in plain English or Italian — the AI analyzes your database schema, picks the right tables, and writes the PostgreSQL query for you. Two-step retrieval: first it identifies relevant tables from your 500+ schema, then it generates the query using only those tables' columns. Cost: €0.05 per query on Cloud, included on flat plans.
AI translator (Wand icon in toolbar)
Paste any MySQL query into the translator dialog and get a PostgreSQL equivalent back in ~2 seconds. Uses GPT-4o-mini under the hood. Cost: €0.01 per translation on the Cloud plan, included on flat plans.
Common conversions cheat sheet
The 20 differences you'll hit most often. Paste the MySQL side on the left, write the Postgres side on the right:
| MySQL | PostgreSQL |
|---|---|
| LIMIT 10, 5 | LIMIT 5 OFFSET 10 |
| `column_name` | "column_name" |
| DATE_FORMAT(d, '%Y-%m-%d') | TO_CHAR(d, 'YYYY-MM-DD') |
| STR_TO_DATE(s, '%Y-%m-%d') | TO_DATE(s, 'YYYY-MM-DD') |
| GROUP_CONCAT(x) | STRING_AGG(x::text, ',') |
| IFNULL(a, b) | COALESCE(a, b) |
| IF(cond, a, b) | CASE WHEN cond THEN a ELSE b END |
| NOW() | NOW() |
| CURDATE() | CURRENT_DATE |
| UNIX_TIMESTAMP(d) | EXTRACT(EPOCH FROM d)::bigint |
| CONCAT(a, b) | a || b (or CONCAT still works) |
| LIKE 'abc' (case-insensitive) | ILIKE 'abc' |
| LENGTH(s) | CHAR_LENGTH(s) |
| SUBSTRING_INDEX(s, ',', 1) | SPLIT_PART(s, ',', 1) |
| AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY |
| UTF8_UNICODE_CI | (use COLLATE "und-u-ks-level2") |
| LAST_INSERT_ID() | RETURNING id (in INSERT) |
| DATEDIFF(a, b) | (a::date - b::date) |
| DATE_ADD(d, INTERVAL 1 DAY) | d + INTERVAL '1 day' |
| REGEXP 'pattern' | ~ 'pattern' (or ~* for case-insensitive) |
Postgres is stricter about quoting: column names are case-sensitive only if you wrapped them in double-quotes at creation time. Unquoted identifiers fold to lowercase. Our sync uses lowercase column names throughout, so you can write id_product without quotes.
Real-time MySQL pattern detection (linting)
While you type in the query editor, a non-blocking amber bar appears when MySQL-only patterns are detected (backticks, DATE_FORMAT, IFNULL, LIMIT x,y, etc.). One click and the Translate dialog opens with your current SQL pre-filled, ready to convert.
Error hints on failed queries
When a query fails with a typical MySQL-vs-Postgres mismatch (e.g. function date_format does not exist), we show the raw Postgres error PLUS a helpful hint suggesting the Postgres equivalent. So you learn while you query.
BeQuery Templates (Saved Queries page)
Pre-built queries for the most common CMS platforms — PrestaShop, WooCommerce, Magento, Shopify — appear in a separate "BeQuery Templates" section above your own saved queries, tagged with the CMS they target. They can be run or copied but not deleted (managed centrally so we can improve them over time). User-saved queries remain fully editable and deletable.
External connections
Connect Grafana, Metabase, Tableau, DBeaver — or any tool that speaks Postgres — directly to your BeQuery clone via the wire protocol. Skips our API entirely, so there's zero load on your production MySQL. Available on the Cloud and Enterprise plans.
Create a credential
- Open Settings → External Connections.
- Click New credential, give it a name and a tool badge (Grafana, Metabase, Tableau, DBeaver, or Custom).
- The modal shows the username (a
bequery_ext_*Postgres role), the password and a ready-to-paste connection string. Copy them now — the password is shown only once. Lost it? Rotate.
Hard limit: 5 credentials per team. Delete an unused one if you need to create a new one.
Grafana setup (example)
- Grafana → Connections → Data sources → PostgreSQL.
- Host: copy from the Connection details panel (
db.<project_ref>.supabase.co, port5432). - Database:
postgres. - User: the
bequery_ext_*role name shown in the credential card. Password: the one shown at creation. - TLS/SSL mode:
require. (Supabase rejects non-TLS.) - Expand Additional settings → under Custom JDBC / connection parameters, set
options=-csearch_path=team_<your_team_id>. This way you can query tables unqualified (e.g.SELECT * FROM ps_product). - Click Save & test → green check. Start building dashboards.
Metabase setup
Same fields as Grafana: Postgres driver, host/port/database from the Connection details panel, user = role name, password = shown-once secret. Metabase supports the Additional JDBC options field — paste options=-csearch_path=team_<your_team_id> there.
Custom scripts (psql / Python / Node)
Use the connection string straight from the create modal:
psql "postgresql://bequery_ext_xxx:<password>@db.<project_ref>.supabase.co:5432/postgres?sslmode=require&options=-csearch_path%3Dteam_xxx"Limits & safety
- Read-only. External roles have
SELECTonly — no INSERT, UPDATE, DELETE, DROP, ALTER. New tables created by future syncs are auto-granted. - 30s statement timeout per query, and a connection limit of 5 per credential — to protect the shared cluster.
- Rotating a password kicks off the new one immediately and breaks any tool still using the old one.
- Revoking drops the role; all open connections close on the next heartbeat.
Billing
Rows read via external tools are billed like UI queries: €0.60 per 1M rows on the Cloud plan, included on Enterprise. A background job samples pg_stat_statements every 10 minutes and attributes usage to the right credential — visible in Settings → Cloud Usage as the External tile (rows + call count).
Billing & plans
Flat plans
Starter (free) → Pro (€49/mo) → Business (€149/mo) → Scale (€349/mo) → Enterprise (€699/mo). Each tier raises DBs, table limits, row caps, members, and sync frequency. Pay monthly or annually (2 months free on yearly).
Cloud (pay-as-you-go)
Base €29/mo covers platform access + storage. Every query is metered from the first row: €0.60 per million rows read + €0.025 per GB of storage per month. Hard spending cap (default €500, configurable) blocks queries before you hit it. Soft alert email at €100 (also configurable).
Billing details
All charges go through Stripe with full VAT / Partita IVA / Codice Fiscale / PEC / SDI collection at checkout. Invoices are issued automatically; change card / cancel / download invoices from the billing portal inside Settings.
Security
- Credentials encrypted (AES-256-GCM) at rest. Never logged or exposed in API responses.
- Multi-tenant schema isolation: every team gets a dedicated Postgres schema. Query Editor runs in a
SET LOCAL search_pathscoped to your schema — you can't reach another team's data. - Row Level Security (RLS) on every metadata table. Postgres enforces that a user sees only their own team's rows, even if the API had a bug.
- Rate limiting on every API endpoint.
- Audit log of admin actions (DB connected, plan upgraded, payment events, Full Resync, etc.) in the
audit_logtable. - SSH tunnel compression + keepalive so the connection doesn't drop during a multi-hour sync.
- Bot / abuse protection on public pages.
Troubleshooting
Sync stuck on "Running"
If heartbeat is stale for >90s, a Postgres-side cron (pg_cron) fires every minute and automatically resumes the orphan from the last saved checkpoint. No action needed. Worst-case recovery time: 2.5 minutes.
Table reports 0 rows but source has data
Check Clone Logs for an error message on that table. Common cause: a data-type mismatch (very rare after the UNSIGNED / TINYINT / zero-date fixes). If you see one, contact support — we can usually patch in the next deploy.
Full Resync vs Sync Now
Sync Now — incremental. Always the right choice. Fast, safe, preserves queryability.
Full Resync — drops and re-clones all tables. Use only when you changed source schema or data looks corrupt. Confirmed by a dialog because it can take hours on big databases.