Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@ Tabularis features end to end.

| Engine | Port | Databases | Theme |
| ------------- | ----- | -------------------------------- | ---------------------------- |
| MySQL 8.4 | 3306 | `tabularis_demo`, `blog_demo` | HR/e-commerce + blog CMS |
| PostgreSQL 16 | 5432 | `tabularis_demo`, `analytics_demo` | HR/e-commerce + web analytics (JSONB) |
| MySQL 8.4 | 3306 | `tabularis_demo`, `blog_demo`, `perf_demo` | HR/e-commerce + blog CMS + wide-table perf |
| PostgreSQL 16 | 5432 | `tabularis_demo`, `analytics_demo`, `perf_demo` | HR/e-commerce + web analytics (JSONB) + wide-table perf |
| SQL Server 2022 | 1433 | `tabularis_demo`, `finance_demo` | HR/e-commerce + accounting |

`tabularis_demo` is the **same logical schema** on all three engines (departments,
employees, products, customers, orders, order_items) — useful for testing the
showcase notebook against any driver. The second database per engine highlights
features specific to that ecosystem.

### `perf_demo` — wide-table scroll stress test

On MySQL and PostgreSQL the `perf_demo` database holds a single
`wide_table` with **50 columns × 50 000 rows** (a mix of int, bigint, decimal,
double, varchar, text, date, datetime, boolean and JSON/JSONB columns). It
reproduces slow-scroll reports with many columns/rows so DataGrid virtualization
and memoization can be profiled — open `perf_demo.wide_table` and scroll both
axes; it should stay fluid. This is a heavy seed, so the **first** container boot
takes a little longer. The SQL is generated by
[`generate-perf-sql.py`](./generate-perf-sql.py) (edit there and re-run to change
the column/row counts).

## Prerequisites

- Docker Desktop or Docker Engine 24+ with the Compose plugin
Expand Down Expand Up @@ -70,11 +82,12 @@ All three servers share the same password for simplicity:

Open Tabularis → **Connections** → **Import** and pick `connections.json`.

This adds a **Tabularis Demo (Docker)** group with two pre-configured
connections:
This adds a **Tabularis Demo (Docker)** group with pre-configured connections:

- **Demo · MySQL** — exposes `tabularis_demo` and `blog_demo`
- **Demo · PostgreSQL** — exposes `tabularis_demo` and `analytics_demo`
- **Demo · MySQL** — exposes `tabularis_demo`, `blog_demo` and `perf_demo`
- **Demo · PostgreSQL** — exposes `tabularis_demo`
- **Demo · PostgreSQL (analytics_demo)** — the JSONB analytics database
- **Demo · PostgreSQL (perf_demo)** — the wide-table scroll stress test

> **SQL Server is not in `connections.json`.** Tabularis core currently ships
> drivers for MySQL, PostgreSQL, and SQLite only; the official plugin registry
Expand All @@ -94,14 +107,19 @@ Once `Demo · MySQL` is connected and selected on `tabularis_demo`, import
demo/
├── docker-compose.yml
├── connections.json # Importable into Tabularis
├── generate-perf-sql.py # Regenerates the 06-perf-wide.sql seeds
├── notebook-showcase.tabularis-notebook
├── init/
│ ├── mysql/
│ │ ├── 01-tabularis-demo.sql
│ │ └── 02-blog-demo.sql
│ │ ├── 02-blog-demo.sql
│ │ ├── ...
│ │ └── 06-perf-wide.sql # 50 cols x 50k rows (perf_demo)
│ ├── postgres/
│ │ ├── 01-tabularis-demo.sql
│ │ └── 02-analytics-demo.sql
│ │ ├── 02-analytics-demo.sql
│ │ ├── ...
│ │ └── 06-perf-wide.sql # 50 cols x 50k rows (perf_demo)
│ └── mssql/
│ ├── run-init.sh # Sidecar entrypoint
│ ├── 01-tabularis-demo.sql # Idempotent
Expand Down
23 changes: 22 additions & 1 deletion demo/connections.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"port": 3306,
"username": "root",
"password": "Tabularis_Demo_2026!",
"database": ["tabularis_demo", "blog_demo"],
"database": ["tabularis_demo", "blog_demo", "perf_demo"],
"ssl_mode": null,
"ssl_ca": null,
"ssl_cert": null,
Expand Down Expand Up @@ -71,6 +71,27 @@
"ssh_connection_id": null,
"save_in_keychain": true
}
},
{
"id": "tabularis-demo-postgres-perf",
"name": "Demo · PostgreSQL (perf_demo)",
"group_id": "tabularis-demo-group",
"sort_order": 3,
"params": {
"driver": "postgres",
"host": "127.0.0.1",
"port": 5432,
"username": "postgres",
"password": "Tabularis_Demo_2026!",
"database": "perf_demo",
"ssl_mode": null,
"ssl_ca": null,
"ssl_cert": null,
"ssl_key": null,
"ssh_enabled": false,
"ssh_connection_id": null,
"save_in_keychain": true
}
}
],
"ssh_connections": []
Expand Down
165 changes: 165 additions & 0 deletions demo/generate-perf-sql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""Generate the perf "wide table" seed for the demo stack (MySQL + Postgres).

