Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
7.7 KiB
Plaintext
188 lines
7.7 KiB
Plaintext
---
|
|
description: Definitive guidelines for writing robust, performant, and secure SQLite code. Focuses on schema design, query optimization, and transaction management.
|
|
globs: **/*
|
|
---
|
|
# sqlite Best Practices
|
|
|
|
> В проекте используется **better-sqlite3** через Drizzle (`packages/db`). WASM-`sql.js` выводится из эксплуатации. См. [`backend-drizzle.mdc`](backend-drizzle.mdc).
|
|
|
|
SQLite is the go-to embedded SQL engine for local, reliable storage. Adhere to these rules to ensure your SQLite code is maintainable, performant, and secure.
|
|
|
|
## 1. Data Modeling & Schema Design
|
|
|
|
Design your schema for integrity and performance from day one.
|
|
|
|
* **Primary Keys**: Always use `INTEGER PRIMARY KEY AUTOINCREMENT` for ID columns. This optimizes `rowid` lookups and simplifies ID generation.
|
|
* ❌ BAD:
|
|
```sql
|
|
CREATE TABLE users (
|
|
id TEXT PRIMARY KEY, -- Manual UUIDs or similar
|
|
name TEXT NOT NULL
|
|
);
|
|
```
|
|
* ✅ GOOD:
|
|
```sql
|
|
CREATE TABLE users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
* **Data Types & Constraints**: Declare appropriate data types and enforce integrity with `NOT NULL`, `UNIQUE`, and `FOREIGN KEY` constraints.
|
|
* ❌ BAD:
|
|
```sql
|
|
CREATE TABLE products (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT, -- Allows NULL, no uniqueness
|
|
price REAL
|
|
);
|
|
```
|
|
* ✅ GOOD:
|
|
```sql
|
|
CREATE TABLE products (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
price REAL NOT NULL,
|
|
stock INTEGER DEFAULT 0,
|
|
category_id INTEGER,
|
|
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
|
|
);
|
|
```
|
|
|
|
* **Naming Conventions**: Use `lower_case_snake_case` for all table, column, and index names. Avoid SQLite keywords as identifiers.
|
|
* ❌ BAD: `CREATE TABLE My_Users ( UserId INTEGER PRIMARY KEY );`
|
|
* ✅ GOOD: `CREATE TABLE my_users ( user_id INTEGER PRIMARY KEY );`
|
|
|
|
## 2. Performance Considerations
|
|
|
|
Optimize for speed by minimizing I/O and leveraging the SQLite engine.
|
|
|
|
* **Enable WAL Mode**: Always enable Write-Ahead Logging for better concurrency and write performance.
|
|
* ❌ BAD: Default journal mode (`DELETE`).
|
|
* ✅ GOOD (at database open or once):
|
|
```sql
|
|
PRAGMA journal_mode = WAL;
|
|
```
|
|
|
|
* **Relax Synchronous Mode**: When using WAL, set `synchronous` to `NORMAL` for faster commits, accepting minimal risk of data loss on power failure (not app crash).
|
|
* ❌ BAD: Default `synchronous = FULL`.
|
|
* ✅ GOOD (at database open or once):
|
|
```sql
|
|
PRAGMA synchronous = NORMAL;
|
|
```
|
|
|
|
* **Indexes**: Create indexes on columns frequently used in `WHERE`, `ORDER BY`, `GROUP BY`, or `JOIN` clauses. Avoid over-indexing.
|
|
* ❌ BAD:
|
|
```sql
|
|
SELECT * FROM users WHERE email = 'test@example.com'; -- No index on email
|
|
```
|
|
* ✅ GOOD:
|
|
```sql
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
SELECT id, name FROM users WHERE email = 'test@example.com';
|
|
```
|
|
* **Multi-column Indexes**: For queries filtering/sorting on multiple columns, create a multi-column index matching the query order.
|
|
```sql
|
|
CREATE INDEX idx_products_category_price ON products(category_id, price);
|
|
SELECT * FROM products WHERE category_id = 1 ORDER BY price DESC;
|
|
```
|
|
|
|
* **Query Optimization**: Select only the columns you need. Push filtering, sorting, and aggregation into SQL.
|
|
* ❌ BAD:
|
|
```sql
|
|
SELECT * FROM products; -- Fetch all columns
|
|
-- Then filter/sort in application code
|
|
```
|
|
* ✅ GOOD:
|
|
```sql
|
|
SELECT id, name, price FROM products WHERE stock > 0 ORDER BY price ASC LIMIT 10;
|
|
```
|
|
|
|
## 3. Transactions & Concurrency
|
|
|
|
Ensure data consistency and improve write performance with explicit transactions.
|
|
|
|
* **Wrap Writes in Transactions**: Group multiple `INSERT`, `UPDATE`, `DELETE` operations within a single transaction. This significantly reduces disk I/O.
|
|
* ❌ BAD:
|
|
```sql
|
|
INSERT INTO logs (action) VALUES ('User created');
|
|
INSERT INTO users (name) VALUES ('New User');
|
|
INSERT INTO logs (action) VALUES ('User name updated');
|
|
UPDATE users SET name = 'Updated User' WHERE id = 1;
|
|
```
|
|
* ✅ GOOD:
|
|
```sql
|
|
BEGIN;
|
|
INSERT INTO logs (action) VALUES ('User created');
|
|
INSERT INTO users (name) VALUES ('New User');
|
|
INSERT INTO logs (action) VALUES ('User name updated');
|
|
UPDATE users SET name = 'Updated User' WHERE id = 1;
|
|
COMMIT;
|
|
```
|
|
|
|
* **Error Handling**: Use `ROLLBACK` to revert all changes if any operation within a transaction fails.
|
|
* ✅ GOOD:
|
|
```sql
|
|
BEGIN;
|
|
-- Perform operations
|
|
INSERT INTO users (name) VALUES ('Valid User');
|
|
INSERT INTO users (name) VALUES (NULL); -- This will fail due to NOT NULL
|
|
-- If an error occurs, catch it and:
|
|
ROLLBACK;
|
|
-- Else:
|
|
COMMIT;
|
|
```
|
|
|
|
## 4. Security Best Practices
|
|
|
|
Prevent common vulnerabilities like SQL injection.
|
|
|
|
* **Prepared Statements**: Always use prepared statements with bound parameters. NEVER concatenate user input directly into SQL queries.
|
|
* ❌ BAD:
|
|
```sql
|
|
String name = userInput.getName();
|
|
String sql = "INSERT INTO users (name) VALUES ('" + name + "');"; // SQL Injection risk!
|
|
```
|
|
* ✅ GOOD (using a typical API pattern):
|
|
```sql
|
|
PreparedStatement stmt = connection.prepareStatement("INSERT INTO users (name) VALUES (?);");
|
|
stmt.setString(1, userInput.getName());
|
|
stmt.executeUpdate();
|
|
```
|
|
|
|
* **Enable Foreign Key Enforcement**: Always enable foreign key constraints at runtime. SQLite defaults to `OFF` for backward compatibility.
|
|
* ❌ BAD: Forgetting to enable foreign keys, leading to orphaned records.
|
|
* ✅ GOOD (at database open or once per connection):
|
|
```sql
|
|
PRAGMA foreign_keys = ON;
|
|
```
|
|
|
|
* **File Permissions**: Store database files in write-protected directories and set restrictive file permissions to limit unauthorized access. This is OS-specific but critical.
|
|
|
|
## 5. Common Pitfalls & Gotchas
|
|
|
|
Avoid these common mistakes that lead to bugs and performance issues.
|
|
|
|
* **Forgetting `PRAGMA foreign_keys = ON;`**: This is the most common pitfall. Always enable it.
|
|
* **Selecting `*`**: Only retrieve the columns you actually need.
|
|
* **Application-level Filtering/Sorting**: Delegate these operations to SQL for better performance, especially on large datasets.
|
|
* **Not Using Transactions**: Leads to slow writes and potential data inconsistencies.
|
|
* **Using SQLite for High-Concurrency Writes**: SQLite is a single-writer database. If multiple processes need to write concurrently, consider a client-server RDBMS.
|
|
|
|
## 6. Testing Approaches
|
|
|
|
Ensure your data access logic is robust and correct.
|
|
|
|
* **In-Memory Databases**: Use `:memory:` databases for fast, isolated unit and integration tests of your data access layer.
|
|
* ✅ GOOD (example in Python, similar patterns exist in other languages):
|
|
```python
|
|
import sqlite3
|
|
conn = sqlite3.connect(':memory:')
|
|
cursor = conn.cursor()
|
|
cursor.execute("CREATE TABLE test_data (id INTEGER PRIMARY KEY, value TEXT)")
|
|
# ... run tests ...
|
|
conn.close() # Database vanishes
|
|
```
|
|
|
|
* **Seed Data**: Create consistent, reproducible test data for your tests.
|
|
* **Mocking**: For higher-level tests, mock your database interactions to focus on business logic. |