Database Architecture
Database Architecture
Section titled “Database Architecture”Tabula Lens supports PostgreSQL, MySQL, SQLite, and Microsoft SQL Server through a unified query layer built on Knex.js. The same TabulaLens API works across all supported engines: the library auto-detects the database type from the connection URL, selects the appropriate driver, and delegates engine-specific SQL syntax to a dialect strategy.
Supported Databases
Section titled “Supported Databases”| Engine | Identifier | Knex Client | Driver Package |
|---|---|---|---|
| PostgreSQL | pg |
pg |
pg |
| MySQL / MariaDB | mysql |
mysql2 |
mysql2 |
| SQLite | sqlite |
better-sqlite3 |
better-sqlite3 |
| Microsoft SQL Server | mssql |
tedious |
tedious |
Each engine brings different strengths:
- PostgreSQL: Advanced querying, rich data types (JSON/JSONB, arrays), strong consistency, and extensibility through extensions.
- MySQL / MariaDB: Widely deployed, strong ecosystem, and good performance for read-heavy workloads.
- SQLite: Serverless, file-based, and ideal for local development, embedded applications, or small deployments.
- SQL Server: Enterprise features, strong tooling, and common in Microsoft-centric environments.
The architecture is extensible: adding another relational database only requires a new DialectStrategy implementation and a corresponding Knex client mapping.
Connection Architecture
Section titled “Connection Architecture”Connection String Format
Section titled “Connection String Format”Tabula Lens accepts standard connection strings for each supported engine. The scheme is used by detectDatabaseType to determine which driver to load.
PostgreSQL
postgresql://[user[:password]@][host][:port][/database][?parameters]DATABASE_URL=postgresql://alice:secret@localhost:5432/mydbMySQL
mysql://[user[:password]@][host][:port][/database][?parameters]DATABASE_URL=mysql://alice:secret@localhost:3306/mydbSQLite
# File path or in-memory databaseDATABASE_URL=./data.dbDATABASE_URL=:memory:DATABASE_URL=sqlite:./data.dbMicrosoft SQL Server
mssql://[user[:password]@][host][:port][/database][?parameters]DATABASE_URL=mssql://alice:secret@localhost:1433/mydbYou can also pass an explicit type in the config object to bypass auto-detection:
const tabulaLens = new TabulaLens({ url: 'mysql://localhost/mydb', type: 'mysql',});Dynamic Client Selection
Section titled “Dynamic Client Selection”The TabulaLens constructor maps the detected (or explicit) DatabaseType to the Knex client that should be used:
const clientMap: Record<DatabaseType, string> = { pg: 'pg', mysql: 'mysql2', sqlite: 'better-sqlite3', mssql: 'tedious',};This means pg connections use the pg driver, mysql connections use mysql2, sqlite connections use better-sqlite3, and mssql connections use tedious. All query construction still goes through Knex, so the rest of the codebase remains database-agnostic.
Connection Pooling
Section titled “Connection Pooling”TabulaLens relies on Knex and the underlying driver for connection pooling. Pool settings are controlled through driver-specific connection string parameters or environment variables, not through TabulaLens constructor options:
# PostgreSQL / MySQL / MSSQL pool hints via connection stringDATABASE_URL="postgresql://user:password@localhost:5432/mydb?pool_min=2&pool_max=10"const tabulaLens = new TabulaLens({ url: process.env.DATABASE_URL, // type is auto-detected from the connection string});Benefits:
- Performance: Reuses connections instead of creating new ones
- Scalability: Handles concurrent requests efficiently
- Resource Management: Prevents connection exhaustion
- Automatic Cleanup: Removes idle connections automatically
Connection Lifecycle
Section titled “Connection Lifecycle”┌─────────────┐│ Application │└──────┬──────┘ │ 1. Request Connection ▼┌─────────────┐│ Knex / ││ Driver ││ Pool │└──────┬──────┘ │ 2. Get Available Connection ▼┌─────────────┐│ Active ││ Connection │└──────┬──────┘ │ 3. Execute Query ▼┌─────────────┐│ Database │└──────┬──────┘ │ 4. Return Results ▼┌─────────────┐│ Active ││ Connection │└──────┬──────┘ │ 5. Return to Pool ▼┌─────────────┐│ Knex / ││ Driver ││ Pool │└─────────────┘The lifecycle is the same for every supported database: Knex acquires a connection from the configured driver pool, runs the parameterized query, returns the results, and releases the connection.
Query Architecture
Section titled “Query Architecture”Query Building Process
Section titled “Query Building Process”TabulaLens builds SQL queries safely and efficiently:
┌─────────────┐│ User ││ Request │└──────┬──────┘ │ Query Parameters ▼┌─────────────┐│ Input ││ Validation │└──────┬──────┘ │ Validated Parameters ▼┌─────────────┐│ Query ││ Builder │└──────┬──────┘ │ SQL Query ▼┌─────────────┐│ Parameter ││ Binding │└──────┬──────┘ │ Parameterized Query ▼┌─────────────┐│ Database ││ Driver │└──────┬──────┘ │ Execute Query ▼┌─────────────┐│ Database │└──────┬──────┘ │ Results ▼┌─────────────┐│ Result ││ Formatting │└──────┬──────┘ │ Formatted Response ▼┌─────────────┐│ User ││ Response │└─────────────┘Query Parameters
Section titled “Query Parameters”TabulaLens accepts standardized query parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
table |
string | Table name to query | users |
page |
number | Page number (1-based) | 1 |
limit |
number | Items per page (default: 10) | 10 |
filter |
string | Search filter string | John |
filterColumns |
string[] | Columns to filter on | ['name', 'email'] |
sort |
string | Sort string (column:direction) |
name:asc |
columns |
string[] | Columns to return | ['id', 'name'] |
Query Examples
Section titled “Query Examples”Basic Query:
SELECT * FROM users LIMIT 10 OFFSET 0;Filtered Query:
-- PostgreSQLSELECT * FROM usersWHERE name ILIKE '%John%'ORDER BY name ASCLIMIT 10 OFFSET 0;
-- MySQL, SQLite, and SQL Server (LIKE is case-insensitive by default for most collations)SELECT * FROM usersWHERE name LIKE '%John%'ORDER BY name ASCLIMIT 10 OFFSET 0;Paginated Query:
SELECT * FROM usersORDER BY created_at DESCLIMIT 20 OFFSET 40;Dialect-Specific Query Building
Section titled “Dialect-Specific Query Building”Although most queries are built through Knex’s shared API, some operations need engine-specific syntax:
- Case-insensitive search: PostgreSQL uses
ILIKE, while MySQL, SQLite, and SQL Server useLIKE(case-insensitive in typical configurations). - Metadata queries: PostgreSQL, MySQL, and SQL Server read from
information_schema, while SQLite usesPRAGMA table_info(...)andsqlite_master. - Type names: Each engine returns different type names for the same conceptual type (e.g.,
character varying,varchar,TEXT,nvarchar). The dialect strategy normalizes these for filtering.
These differences are encapsulated behind the DialectStrategy interface so that the rest of the query builder stays generic.
SQL Injection Prevention
Section titled “SQL Injection Prevention”TabulaLens prevents SQL injection through:
- Parameterized Queries: All user input is parameterized
- Input Validation: Strict validation of all parameters
- Column Whitelisting: Only allowed column names are accepted
- Type Checking: Ensures correct data types
// Safe: Parameterized queryconst query = 'SELECT * FROM users WHERE name = $1';const params = ['Alice'];
// Never happens: Direct string interpolation// const query = `SELECT * FROM users WHERE name = '${userInput}'`;Dialect Strategy Pattern
Section titled “Dialect Strategy Pattern”Engine-specific behavior is isolated behind the DialectStrategy interface. This keeps TabulaLens generic while still supporting the metadata queries and operators that differ between databases.
export interface DialectStrategy { getTables(db: Knex): Promise<string[]>; getColumns(db: Knex, table: string): Promise<ColumnInfo[]>; getFilterableTypes(): string[]; getLikeOperator(): 'LIKE' | 'ILIKE';}The createDialect factory instantiates the correct implementation based on the detected DatabaseType:
import { createDialect } from './dialects';
const dialect = createDialect('mysql');const tables = await dialect.getTables(knexInstance);const columns = await dialect.getColumns(knexInstance, 'users');const likeOperator = dialect.getLikeOperator(); // 'LIKE' for MySQLCurrent implementations:
PostgresDialect—information_schemametadata andILIKEMySQLDialect—information_schemawithDATABASE()andLIKESQLiteDialect—sqlite_master/PRAGMA table_infoandLIKEMSSQLDialect—information_schemametadata andLIKE
Adding a new database only requires implementing this interface and registering it in createDialect.
Database-Specific Behaviors
Section titled “Database-Specific Behaviors”The table below summarizes how each engine handles the operations that vary across databases.
| Behavior | PostgreSQL | MySQL | SQLite | SQL Server |
|---|---|---|---|---|
| Case-insensitive LIKE | ILIKE |
LIKE |
LIKE |
LIKE |
| List tables | information_schema.tables (public schema, BASE TABLE) |
information_schema.tables (DATABASE(), BASE TABLE) |
sqlite_master (type = 'table', excluding sqlite_*) |
information_schema.tables (BASE TABLE) |
| List columns | information_schema.columns (public schema) |
information_schema.columns (DATABASE()) |
PRAGMA table_info(?) |
information_schema.columns |
| Filterable text types | character varying, varchar, text, char, character, uuid |
varchar, text, tinytext, mediumtext, longtext, char |
TEXT, text, VARCHAR, varchar, CHAR, char, CLOB, clob |
varchar, nvarchar, text, ntext, char, nchar |
LIKE Operator
Section titled “LIKE Operator”- PostgreSQL returns
ILIKEso searches are case-insensitive. - MySQL, SQLite, and SQL Server return
LIKE. For the collations used by most MySQL and SQL Server installations, and for ASCII characters in SQLite,LIKEis already case-insensitive.
Table Listing
Section titled “Table Listing”- PostgreSQL and MySQL filter
information_schema.tablesto the current/default database schema. - SQLite reads from
sqlite_masterbecause it has noinformation_schema. - SQL Server reads from
information_schema.tableswithout a schema filter, relying on the connection’s default database.
Column Listing
Section titled “Column Listing”- PostgreSQL, MySQL, and SQL Server query
information_schema.columns. - SQLite uses
PRAGMA table_info(?), which returns a different shape that the dialect normalizes to{ name, type }.
Filterable Types
Section titled “Filterable Types”Only text-like columns participate in full-text filtering. The exact type names match what each engine returns from its metadata query. Comparisons are case-insensitive, so variations like TEXT and text are treated the same.
Auto-Detection
Section titled “Auto-Detection”detectDatabaseType(url) inspects the connection string scheme or file extension and returns one of the supported identifiers:
detectDatabaseType('postgresql://localhost/mydb'); // 'pg'detectDatabaseType('mysql://localhost/mydb'); // 'mysql'detectDatabaseType('./database.db'); // 'sqlite'detectDatabaseType('mssql://localhost/mydb'); // 'mssql'Recognized URL patterns:
| Engine | Detected Schemes / Patterns |
|---|---|
| PostgreSQL | postgresql://, postgres://, pgsql:// |
| MySQL / MariaDB | mysql://, mariadb://, mysql2://, mysqlx:// |
| SQLite | sqlite://, sqlite3://, file:, :memory:, paths ending in .db, .sqlite, .sqlite3, .db3 |
| SQL Server | mssql://, sqlserver://, mssql+tcp://, mssql+udp:// |
If the type cannot be determined, TabulaLensError (AUTO_DETECTION_FAILED) is thrown. You can avoid this by supplying type explicitly in the config object.
Database Driver Dependency Management
Section titled “Database Driver Dependency Management”Tabula Lens declares all database drivers as optional peer dependencies. Only the driver for the database you actually connect to needs to be installed.
| Engine | Peer Dependency | Typical Version |
|---|---|---|
| PostgreSQL | pg |
^8.0.0 |
| MySQL | mysql2 |
^3.0.0 |
| SQLite | better-sqlite3 |
^12.0.0 |
| SQL Server | tedious |
^20.0.0 |
Install only the drivers you need:
# PostgreSQL onlynpm install pg
# MySQL onlynpm install mysql2
# SQLite onlynpm install better-sqlite3
# SQL Server onlynpm install tediousKnex loads the peer driver that matches the configured client name. If a driver is missing at runtime, the underlying database client will raise an error when the first query is attempted. This approach keeps installs small and avoids forcing native build toolchains for engines you do not use.
Data Types
Section titled “Data Types”Supported PostgreSQL Types
Section titled “Supported PostgreSQL Types”TabulaLens supports all major PostgreSQL data types:
Numeric Types
Section titled “Numeric Types”integer/int4- 32-bit integerbigint/int8- 64-bit integersmallint/int2- 16-bit integerdecimal/numeric- Exact numeric with precisionreal/float4- 32-bit floating pointdouble precision/float8- 64-bit floating point
String Types
Section titled “String Types”varchar(n)- Variable-length string with limittext- Variable-length string without limitchar(n)- Fixed-length stringjson- JSON datajsonb- Binary JSON data (optimized)
Date/Time Types
Section titled “Date/Time Types”date- Date (year, month, day)time- Time of daytimestamp- Date and timetimestamptz- Date and time with timezoneinterval- Time span
Boolean Type
Section titled “Boolean Type”boolean/bool- True or false
Binary Types
Section titled “Binary Types”bytea- Binary data
Array Types
Section titled “Array Types”integer[]- Array of integerstext[]- Array of strings- Custom array types
Custom Types
Section titled “Custom Types”enum- Enumeration typescomposite- Custom composite typesdomain- Custom domains
Common Type Mapping
Section titled “Common Type Mapping”TabulaLens automatically maps database types to JavaScript types. The table below covers common mappings across all supported engines.
| Database | Source Type(s) | JavaScript Type |
|---|---|---|
| PostgreSQL | integer |
number |
| PostgreSQL | bigint |
string (or number if in range) |
| PostgreSQL | varchar, text |
string |
| PostgreSQL | boolean |
boolean |
| PostgreSQL | json, jsonb |
object |
| PostgreSQL | date, timestamp, timestamptz |
string (ISO format) |
| PostgreSQL | bytea |
Buffer |
| MySQL | int, integer |
number |
| MySQL | bigint |
string |
| MySQL | varchar, text |
string |
| MySQL | tinyint(1), boolean |
boolean |
| MySQL | json |
object |
| MySQL | date, timestamp |
string (ISO format) |
| MySQL | blob, binary |
Buffer |
| SQLite | INTEGER |
number |
| SQLite | TEXT, VARCHAR, CHAR |
string |
| SQLite | REAL |
number |
| SQLite | BLOB |
Buffer |
| SQLite | NUMERIC |
number or string (depends on stored value) |
| SQL Server | int |
number |
| SQL Server | bigint |
string |
| SQL Server | varchar, nvarchar, text |
string |
| SQL Server | bit |
boolean |
| SQL Server | date, datetime, datetime2 |
string (ISO format) |
| SQL Server | varbinary |
Buffer |
Performance Optimization
Section titled “Performance Optimization”Indexing
Section titled “Indexing”Proper indexing is crucial for performance:
-- Create index on frequently queried columnsCREATE INDEX idx_users_name ON users(name);CREATE INDEX idx_users_email ON users(email);
-- Create composite index for multiple columnsCREATE INDEX idx_users_name_email ON users(name, email);Query Optimization Tips
Section titled “Query Optimization Tips”- Use Appropriate Indexes: Index columns used in WHERE, JOIN, and ORDER BY clauses
- Limit Result Sets: Use pagination to avoid large result sets
- **Avoid SELECT ***: Specify only needed columns
- Use EXPLAIN: Analyze query execution plans
- Optimize Joins: Ensure join conditions are indexed
- Use Connection Pooling: Reuse database connections
- Monitor Slow Queries: Track and optimize slow queries
Connection Optimization
Section titled “Connection Optimization”Connection pooling is handled by the underlying Knex driver. Configure pool size, timeouts, and other driver-specific settings through the connection string or the driver environment variables. The exact parameters vary by engine, so consult the documentation for pg, mysql2, better-sqlite3, or tedious.
Transaction Support
Section titled “Transaction Support”TabulaLens relies on the database driver for transactions. Use the standard SQL transaction pattern through the underlying query layer:
async function transferFunds(fromId: number, toId: number, amount: number) { // Driver-specific transaction API await db.transaction(async (trx) => { await trx('accounts') .where({ id: fromId }) .decrement('balance', amount);
await trx('accounts') .where({ id: toId }) .increment('balance', amount); });}Transaction Best Practices
Section titled “Transaction Best Practices”- Keep Transactions Short: Minimize time in transactions
- Handle Errors Properly: Always rollback on errors
- Use Appropriate Isolation Levels: Choose the right isolation level for your engine
- Avoid Long-Running Transactions: They can block other operations
- Test Transaction Logic: Ensure rollback works correctly
Security
Section titled “Security”Credential Security
Section titled “Credential Security”- Environment Variables: Store credentials in environment variables
- No Hardcoding: Never hardcode credentials in code
- Secret Management: Use secret management systems in production
- Limited Privileges: Use database users with minimal required privileges
Network Security
Section titled “Network Security”- SSL/TLS: Use encrypted connections
- Firewall Rules: Restrict database access
- VPN: Use VPN for remote database access
- Connection Limits: Set connection limits per user
Data Security
Section titled “Data Security”- Input Validation: Validate all inputs
- Parameterized Queries: Prevent SQL injection
- Row-Level Security: Implement row-level security where your engine supports it
- Audit Logging: Log database access and modifications
Monitoring and Observability
Section titled “Monitoring and Observability”Connection Monitoring
Section titled “Connection Monitoring”Monitor connection pool health through the underlying driver or Knex. Most drivers expose pool statistics such as total, idle, and waiting connections.
// Example for pg; other drivers expose similar metricsconst poolStats = { totalCount: pool.totalCount, idleCount: pool.idleCount, waitingCount: pool.waitingCount,};Query Monitoring
Section titled “Query Monitoring”Track query performance:
const tabulaLens = new TabulaLens(databaseUrl, { logger: { info: (message) => { if (message.queryTime > 1000) { console.warn('Slow query:', message); } }, },});Health Checks
Section titled “Health Checks”Implement database health checks:
async function healthCheck() { try { await tabulaLens.query({ table: 'users', limit: 1 }); return { status: 'healthy', database: 'connected' }; } catch (error) { return { status: 'unhealthy', database: 'disconnected', error: error.message }; }}Backup and Recovery
Section titled “Backup and Recovery”Backup Strategies
Section titled “Backup Strategies”- Regular Backups: Schedule regular database backups
- Point-in-Time Recovery: Enable transaction log / WAL archiving where supported
- Offsite Backups: Store backups in multiple locations
- Backup Testing: Regularly test backup restoration
Example Backup Commands
Section titled “Example Backup Commands”# PostgreSQLpg_dump -U username -h localhost -d mydb > backup.sql
# MySQLmysqldump -u username -p mydb > backup.sql
# SQLite (file copy)cp mydb.db mydb-backup.db
# SQL Serversqlcmd -S localhost -Q "BACKUP DATABASE [mydb] TO DISK = 'backup.bak'"Migration and Schema Management
Section titled “Migration and Schema Management”Schema Migrations
Section titled “Schema Migrations”Use the migration tool appropriate for your engine:
# PostgreSQLnpx node-pg-migrate up
# MySQL / general SQLnpx knex migrate:latest
# SQLitenpx knex migrate:latest --client better-sqlite3Migration Best Practices
Section titled “Migration Best Practices”- Version Control: Store migrations in version control
- Test Migrations: Test migrations on staging first
- Backward Compatibility: Ensure migrations are backward compatible
- Rollback Plans: Always have rollback plans
- Document Changes: Document schema changes
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Connection Issues:
- Check
DATABASE_URLis correct - Verify the database server is running
- Check network connectivity
- Verify firewall rules
Performance Issues:
- Check for missing indexes
- Analyze slow queries with
EXPLAIN - Monitor connection pool usage
- Check database server resources
Query Issues:
- Verify table and column names
- Check data types match
- Validate query parameters
- Review error messages
Debug Mode
Section titled “Debug Mode”Enable debug logging:
const tabulaLens = new TabulaLens(databaseUrl, { logLevel: 'debug', logFormat: 'pretty',});Best Practices
Section titled “Best Practices”- Use Environment Variables: Never hardcode credentials
- Implement Connection Pooling: Configure appropriate pool sizes for your engine
- Use Indexes: Index frequently queried columns
- Monitor Performance: Track query performance and connection usage
- Handle Errors Gracefully: Provide meaningful error messages
- Use Transactions: Use transactions for multi-step operations
- Regular Backups: Schedule regular database backups
- Test Thoroughly: Test database operations thoroughly
- Document Schema: Document your database schema
- Security First: Always prioritize security considerations