Produces a `perf_demo.wide_table` with 50 columns (id + 49 data columns of
mixed types) and 50_000 rows, used to stress-test DataGrid scroll performance.

Run from the repo root: python3 demo/generate-perf-sql.py
Writes: demo/init/mysql/06-perf-wide.sql, demo/init/postgres/06-perf-wide.sql
"""

ROWS = 50000

# Each spec: (name, mysql_type, pg_type, mysql_expr, pg_expr)
# {g} is the row counter (1..ROWS) and is the same variable name in both dialects.
specials = [
("json_data", "JSON", "JSONB",
"JSON_OBJECT('row', {g}, 'label', CONCAT('item-', {g}), 'tags', JSON_ARRAY('alpha','beta', {g} MOD 7))",
"jsonb_build_object('row', {g}, 'label', 'item-' || {g}, 'tags', jsonb_build_array('alpha','beta', {g} % 7))"),
("long_text", "TEXT", "TEXT",
"REPEAT(CONCAT('Lorem ipsum dolor row ', {g}, '. '), 6)",
"repeat('Lorem ipsum dolor row ' || {g} || '. ', 6)"),
("created_at", "DATETIME", "TIMESTAMP",
"DATE_ADD('2000-01-01 00:00:00', INTERVAL {g} SECOND)",
"TIMESTAMP '2000-01-01 00:00:00' + ({g} * INTERVAL '1 second')"),
("updated_at", "DATETIME", "TIMESTAMP",
"DATE_ADD('2010-01-01 00:00:00', INTERVAL ({g}*2) SECOND)",
"TIMESTAMP '2010-01-01 00:00:00' + (({g}*2) * INTERVAL '1 second')"),
("birth_date", "DATE", "DATE",
"DATE_ADD('1970-01-01', INTERVAL ({g} MOD 18000) DAY)",
"DATE '1970-01-01' + ({g} % 18000)"),
("is_active", "TINYINT(1)", "BOOLEAN",
"({g} MOD 2)",
"({g} % 2 = 0)"),
("is_verified", "TINYINT(1)", "BOOLEAN",
"({g} MOD 3 = 0)",
"({g} % 3 = 0)"),
("amount", "DECIMAL(12,2)", "DECIMAL(12,2)",
"ROUND(({g} MOD 100000) * 1.25, 2)",
"ROUND((({g} % 100000) * 1.25)::numeric, 2)"),
("balance", "DECIMAL(14,4)", "DECIMAL(14,4)",
"ROUND(({g} MOD 50000) * 0.337, 4)",
"ROUND((({g} % 50000) * 0.337)::numeric, 4)"),
("score", "DOUBLE", "DOUBLE PRECISION",
"({g} * 0.5)",
"({g} * 0.5)"),
("big_value", "BIGINT", "BIGINT",
"({g} * 1000003)",
"({g}::bigint * 1000003)"),
]

# Fill the remaining columns up to 49 data columns with attr_NN cycling types.
DATA_COLS = 49
fill_count = DATA_COLS - len(specials)
cycle = ["varchar", "int", "decimal"]

fills = []
for i in range(fill_count):
idx = i + 1
name = f"attr_{idx:02d}"
kind = cycle[i % len(cycle)]
if kind == "varchar":
fills.append((name, "VARCHAR(80)", "VARCHAR(80)",
f"CONCAT('attr{idx}-', {{g}})",
f"'attr{idx}-' || {{g}}"))
elif kind == "int":
fills.append((name, "INT", "INTEGER",
f"(({{g}} * {idx}) MOD 100000)",
f"(({{g}} * {idx}) % 100000)"))
else: # decimal
fills.append((name, "DECIMAL(12,2)", "DECIMAL(12,2)",
f"ROUND(({{g}} MOD 9999) * {idx} * 0.1, 2)",
f"ROUND((({{g}} % 9999) * {idx} * 0.1)::numeric, 2)"))

cols = specials + fills
assert len(cols) == DATA_COLS, len(cols)

HEADER = f"""-- =============================================================
-- Tabularis Demo — Perf / wide-table stress test ({{engine}})
-- Database: perf_demo
-- Table: wide_table -> 50 columns x {ROWS:,} rows
-- Purpose: reproduce slow-scroll reports with many columns/rows so
-- DataGrid virtualization/memoization can be profiled.
-- NOTE: heavy dataset — first container boot takes a little longer.
-- Generated by demo/generate-perf-sql.py (edit there, then re-run).
-- ============================================================="""

# ---- MySQL ----------------------------------------------------------------
my_coldefs = [" id BIGINT NOT NULL PRIMARY KEY"]
for name, mty, _pty, _me, _pe in cols:
my_coldefs.append(f" {name} {mty}")
my_collist = ",\n".join(my_coldefs)

my_insert_cols = ["id"] + [c[0] for c in cols]
my_insert_exprs = ["g.g"] + [c[3].replace("{g}", "g.g") for c in cols]

digit = "(SELECT 0 d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9)"

mysql = f"""{HEADER.replace("{engine}", "MySQL 8")}

