Skip to main content

Overview

ICL Cotizaciones uses SQLite with better-sqlite3 for data storage. The database is located at {cwd}/data/icl.db and runs in WAL (Write-Ahead Logging) mode with foreign keys enabled.

Database Configuration

Location and Settings

Database Path: ./data/icl.db (relative to application root) Configuration (src/db/index.ts:12-15):
const sqlite = new Database(dbPath);
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("foreign_keys = ON");
Drizzle Configuration (drizzle.config.ts):
export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "sqlite",
  dbCredentials: {
    url: "./data/icl.db",
  },
});

Database Schema

The database contains the following tables:
TableDescription
usersSystem users with authentication and roles
clientsCustomer records (FFWW, Final, or Both)
locationsOrigin and via locations for shipping
commercial_agreementsClient pricing and terms
quotationsFreight quotations and sales records
pricing_netosBase freight rates by port and container type
port_ratesLCL rate schedules by volume

Schema Migrations

Migration Workflow

1
Modify Schema
2
Edit the schema definition in src/db/schema.ts.
3
Example: Adding a new field
4
export const clients = sqliteTable("clients", {
  // ... existing fields
  website: text("website"), // new field
});
5
Generate Migration
6
Create SQL migration files:
7
npm run db:generate
8
This runs drizzle-kit generate and creates timestamped SQL files in drizzle/.
9
Review Migration
10
Check the generated SQL:
11
cat drizzle/0001_*.sql
12
Expected output:
13
ALTER TABLE clients ADD COLUMN website TEXT;
14
Apply Migration
15
Run the migration:
16
npm run db:migrate
17
This runs drizzle-kit migrate and applies changes to data/icl.db.
18
Verify Migration
19
Check the schema:
20
sqlite3 data/icl.db ".schema clients"

SQLite Limitations

SQLite has limited ALTER TABLE support. Some schema changes require table recreation:
  • Adding/removing CHECK constraints
  • Modifying column types
  • Adding NOT NULL to existing columns
For complex migrations, use manual migration scripts (see below).

Manual Migration Scripts

The repository includes manual migration scripts for operations that Drizzle/SQLite cannot handle automatically:

Available Scripts

ScriptCommandDescription
migrate.tsnpx tsx src/db/migrate.tsInitial schema creation
migrate-v2.tsnpx tsx src/db/migrate-v2.tsSchema version 2 changes
migrate-v3.tsnpx tsx src/db/migrate-v3.tsFix CHECK constraint: location_type enum
migrate-v4.tsnpx tsx src/db/migrate-v4.tsAdd CSV role to users CHECK constraint
migrate-v5.tsnpx tsx src/db/migrate-v5.tsBackfill week column for 1049 records

Running Manual Migrations

1
Execute Migration Script
2
npx tsx src/db/migrate-v5.ts
3
Check Migration Output
4
Review console output for success messages:
5
Backfilling week for 1049 quotations...
Updated 1049 records
6
Verify Data
7
Query the database:
8
sqlite3 data/icl.db "SELECT COUNT(*) FROM quotations WHERE week IS NULL;"
9
Expected: 0

Data Import Scripts

For migrating historical data from AppSheet or Excel:

Import Scripts

ScriptCommandTypeDescription
import.tsnpx tsx src/db/import.tsDestructiveImport from XLSX (icldata.xlsx)
import-cotizaciones.tsnpx tsx src/db/import-cotizaciones.tsIdempotentImport quotations from JSON
link-cotizaciones.tsnpx tsx src/db/link-cotizaciones.tsIdempotentLink quotations to clients
normalize-clients.tsnpx tsx src/db/normalize-clients.tsIdempotentNormalize client names to Title Case
enrich-clients.tsnpx tsx src/db/enrich-clients.tsIdempotentAdd phone, email, tax_id from JSON
tag-final-clients.tsnpx tsx src/db/tag-final-clients.tsIdempotentMark Both client types
import.ts is destructive and clears tables before import. Use only for initial data load.

AppSheet Migration Workflow

Recommended order for importing AppSheet data:
1
Import Quotations
2
npx tsx src/db/import-cotizaciones.ts
3
Imports from allcotis.json. Uses INSERT OR IGNORE (safe to re-run).
5
npx tsx src/db/link-cotizaciones.ts
6
Matches quotations to clients by name. Creates missing clients.
7
Normalize Names
8
npx tsx src/db/normalize-clients.ts
9
Converts names to Title Case (e.g., “ACME SRL” → “Acme SRL”).
10
Enrich Client Data
11
npx tsx src/db/enrich-clients.ts
12
Adds phone, email, user_id, tax_id from CLIENTES.json.
13
Tag Final Clients
14
npx tsx src/db/tag-final-clients.ts
15
Marks clients from CLIENTES_FINALES.json as Both type.

Seed Data

For development and testing:

Running Seed

DESTRUCTIVE: This deletes the existing database and creates a new one.
npm run db:seed
This runs tsx src/db/seed.ts which:
  1. Deletes data/icl.db and WAL files
  2. Creates fresh database with schema
  3. Inserts test data:
    • 8 users (all password: icl2026)
    • 30 clients
    • 20 origin locations
    • 20 via locations
    • 200 quotations

Seed User Accounts