SET NAMES utf8mb4;

CREATE DATABASE IF NOT EXISTS perf_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

USE perf_demo;

DROP TABLE IF EXISTS wide_table;

CREATE TABLE wide_table (
{my_collist}
) ENGINE=InnoDB;

-- 50k rows via 5 cross-joined digit tables (0..9 each -> 0..99999), filtered.
INSERT INTO wide_table (
{", ".join(my_insert_cols)}
)
SELECT
{",\n ".join(my_insert_exprs)}
FROM (
SELECT (d5.d*10000 + d4.d*1000 + d3.d*100 + d2.d*10 + d1.d) + 1 AS g
FROM {digit} d1
CROSS JOIN {digit} d2
CROSS JOIN {digit} d3
CROSS JOIN {digit} d4
CROSS JOIN {digit} d5
) AS g
WHERE g.g <= {ROWS};
"""

# ---- Postgres -------------------------------------------------------------
pg_coldefs = [" id BIGINT NOT NULL PRIMARY KEY"]
for name, _mty, pty, _me, _pe in cols:
pg_coldefs.append(f" {name} {pty}")
pg_collist = ",\n".join(pg_coldefs)

pg_insert_cols = ["id"] + [c[0] for c in cols]
pg_insert_exprs = ["g"] + [c[4].replace("{g}", "g") for c in cols]

postgres = f"""{HEADER.replace("{engine}", "PostgreSQL 16")}

CREATE DATABASE perf_demo;

\\connect perf_demo

DROP TABLE IF EXISTS wide_table;

CREATE TABLE wide_table (
{pg_collist}
);

INSERT INTO wide_table (
{", ".join(pg_insert_cols)}
)
SELECT
{",\n ".join(pg_insert_exprs)}
FROM generate_series(1, {ROWS}) AS g;
"""

with open("demo/init/mysql/06-perf-wide.sql", "w") as f:
f.write(mysql)
with open("demo/init/postgres/06-perf-wide.sql", "w") as f:
f.write(postgres)

print("wrote", DATA_COLS + 1, "columns,", ROWS, "rows")
Loading