EmailNameRolePassword
[email protected]Admin ICLDIRECTORicl2026
[email protected]Maria LopezGERENTEicl2026
[email protected]Agustina BosichCOMERCIALicl2026
[email protected]Bruno GonzalezCOMERCIALicl2026
[email protected]Diego OsmanCOMERCIALicl2026
[email protected]Federico MonserratCOMERCIALicl2026
[email protected]Gonzalo BalenaCOMERCIALicl2026
[email protected]Pablo PascalCOMERCIALicl2026

Database Backups

Manual Backup

1
Stop Application
2
pm2 stop icl-cotizaciones
# or
sudo systemctl stop icl-cotizaciones
3
Create Backup
4
cp data/icl.db data/icl.db.backup.$(date +%Y%m%d_%H%M%S)
cp data/icl.db-wal data/icl.db-wal.backup.$(date +%Y%m%d_%H%M%S)
cp data/icl.db-shm data/icl.db-shm.backup.$(date +%Y%m%d_%H%M%S)
5
Verify Backup
6
sqlite3 data/icl.db.backup.* "PRAGMA integrity_check;"
7
Expected: ok
8
Restart Application
9
pm2 start icl-cotizaciones

Automated Backups

Create a backup script at /usr/local/bin/backup-icl-db.sh:
#!/bin/bash
APP_DIR="/opt/iclapp"
BACKUP_DIR="/backups/icl-cotizaciones"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30

mkdir -p "$BACKUP_DIR"

# Create backup using SQLite VACUUM INTO (safe for WAL mode)
sqlite3 "$APP_DIR/data/icl.db" "VACUUM INTO '$BACKUP_DIR/icl.db.$DATE';"

# Compress backup
gzip "$BACKUP_DIR/icl.db.$DATE"

# Remove old backups
find "$BACKUP_DIR" -name "icl.db.*.gz" -mtime +$RETENTION_DAYS -delete

echo "Backup completed: icl.db.$DATE.gz"
Make executable:
sudo chmod +x /usr/local/bin/backup-icl-db.sh
Add to crontab (daily at 2 AM):
sudo crontab -e
Add line:
0 2 * * * /usr/local/bin/backup-icl-db.sh >> /var/log/icl-backup.log 2>&1

Restore from Backup

Restoring will overwrite current database. Make a backup first.
1
Stop Application
2
pm2 stop icl-cotizaciones
3
Decompress Backup
4
gunzip -c /backups/icl-cotizaciones/icl.db.20260304_020000.gz > data/icl.db.restore
5
Verify Integrity
6
sqlite3 data/icl.db.restore "PRAGMA integrity_check;"
7
Replace Database
8
mv data/icl.db data/icl.db.old
mv data/icl.db.restore data/icl.db
rm -f data/icl.db-wal data/icl.db-shm
9
Restart Application
10
pm2 start icl-cotizaciones
11
Verify Restoration
12
sqlite3 data/icl.db "SELECT COUNT(*) FROM quotations;"

Database Maintenance

Optimize Database

Run VACUUM to reclaim space and optimize performance:
1
Stop Application
2
pm2 stop icl-cotizaciones
3
Run VACUUM
4
sqlite3 data/icl.db "VACUUM;"
5
Analyze Statistics
6
sqlite3 data/icl.db "ANALYZE;"
7
Restart Application
8
pm2 start icl-cotizaciones

WAL Checkpoint

Manually checkpoint WAL to main database:
sqlite3 data/icl.db "PRAGMA wal_checkpoint(TRUNCATE);"

Check Database Size

du -h data/icl.db*
Example output:
5.2M    data/icl.db
128K    data/icl.db-shm
2.1M    data/icl.db-wal

Verify Integrity

Run integrity check:
sqlite3 data/icl.db "PRAGMA integrity_check;"
Expected: ok If errors found:
sqlite3 data/icl.db "PRAGMA integrity_check;"
# If corruption detected, restore from backup

Direct Database Access

Using SQLite CLI

1
Open Database
2
sqlite3 data/icl.db
3
Run Query
4
SELECT COUNT(*) FROM quotations WHERE status = 'SI';
5
Export Results
6
.mode csv
.output export.csv
SELECT * FROM quotations WHERE date > '2026-01-01';
.quit

Common Queries

Count quotations by status:
SELECT status, COUNT(*) as count 
FROM quotations 
GROUP BY status;
Find inactive users:
SELECT id, full_name, email 
FROM users 
WHERE is_active = 0;
List clients with no quotations:
SELECT c.id, c.legal_name 
FROM clients c
LEFT JOIN quotations q ON c.id = q.client_id
WHERE q.id IS NULL;

Performance Tuning

Enable Query Profiling

Log slow queries:
// In src/db/index.ts
sqlite.pragma("query_only = OFF");
sqlite.function("log_query", (query) => {
  console.log("[SQL]", query);
});

Add Indexes

For frequently queried columns:
CREATE INDEX idx_quotations_client ON quotations(client_id);
CREATE INDEX idx_quotations_status ON quotations(status);
CREATE INDEX idx_quotations_date ON quotations(date);
CREATE INDEX idx_quotations_user ON quotations(user_id);

Configure WAL Settings

Optimize WAL performance:
sqlite.pragma("wal_autocheckpoint = 1000");
sqlite.pragma("journal_size_limit = 67108864"); // 64MB

Troubleshooting

For database-related issues, see Troubleshooting Guide.

Build docs developers (and LLMs) love