From f063389ab01d57fa73b836227556db36b5ebb45f Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:16:20 +0800 Subject: [PATCH 1/7] feat(transaction): implement transaction management methods in QueryHandler and add usage examples --- examples/transaction_example.js | 228 ++++++++++++++++++++++++++++++++ index.d.ts | 26 ++++ src/operator.js | 105 +++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 examples/transaction_example.js diff --git a/examples/transaction_example.js b/examples/transaction_example.js new file mode 100644 index 0000000..e09d9a4 --- /dev/null +++ b/examples/transaction_example.js @@ -0,0 +1,228 @@ +/* eslint-disable no-console */ +'use strict'; + +/** + * 事务使用示例 + * + * 这个示例展示了如何正确使用事务,避免连接阻塞问题 + */ + +const mysql = require('mysql2/promise'); +const { QueryHandler } = require('../src/operator'); + +/** + * 示例 1: 使用连接池(推荐) + * 优点:自动从池中获取新连接,不会阻塞其他操作 + */ +async function exampleWithPool() { + const pool = mysql.createPool({ + host: 'localhost', + port: 3306, + user: 'root', + password: 'password', + database: 'test_db', + connectionLimit: 10 + }); + + const queryHandler = new QueryHandler(pool); + + try { + // 方式 1: 使用 QueryHandler.beginTransaction()(推荐) + // 这会自动从连接池获取一个新连接用于事务 + const transaction = await queryHandler.beginTransaction({ level: 'RC' }); + + try { + // 执行事务操作 + await transaction.table('users').insert({ name: 'John', age: 30 }); + await transaction.table('orders').insert({ user_id: 1, total: 100 }); + + // 提交事务(会自动释放连接回池) + await transaction.commit(); + console.log('Transaction committed successfully'); + } catch (err) { + // 回滚事务(会自动释放连接回池) + await transaction.rollback(); + console.error('Transaction rolled back:', err.message); + throw err; + } + + // 同时,其他操作不会被阻塞 + const users = await queryHandler.table('users').select(); + console.log('Users:', users); + + } finally { + await pool.end(); + } +} + +/** + * 示例 2: 使用单一连接(不推荐用于生产环境) + * 注意:事务执行期间会阻塞该连接的其他操作 + */ +async function exampleWithSingleConnection() { + const connection = await mysql.createConnection({ + host: 'localhost', + port: 3306, + user: 'root', + password: 'password', + database: 'test_db' + }); + + const queryHandler = new QueryHandler(connection); + + try { + // 使用单一连接创建事务 + const transaction = await queryHandler.beginTransaction({ level: 'RR' }); + + try { + await transaction.table('users').insert({ name: 'Jane', age: 25 }); + await transaction.commit(); + console.log('Transaction committed'); + } catch (err) { + await transaction.rollback(); + console.error('Transaction rolled back:', err); + throw err; + } + + } finally { + await connection.end(); + } +} + +/** + * 示例 3: 直接使用 TransactionHandler + * 适用于需要更多控制的场景 + */ +async function exampleWithTransactionHandler() { + const { TransactionHandler } = require('../src/transaction'); + + const connection = await mysql.createConnection({ + host: 'localhost', + port: 3306, + user: 'root', + password: 'password', + database: 'test_db' + }); + + const transaction = new TransactionHandler(connection, { level: 'SERIALIZABLE' }); + + try { + await transaction.begin(); + + // 执行多个操作 + await transaction.table('users').insert({ name: 'Bob', age: 35 }); + await transaction.table('user_profiles').insert({ user_id: 1, bio: 'Developer' }); + + // 使用锁 + const lockedRows = await transaction.table('products') + .where('id', [1, 2, 3], 'IN') + .append('FOR UPDATE') + .select(); + + console.log('Locked rows:', lockedRows); + + await transaction.commit(); + console.log('Transaction completed'); + } catch (err) { + await transaction.rollback(); + console.error('Error:', err); + throw err; + } finally { + await connection.end(); + } +} + +/** + * 示例 4: 并发事务(使用连接池) + * 展示多个事务可以同时执行而不相互阻塞 + */ +async function exampleConcurrentTransactions() { + const pool = mysql.createPool({ + host: 'localhost', + port: 3306, + user: 'root', + password: 'password', + database: 'test_db', + connectionLimit: 10 + }); + + const queryHandler = new QueryHandler(pool); + + try { + // 并发执行多个事务 + const results = await Promise.all([ + // 事务 1 + (async () => { + const tx = await queryHandler.beginTransaction(); + try { + await tx.table('users').insert({ name: 'User1', age: 20 }); + await tx.commit(); + return 'Transaction 1 completed'; + } catch (err) { + await tx.rollback(); + throw err; + } + })(), + + // 事务 2 + (async () => { + const tx = await queryHandler.beginTransaction(); + try { + await tx.table('users').insert({ name: 'User2', age: 21 }); + await tx.commit(); + return 'Transaction 2 completed'; + } catch (err) { + await tx.rollback(); + throw err; + } + })(), + + // 事务 3 + (async () => { + const tx = await queryHandler.beginTransaction(); + try { + await tx.table('users').insert({ name: 'User3', age: 22 }); + await tx.commit(); + return 'Transaction 3 completed'; + } catch (err) { + await tx.rollback(); + throw err; + } + })() + ]); + + console.log('All transactions completed:', results); + } finally { + await pool.end(); + } +} + +/** + * 最佳实践总结: + * + * 1. 生产环境推荐使用连接池(Pool) + * 2. 使用 QueryHandler.beginTransaction() 自动管理连接 + * 3. 始终在 try-catch-finally 中使用事务 + * 4. 确保调用 commit() 或 rollback() + * 5. 连接池会自动释放连接,无需手动管理 + * 6. 避免在事务中执行长时间运行的操作 + * 7. 根据需要选择合适的隔离级别: + * - 'RU' / 'READ UNCOMMITTED': 最低隔离级别,性能最好,可能读到脏数据 + * - 'RC' / 'READ COMMITTED': 避免脏读 + * - 'RR' / 'REPEATABLE READ': MySQL 默认级别,避免不可重复读 + * - 'S' / 'SERIALIZABLE': 最高隔离级别,完全串行化,性能最差 + */ + +// 运行示例(取消注释以运行) +// exampleWithPool().catch(console.error); +// exampleWithSingleConnection().catch(console.error); +// exampleWithTransactionHandler().catch(console.error); +// exampleConcurrentTransactions().catch(console.error); + +module.exports = { + exampleWithPool, + exampleWithSingleConnection, + exampleWithTransactionHandler, + exampleConcurrentTransactions +}; + diff --git a/index.d.ts b/index.d.ts index 9c4a498..05648df 100644 --- a/index.d.ts +++ b/index.d.ts @@ -412,6 +412,32 @@ export declare class QueryHandler { * @param attrs */ getTableFields(database: string, table: string, ...attrs: TableInfoColumn[]): Promise; + + /** + * Begin a transaction and return a TransactionHandler instance + * + * Note: If using a Pool, this will automatically get a new connection from the pool + * to avoid blocking other operations. The connection will be released back to pool + * after commit/rollback. + * + * @param options - Transaction options + * @param options.level - Transaction isolation level + * @param options.useNewConnection - Force create new connection from pool (default: true for Pool, false for Connection) + */ + beginTransaction(options?: { + level?: TransactionLevel; + useNewConnection?: boolean; + }): Promise; + + /** + * Commit current connection transaction (if using promise connection directly) + */ + commit(): Promise; + + /** + * Rollback current connection transaction (if using promise connection directly) + */ + rollback(): Promise; } export declare class TransactionOperator extends QueryOperator { diff --git a/src/operator.js b/src/operator.js index 6216f31..6a4601a 100644 --- a/src/operator.js +++ b/src/operator.js @@ -259,6 +259,111 @@ class QueryHandler { values: [database, table] }); } + + /** + * Begin a transaction and return a TransactionHandler instance + * + * Note: If using a Pool, this will automatically get a new connection from the pool + * to avoid blocking other operations. The connection will be released back to pool + * after commit/rollback. + * + * @param {object} options - Transaction options + * @param {string} options.level - Transaction isolation level (RU, RC, RR, S or full name) + * @param {boolean} options.useNewConnection - Force create new connection from pool (default: true for Pool, false for Connection) + * @returns {Promise} + */ + async beginTransaction(options = {}) { + const { TransactionHandler } = require('./transaction'); + const transactionOptions = { + ...this.options, + level: options.level || 'SERIALIZABLE' + }; + + let conn = this.conn; + let isPoolConnection = false; + + // Check if this.conn is a Pool + if (this.conn && typeof this.conn.getConnection === 'function') { + // This is a Pool, get a new connection from pool for transaction + const useNewConnection = options.useNewConnection !== false; // default true for pool + if (useNewConnection) { + // Get connection from pool (works for both callback and promise pools) + conn = await new Promise((resolve, reject) => { + this.conn.getConnection((err, connection) => { + if (err) { + reject(err); + } else { + resolve(connection); + } + }); + }); + // Convert callback connection to promise-based if needed + if (conn && typeof conn.promise === 'function') { + conn = conn.promise(); + } + isPoolConnection = true; + } + } else if (this.conn && typeof this.conn.promise === 'function') { + // This is a mysql2 Connection, convert to promise-based + conn = this.conn.promise(); + } + + const transaction = new TransactionHandler(conn, transactionOptions); + + // Store pool connection flag for cleanup + if (isPoolConnection) { + transaction._poolConnection = conn; + transaction._originalCommit = transaction.commit.bind(transaction); + transaction._originalRollback = transaction.rollback.bind(transaction); + + // Override commit to release connection back to pool + transaction.commit = async function () { + try { + await this._originalCommit(); + } finally { + if (this._poolConnection && typeof this._poolConnection.release === 'function') { + this._poolConnection.release(); + } + } + }; + + // Override rollback to release connection back to pool + transaction.rollback = async function () { + try { + await this._originalRollback(); + } finally { + if (this._poolConnection && typeof this._poolConnection.release === 'function') { + this._poolConnection.release(); + } + } + }; + } + + await transaction.begin(); + return transaction; + } + + /** + * Commit current connection transaction (if using promise connection directly) + */ + async commit() { + if (this.conn && typeof this.conn.commit === 'function') { + await this.conn.commit(); + } else { + throw new Error('Connection does not support commit operation'); + } + } + + /** + * Rollback current connection transaction (if using promise connection directly) + */ + async rollback() { + if (this.conn && typeof this.conn.rollback === 'function') { + await this.conn.rollback(); + } else { + throw new Error('Connection does not support rollback operation'); + } + } } module.exports = { From d19600b008c2105bb57c5d01425fc3404a654501 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:16:50 +0800 Subject: [PATCH 2/7] chore: remove outdated documentation files for coding standards, API patterns, development guidelines, project context, and README --- .cursor/rules/README.md | 111 ---- .cursor/rules/api-patterns.md | 657 ------------------------ .cursor/rules/coding-standards.md | 560 -------------------- .cursor/rules/development-guidelines.md | 326 ------------ .cursor/rules/project-context.md | 306 ----------- 5 files changed, 1960 deletions(-) delete mode 100644 .cursor/rules/README.md delete mode 100644 .cursor/rules/api-patterns.md delete mode 100644 .cursor/rules/coding-standards.md delete mode 100644 .cursor/rules/development-guidelines.md delete mode 100644 .cursor/rules/project-context.md diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md deleted file mode 100644 index 27eca52..0000000 --- a/.cursor/rules/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# Cursor Rules for @axiosleo/orm-mysql - -This directory contains development guidelines and rules for the @axiosleo/orm-mysql project. These files provide comprehensive guidance for developers working on this MySQL ORM library. - -## Files Overview - -### 📋 [development-guidelines.md](./development-guidelines.md) -Comprehensive development guidelines covering: -- Project architecture and core components -- Development standards and best practices -- Query builder patterns and database operations -- Testing guidelines and security considerations -- Performance optimization and error handling -- Migration best practices and CLI development - -### 🎯 [coding-standards.md](./coding-standards.md) -Detailed coding standards including: -- File organization and naming conventions -- JavaScript code style guidelines -- Class design patterns and method chaining -- Documentation standards with JSDoc -- Testing structure and patterns -- Performance guidelines and security practices - -### 🔧 [api-patterns.md](./api-patterns.md) -API usage patterns and examples: -- Query builder construction patterns -- CRUD operation examples -- Transaction management patterns -- Hook system implementation -- Migration patterns and best practices -- Custom driver development - -### 🏗️ [project-context.md](./project-context.md) -Project context and background: -- Project overview and key features -- Technical architecture and dependencies -- Database support and SQL features -- Development workflow and principles -- Performance considerations and security features -- Testing strategy and deployment guidelines - -## Quick Reference - -### Core Principles - -1. **Fluent Interface**: Use method chaining for readable query building -2. **Type Safety**: Leverage TypeScript definitions for better development experience -3. **Security First**: Always use parameterized queries to prevent SQL injection -4. **Performance**: Optimize queries and use appropriate connection management -5. **Testing**: Maintain high test coverage with comprehensive test suites - -### Common Patterns - -```javascript -// Basic query pattern -const users = await db.table('users') - .where('status', 'active') - .orderBy('created_at', 'desc') - .limit(10) - .select(); - -// Transaction pattern -const transaction = new TransactionHandler(conn); -await transaction.begin(); -try { - // operations... - await transaction.commit(); -} catch (error) { - await transaction.rollback(); - throw error; -} - -// Migration pattern -function up(migration) { - migration.createTable('table_name', { - id: { type: 'int', primaryKey: true, autoIncrement: true }, - // ... columns - }); -} -``` - -### Key Guidelines - -- Use `'use strict';` in all JavaScript files -- Document all public APIs with JSDoc comments -- Implement proper error handling with context -- Use the built-in validation system for input validation -- Follow the established testing patterns with Mocha and Chai -- Maintain backward compatibility when possible - -## Getting Started - -When working on this project: - -1. **Read the Guidelines**: Start with [development-guidelines.md](./development-guidelines.md) -2. **Follow Standards**: Adhere to [coding-standards.md](./coding-standards.md) -3. **Use Patterns**: Reference [api-patterns.md](./api-patterns.md) for implementation examples -4. **Understand Context**: Review [project-context.md](./project-context.md) for background - -## Contributing - -When contributing to the project: - -1. Follow all coding standards and guidelines -2. Include comprehensive tests for new features -3. Update documentation for API changes -4. Ensure backward compatibility -5. Use the established patterns and conventions - -These rules ensure consistency, maintainability, and quality across the @axiosleo/orm-mysql codebase. diff --git a/.cursor/rules/api-patterns.md b/.cursor/rules/api-patterns.md deleted file mode 100644 index 12962c3..0000000 --- a/.cursor/rules/api-patterns.md +++ /dev/null @@ -1,657 +0,0 @@ -# API Patterns for @axiosleo/orm-mysql - -## Query Builder Patterns - -### Basic Query Construction - -1. **Table Selection** - ```javascript - // Single table - const query = db.table('users'); - - // Table with alias - const query = db.table('users', 'u'); - - // Multiple tables - const query = db.tables( - { table: 'users', alias: 'u' }, - { table: 'profiles', alias: 'p' } - ); - ``` - -2. **Attribute Selection** - ```javascript - // Select all columns - const users = await db.table('users').select(); - - // Select specific columns - const users = await db.table('users').select('id', 'name', 'email'); - - // Using attr() method - const users = await db.table('users') - .attr('id', 'name', 'email') - .select(); - - // Subquery as attribute - const usersWithPostCount = await db.table('users', 'u') - .attr('u.id', 'u.name', () => { - return new Query('select') - .table('posts') - .where('user_id', '`u.id`') - .attr('COUNT(*)') - .alias('post_count'); - }) - .select(); - ``` - -### Condition Building - -1. **Basic WHERE Conditions** - ```javascript - // Equality - query.where('status', 'active'); - query.where('id', '=', 1); - - // Comparison operators - query.where('age', '>', 18); - query.where('created_at', '<=', new Date()); - - // Multiple conditions (AND by default) - query.where('status', 'active') - .where('age', '>', 18); - ``` - -2. **Logical Operators** - ```javascript - // OR conditions - query.where('status', 'active') - .whereOr() - .where('role', 'admin'); - - // Complex grouping - query.where('status', 'active') - .whereAnd() - .groupWhere( - ['age', '>', 18], - 'OR', - ['role', 'admin'] - ); - ``` - -3. **Advanced Conditions** - ```javascript - // IN conditions - query.whereIn('id', [1, 2, 3, 4]); - query.whereNotIn('status', ['deleted', 'banned']); - - // BETWEEN conditions - query.whereBetween('age', [18, 65]); - query.whereNotBetween('created_at', [startDate, endDate]); - - // LIKE conditions - query.whereLike('name', '%john%'); - query.whereNotLike('email', '%spam%'); - - // NULL conditions - query.where('deleted_at', null); - query.where('deleted_at', '!=', null); - - // JSON conditions (MySQL 5.7+) - query.where('metadata->name', 'John'); - query.whereContain('tags->hobbies', 'reading'); - query.whereOverlaps('permissions->roles', ['admin', 'user']); - ``` - -4. **Subquery Conditions** - ```javascript - // Subquery in WHERE - const subQuery = new Query('select') - .table('orders') - .where('status', 'completed') - .attr('user_id'); - - const users = await db.table('users') - .where('id', subQuery, 'IN') - .select(); - ``` - -### Joins - -1. **Basic Joins** - ```javascript - // Inner join - query.innerJoin('profiles', 'users.id = profiles.user_id'); - - // Left join - query.leftJoin('profiles', 'users.id = profiles.user_id', { alias: 'p' }); - - // Right join - query.rightJoin('orders', 'users.id = orders.user_id'); - ``` - -2. **Advanced Join Options** - ```javascript - // Join with subquery - const subQuery = new Query('select') - .table('order_items') - .attr('order_id', 'SUM(price) as total') - .groupBy('order_id'); - - query.join({ - table: subQuery, - alias: 'order_totals', - self_column: 'orders.id', - foreign_column: 'order_totals.order_id', - join_type: 'left' - }); - ``` - -### Sorting and Pagination - -1. **Ordering** - ```javascript - // Single column - query.orderBy('created_at', 'desc'); - - // Multiple columns - query.orderBy('status', 'asc') - .orderBy('created_at', 'desc'); - ``` - -2. **Pagination** - ```javascript - // Using limit and offset - query.limit(10).offset(20); - - // Using page method - query.page(10, 2); // limit 10, page 2 (offset 10) - ``` - -3. **Grouping and Having** - ```javascript - // Group by - query.groupBy('status', 'department'); - - // Having conditions - query.groupBy('department') - .having('COUNT(*)', '>', 5); - ``` - -## CRUD Operations - -### Create (Insert) - -1. **Single Record** - ```javascript - // Basic insert - const result = await db.table('users').insert({ - name: 'John Doe', - email: 'john@example.com', - status: 'active' - }); - - console.log(result.insertId); // Auto-generated ID - ``` - -2. **Upsert (Insert or Update)** - ```javascript - // Insert with duplicate key handling - const result = await db.table('users') - .keys('email') // Unique key - .insert({ - email: 'john@example.com', - name: 'John Doe', - status: 'active' - }); - ``` - -3. **Batch Insert** - ```javascript - const users = [ - { name: 'John', email: 'john@example.com' }, - { name: 'Jane', email: 'jane@example.com' }, - { name: 'Bob', email: 'bob@example.com' } - ]; - - const results = await db.table('users').insertAll(users); - ``` - -### Read (Select) - -1. **Single Record** - ```javascript - // Find by ID - const user = await db.table('users').where('id', 1).find(); - - // Find with conditions - const user = await db.table('users') - .where('email', 'john@example.com') - .where('status', 'active') - .find(); - ``` - -2. **Multiple Records** - ```javascript - // All records - const users = await db.table('users').select(); - - // With conditions - const activeUsers = await db.table('users') - .where('status', 'active') - .orderBy('created_at', 'desc') - .select(); - - // With pagination - const users = await db.table('users') - .where('status', 'active') - .limit(10) - .offset(0) - .select(); - ``` - -3. **Counting** - ```javascript - // Total count - const totalUsers = await db.table('users').count(); - - // Conditional count - const activeUsers = await db.table('users') - .where('status', 'active') - .count(); - ``` - -### Update - -1. **Basic Update** - ```javascript - // Update with conditions - const result = await db.table('users') - .where('id', 1) - .update({ - name: 'John Smith', - updated_at: new Date() - }); - - console.log(result.affectedRows); // Number of updated rows - ``` - -2. **Increment/Decrement** - ```javascript - // Increment by 1 - await db.table('users').where('id', 1).incrBy('login_count'); - - // Increment by specific amount - await db.table('users').where('id', 1).incrBy('points', 10); - - // Conditional increment - await db.table('users').where('id', 1).incrBy('error_count', (current) => { - return current > 5 ? 0 : 1; // Reset if too high - }); - ``` - -3. **Upsert Pattern** - ```javascript - // Update or insert - const result = await db.table('user_settings') - .upsertRow( - { user_id: 1, theme: 'dark', language: 'en' }, - { user_id: 1 } // Condition for existing record - ); - ``` - -### Delete - -1. **Conditional Delete** - ```javascript - // Delete with conditions - const result = await db.table('users') - .where('status', 'inactive') - .where('last_login', '<', oldDate) - .delete(); - ``` - -2. **Delete by ID** - ```javascript - // Delete single record by ID - const result = await db.table('users').delete(1); - - // Delete by custom key - const result = await db.table('users').delete('john@example.com', 'email'); - ``` - -## Transaction Patterns - -### Basic Transactions - -1. **Simple Transaction** - ```javascript - const conn = await createPromiseClient(connectionOptions); - const transaction = new TransactionHandler(conn); - - await transaction.begin(); - - try { - // Multiple operations - const user = await transaction.table('users').insert({ - name: 'John Doe', - email: 'john@example.com' - }); - - await transaction.table('profiles').insert({ - user_id: user.insertId, - bio: 'Software developer' - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - ``` - -2. **Transaction with Isolation Levels** - ```javascript - const transaction = new TransactionHandler(conn, { - level: 'SERIALIZABLE' // or 'READ COMMITTED', 'REPEATABLE READ', etc. - }); - ``` - -3. **Transaction with Locking** - ```javascript - // Select with lock - const user = await transaction.table('users') - .where('id', 1) - .append('FOR UPDATE') - .find(); - - // Shared lock - const user = await transaction.table('users') - .where('id', 1) - .append('LOCK IN SHARE MODE') - .find(); - ``` - -### Advanced Transaction Patterns - -1. **Nested Operations** - ```javascript - async function createUserWithProfile(userData, profileData) { - const transaction = new TransactionHandler(conn); - await transaction.begin(); - - try { - // Create user - const userResult = await transaction.table('users').insert(userData); - const userId = userResult.insertId; - - // Create profile - profileData.user_id = userId; - await transaction.table('profiles').insert(profileData); - - // Create default settings - await transaction.table('user_settings').insert({ - user_id: userId, - theme: 'light', - notifications: true - }); - - await transaction.commit(); - return userId; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - ``` - -2. **Batch Operations in Transaction** - ```javascript - async function batchUpdateUsers(updates) { - const transaction = new TransactionHandler(conn); - await transaction.begin(); - - try { - for (const update of updates) { - await transaction.table('users') - .where('id', update.id) - .update(update.data); - } - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - ``` - -## Hook System Patterns - -### Pre and Post Hooks - -1. **Operation Hooks** - ```javascript - const { Hook } = require('@axiosleo/orm-mysql'); - - // Pre-insert hook - Hook.pre(async (options, conn) => { - if (options.data && options.data.password) { - options.data.password = await hashPassword(options.data.password); - } - }, { table: 'users', opt: 'insert' }); - - // Post-select hook - Hook.post(async (options, result, conn) => { - if (Array.isArray(result)) { - result.forEach(row => { - delete row.password; // Remove sensitive data - }); - } - }, { table: 'users', opt: 'select' }); - ``` - -2. **Global Hooks** - ```javascript - // Hook for all operations - Hook.pre(async (options, conn) => { - console.log(`Executing ${options.operator} on ${options.tables[0].table}`); - }); - - // Hook for specific tables - Hook.post(async (options, result, conn) => { - // Log all operations on sensitive tables - if (['users', 'payments', 'orders'].includes(options.tables[0].table)) { - await logOperation(options, result); - } - }, { table: ['users', 'payments', 'orders'] }); - ``` - -3. **Custom Event Hooks** - ```javascript - // Register custom hook - Hook.register(async (options) => { - // Custom validation logic - await validateBusinessRules(options); - }, 'validation.before.insert'); - - // Trigger custom hook - Hook.trigger(['validation.before.insert'], options); - ``` - -## Migration Patterns - -### Schema Migrations - -1. **Table Creation** - ```javascript - function up(migration) { - migration.createTable('posts', { - id: { - type: 'int', - allowNull: false, - primaryKey: true, - autoIncrement: true - }, - title: { - type: 'varchar', - length: 255, - allowNull: false - }, - content: { - type: 'text', - allowNull: true - }, - user_id: { - type: 'int', - allowNull: false, - references: { - table: 'users', - column: 'id', - onDelete: 'CASCADE', - onUpdate: 'CASCADE' - } - }, - status: { - type: 'enum', - values: ['draft', 'published', 'archived'], - default: 'draft' - }, - created_at: { - type: 'timestamp', - allowNull: false, - default: 'CURRENT_TIMESTAMP' - }, - updated_at: { - type: 'timestamp', - allowNull: false, - default: 'CURRENT_TIMESTAMP', - onUpdate: 'CURRENT_TIMESTAMP' - } - }); - } - ``` - -2. **Index Management** - ```javascript - function up(migration) { - // Create indexes - migration.createIndex('posts', ['user_id']); - migration.createIndex('posts', ['status', 'created_at']); - migration.createIndex('posts', ['title'], { - indexName: 'idx_posts_title', - unique: false - }); - - // Full-text index - migration.createIndex('posts', ['title', 'content'], { - fulltext: true - }); - } - ``` - -3. **Data Migrations** - ```javascript - function up(migration) { - // Add new column - migration.createColumn('users', 'full_name', 'varchar', { - length: 255, - allowNull: true - }); - - // Migrate existing data - migration.insertData('users', { - sql: "UPDATE users SET full_name = CONCAT(first_name, ' ', last_name) WHERE first_name IS NOT NULL AND last_name IS NOT NULL" - }); - } - ``` - -### Migration Best Practices - -1. **Incremental Changes** - ```javascript - // Good: Small, focused migration - function up(migration) { - migration.createColumn('users', 'phone', 'varchar', { - length: 20, - allowNull: true - }); - } - - function down(migration) { - migration.dropColumn('users', 'phone'); - } - ``` - -2. **Safe Rollbacks** - ```javascript - function up(migration) { - // Always provide rollback - migration.createTable('user_preferences', { - id: { type: 'int', primaryKey: true, autoIncrement: true }, - user_id: { type: 'int', allowNull: false }, - key: { type: 'varchar', length: 100, allowNull: false }, - value: { type: 'text', allowNull: true } - }); - } - - function down(migration) { - migration.dropTable('user_preferences'); - } - ``` - -## Custom Query Drivers - -### Custom Driver Implementation - -1. **Driver Registration** - ```javascript - const customHandler = new QueryHandler(connection, { - driver: 'custom', - queryHandler: (conn, options) => { - const builder = new Builder(options); - - return new Promise((resolve, reject) => { - // Custom query execution logic - if (options.operator === 'select') { - // Custom select implementation - resolve(customSelectLogic(builder.sql, builder.values)); - } else { - reject(new Error('Unsupported operation')); - } - }); - } - }); - ``` - -2. **Driver with Caching** - ```javascript - const cachedQueryHandler = (conn, options) => { - const builder = new Builder(options); - const cacheKey = generateCacheKey(builder.sql, builder.values); - - return new Promise(async (resolve, reject) => { - try { - // Check cache first - const cached = await cache.get(cacheKey); - if (cached && options.operator === 'select') { - resolve(cached); - return; - } - - // Execute query - const result = await conn.query(builder.sql, builder.values); - - // Cache result for SELECT queries - if (options.operator === 'select') { - await cache.set(cacheKey, result, 300); // 5 minutes - } - - resolve(result); - } catch (error) { - reject(error); - } - }); - }; - ``` - -These patterns provide a comprehensive guide for using the @axiosleo/orm-mysql library effectively across different scenarios and use cases. diff --git a/.cursor/rules/coding-standards.md b/.cursor/rules/coding-standards.md deleted file mode 100644 index f2cec49..0000000 --- a/.cursor/rules/coding-standards.md +++ /dev/null @@ -1,560 +0,0 @@ -# Coding Standards for @axiosleo/orm-mysql - -## File Organization - -### Directory Structure -``` -src/ -├── builder.js # SQL query builders -├── client.js # Database connection management -├── core.js # Core query execution logic -├── hook.js # Event hooks system -├── migration.js # Database migration system -├── operator.js # Query operators and handlers -├── query.js # Query builder classes -├── transaction.js # Transaction management -└── utils.js # Utility functions - -tests/ # Test files -commands/ # CLI commands -examples/ # Usage examples -``` - -### File Naming -- Use kebab-case for file names: `query-builder.js` -- Use PascalCase for class files when appropriate -- Test files should end with `.tests.js` -- Example files should be descriptive: `basic-usage.js` - -## Code Style Guidelines - -### JavaScript Standards - -1. **Strict Mode** - ```javascript - 'use strict'; - ``` - -2. **Variable Declarations** - ```javascript - // Use const for immutable values - const connectionOptions = { host: 'localhost' }; - - // Use let for mutable values - let queryResult = null; - - // Avoid var - ``` - -3. **Function Declarations** - ```javascript - // Prefer async/await over promises - async function executeQuery(sql, params) { - try { - const result = await connection.query(sql, params); - return result; - } catch (error) { - throw new Error(`Query execution failed: ${error.message}`); - } - } - - // Use arrow functions for short callbacks - const users = results.map(row => ({ id: row.id, name: row.name })); - ``` - -4. **Object and Array Destructuring** - ```javascript - // Object destructuring - const { host, port, database } = connectionConfig; - - // Array destructuring - const [firstResult] = await query.select(); - ``` - -5. **Template Literals** - ```javascript - // Use template literals for string interpolation - const message = `Connected to database: ${database}`; - - // For SQL, use parameterized queries instead - const sql = 'SELECT * FROM users WHERE id = ?'; - ``` - -### Class Design - -1. **Class Structure** - ```javascript - class QueryBuilder { - constructor(connection, options = {}) { - this.connection = connection; - this.options = { ...defaultOptions, ...options }; - this._conditions = []; - this._tables = []; - } - - // Public methods - table(tableName, alias = null) { - this._tables.push({ table: tableName, alias }); - return this; - } - - // Private methods (prefix with _) - _buildConditions() { - return this._conditions.map(condition => this._formatCondition(condition)); - } - - _formatCondition(condition) { - // Implementation - } - } - ``` - -2. **Method Chaining** - ```javascript - // Enable fluent interface by returning 'this' - where(field, operator, value) { - this._conditions.push({ field, operator, value }); - return this; - } - - // Usage - const result = await query - .table('users') - .where('status', '=', 'active') - .where('age', '>', 18) - .select(); - ``` - -### Error Handling - -1. **Custom Error Classes** - ```javascript - class QueryError extends Error { - constructor(message, sql, params) { - super(message); - this.name = 'QueryError'; - this.sql = sql; - this.params = params; - } - } - ``` - -2. **Error Context** - ```javascript - try { - const result = await this._executeQuery(sql, params); - return result; - } catch (error) { - throw new QueryError( - `Failed to execute query: ${error.message}`, - sql, - params - ); - } - ``` - -3. **Input Validation** - ```javascript - validateTableName(tableName) { - if (!tableName || typeof tableName !== 'string') { - throw new Error('Table name must be a non-empty string'); - } - if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { - throw new Error('Invalid table name format'); - } - } - ``` - -## Documentation Standards - -### JSDoc Comments - -1. **Class Documentation** - ```javascript - /** - * Query builder for MySQL operations - * @class QueryBuilder - * @example - * const query = new QueryBuilder(connection); - * const users = await query.table('users').where('active', true).select(); - */ - class QueryBuilder { - ``` - -2. **Method Documentation** - ```javascript - /** - * Add a WHERE condition to the query - * @param {string} field - The field name - * @param {string|*} operator - The operator or value if operator is '=' - * @param {*} [value] - The value to compare against - * @returns {QueryBuilder} Returns this for method chaining - * @throws {Error} When field name is invalid - * @example - * query.where('name', 'John') - * query.where('age', '>', 18) - */ - where(field, operator, value) { - ``` - -3. **Type Definitions** - ```javascript - /** - * @typedef {Object} ConnectionOptions - * @property {string} host - Database host - * @property {number} port - Database port - * @property {string} user - Database user - * @property {string} password - Database password - * @property {string} database - Database name - */ - ``` - -### README and Examples - -1. **Clear Examples** - ```javascript - // Basic usage - const { createClient, QueryHandler } = require('@axiosleo/orm-mysql'); - - const client = createClient({ - host: 'localhost', - user: 'root', - password: 'password', - database: 'myapp' - }); - - const db = new QueryHandler(client); - - // Simple query - const users = await db.table('users') - .where('status', 'active') - .select(); - ``` - -2. **Migration Examples** - ```javascript - // Migration file: 20231201_create_users_table.js - function up(migration) { - migration.createTable('users', { - id: { - type: 'int', - allowNull: false, - primaryKey: true, - autoIncrement: true - }, - email: { - type: 'varchar', - length: 255, - allowNull: false, - uniqIndex: true - }, - created_at: { - type: 'timestamp', - allowNull: false, - default: 'CURRENT_TIMESTAMP' - } - }); - } - - function down(migration) { - migration.dropTable('users'); - } - - module.exports = { up, down }; - ``` - -## Testing Standards - -### Test Structure - -1. **Test Organization** - ```javascript - 'use strict'; - - const { expect } = require('chai'); - const { QueryBuilder } = require('../src/query-builder'); - - describe('QueryBuilder', () => { - let queryBuilder; - - beforeEach(() => { - queryBuilder = new QueryBuilder(mockConnection); - }); - - describe('#where()', () => { - it('should add simple where condition', () => { - queryBuilder.where('name', 'John'); - expect(queryBuilder._conditions).to.have.length(1); - }); - - it('should handle operator and value', () => { - queryBuilder.where('age', '>', 18); - const condition = queryBuilder._conditions[0]; - expect(condition.operator).to.equal('>'); - expect(condition.value).to.equal(18); - }); - }); - }); - ``` - -2. **Mock Objects** - ```javascript - const mockConnection = { - query: sinon.stub().resolves([{ id: 1, name: 'John' }]), - execute: sinon.stub().resolves([{ insertId: 1 }]) - }; - ``` - -3. **Integration Tests** - ```javascript - describe('Database Integration', () => { - let connection; - - before(async () => { - connection = await createTestConnection(); - await setupTestTables(connection); - }); - - after(async () => { - await cleanupTestTables(connection); - await connection.end(); - }); - - it('should perform CRUD operations', async () => { - const db = new QueryHandler(connection); - - // Create - const result = await db.table('test_users') - .insert({ name: 'Test User', email: 'test@example.com' }); - - expect(result.insertId).to.be.a('number'); - - // Read - const user = await db.table('test_users') - .where('id', result.insertId) - .find(); - - expect(user.name).to.equal('Test User'); - }); - }); - ``` - -## Performance Guidelines - -### Query Optimization - -1. **Use Appropriate Methods** - ```javascript - // Use find() for single records - const user = await db.table('users').where('id', 1).find(); - - // Use select() for multiple records - const users = await db.table('users').where('active', true).select(); - - // Use count() for counting - const userCount = await db.table('users').where('active', true).count(); - ``` - -2. **Limit Result Sets** - ```javascript - // Always use limit for large datasets - const recentUsers = await db.table('users') - .orderBy('created_at', 'desc') - .limit(100) - .select(); - ``` - -3. **Index Usage** - ```javascript - // Create indexes for frequently queried columns - migration.createIndex('users', ['email'], { unique: true }); - migration.createIndex('users', ['status', 'created_at']); - ``` - -### Connection Management - -1. **Connection Pooling** - ```javascript - // Use connection pools for production - const pool = createPool({ - host: 'localhost', - user: 'root', - password: 'password', - database: 'myapp', - connectionLimit: 10, - acquireTimeout: 60000, - timeout: 60000 - }); - ``` - -2. **Transaction Scope** - ```javascript - // Keep transactions short - async function transferFunds(fromId, toId, amount) { - const transaction = new TransactionHandler(connection); - await transaction.begin(); - - try { - await transaction.table('accounts') - .where('id', fromId) - .update({ balance: db.raw('balance - ?', [amount]) }); - - await transaction.table('accounts') - .where('id', toId) - .update({ balance: db.raw('balance + ?', [amount]) }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - ``` - -## Security Guidelines - -### SQL Injection Prevention - -1. **Parameterized Queries** - ```javascript - // Always use parameterized queries - const users = await db.table('users') - .where('email', userEmail) // Safe - .select(); - - // Never use string concatenation - // const sql = `SELECT * FROM users WHERE email = '${userEmail}'`; // DANGEROUS - ``` - -2. **Input Validation** - ```javascript - function validateEmail(email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - throw new Error('Invalid email format'); - } - } - - function validateId(id) { - const numericId = parseInt(id, 10); - if (!Number.isInteger(numericId) || numericId <= 0) { - throw new Error('Invalid ID'); - } - return numericId; - } - ``` - -### Data Sanitization - -1. **Output Encoding** - ```javascript - function sanitizeOutput(data) { - if (typeof data === 'string') { - return data.replace(/[<>&"']/g, (match) => { - const escapeMap = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - "'": ''' - }; - return escapeMap[match]; - }); - } - return data; - } - ``` - -## Migration Guidelines - -### Schema Changes - -1. **Incremental Migrations** - ```javascript - // Good: Small, focused changes - function up(migration) { - migration.createColumn('users', 'phone', 'varchar', { - length: 20, - allowNull: true - }); - } - - // Avoid: Large, complex changes in single migration - ``` - -2. **Data Migrations** - ```javascript - function up(migration) { - // Schema change first - migration.createColumn('users', 'full_name', 'varchar', { length: 255 }); - - // Data migration - migration.insertData('temp_update', [ - { sql: "UPDATE users SET full_name = CONCAT(first_name, ' ', last_name)" } - ]); - - // Cleanup old columns (in separate migration) - } - ``` - -3. **Rollback Support** - ```javascript - function up(migration) { - migration.createTable('new_table', { - id: { type: 'int', primaryKey: true, autoIncrement: true }, - name: { type: 'varchar', length: 255, allowNull: false } - }); - } - - function down(migration) { - migration.dropTable('new_table'); - } - ``` - -## CLI Development - -### Command Structure - -1. **Command Files** - ```javascript - // commands/generate.js - 'use strict'; - - const { Command } = require('@axiosleo/cli-tool'); - - class GenerateCommand extends Command { - constructor() { - super({ - name: 'generate', - desc: 'Generate migration file', - alias: 'gen' - }); - } - - exec(args, options, app) { - // Command implementation - } - } - - module.exports = GenerateCommand; - ``` - -2. **Help Documentation** - ```javascript - constructor() { - super({ - name: 'migrate', - desc: 'Run database migrations', - args: [ - { name: 'action', mode: 'required', desc: 'Migration action (up|down)' }, - { name: 'dir', desc: 'Migration directory path' } - ], - options: [ - { name: 'debug', alias: 'd', desc: 'Enable debug mode' }, - { name: 'host', desc: 'Database host', default: 'localhost' } - ] - }); - } - ``` - -These coding standards ensure consistency, maintainability, and reliability across the @axiosleo/orm-mysql project. diff --git a/.cursor/rules/development-guidelines.md b/.cursor/rules/development-guidelines.md deleted file mode 100644 index 3d9127e..0000000 --- a/.cursor/rules/development-guidelines.md +++ /dev/null @@ -1,326 +0,0 @@ -# @axiosleo/orm-mysql Development Guidelines - -## Project Overview - -This is a MySQL ORM (Object-Relational Mapping) library for Node.js that provides a fluent query builder, transaction support, migration system, and database management utilities. The library is designed to simplify MySQL database operations while maintaining flexibility and performance. - -## Architecture - -### Core Components - -1. **Query System** (`src/query.js`, `src/operator.js`) - - `Query`: Base query builder with fluent interface - - `QueryCondition`: Handles WHERE conditions and logical operators - - `QueryOperator`: Executes queries against database connections - - `QueryHandler`: Main entry point for database operations - -2. **SQL Builder** (`src/builder.js`) - - `Builder`: Converts query objects to SQL statements - - `ManageSQLBuilder`: Specialized builder for DDL operations (migrations) - -3. **Connection Management** (`src/client.js`) - - Multiple connection types: single connection, connection pool, promise-based - - Connection pooling and lifecycle management - -4. **Transaction Support** (`src/transaction.js`) - - `TransactionHandler`: Manages database transactions - - `TransactionOperator`: Query operations within transactions - - Support for different isolation levels - -5. **Migration System** (`src/migration.js`) - - Database schema versioning and migration management - - DDL operations: CREATE/DROP tables, columns, indexes, foreign keys - - Migration tracking and rollback support - -6. **Hook System** (`src/hook.js`) - - Pre and post-operation hooks for extensibility - - Event-driven architecture for custom logic - -## Development Standards - -### Code Style - -1. **Use strict mode** - All files must start with `'use strict';` - -2. **JSDoc Documentation** - All public methods and classes must have JSDoc comments: - ```javascript - /** - * @param {Connection} conn - Database connection - * @param {QueryOperatorOptions} options - Query options - * @returns {Promise} - */ - ``` - -3. **Error Handling** - Always use proper error handling: - ```javascript - try { - const result = await query.exec(); - return result; - } catch (error) { - // Log error with context - throw new Error(`Query failed: ${error.message}`); - } - ``` - -4. **Validation** - Use the built-in validation system for input validation: - ```javascript - const { _validate } = require('./utils'); - _validate(options, { - name: 'required|string', - type: 'required|string' - }); - ``` - -### Query Builder Patterns - -1. **Fluent Interface** - Chain methods for readability: - ```javascript - const result = await db.table('users') - .where('status', 'active') - .where('age', '>', 18) - .orderBy('created_at', 'desc') - .limit(10) - .select(); - ``` - -2. **Method Naming** - Use descriptive method names: - - `where()` for conditions - - `orderBy()` for sorting - - `limit()` and `offset()` for pagination - - `select()`, `find()`, `insert()`, `update()`, `delete()` for operations - -3. **Condition Building** - Support multiple condition formats: - ```javascript - // Object format - query.where({ name: 'John', age: 25 }); - - // Key-value format - query.where('name', 'John'); - - // Key-operator-value format - query.where('age', '>', 18); - ``` - -### Database Operations - -1. **Connection Management**: - ```javascript - // Use connection pools for production - const pool = createPool(connectionOptions); - - // Use single connections for simple operations - const conn = createClient(connectionOptions); - - // Use promise connections for transactions - const promiseConn = createPromiseClient(connectionOptions); - ``` - -2. **Transaction Patterns**: - ```javascript - const transaction = new TransactionHandler(conn); - await transaction.begin(); - - try { - await transaction.table('users').insert(userData); - await transaction.table('profiles').insert(profileData); - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - ``` - -3. **Migration Structure**: - ```javascript - function up(migration) { - migration.createTable('table_name', { - id: { - type: 'int', - allowNull: false, - primaryKey: true, - autoIncrement: true - }, - // ... other columns - }); - } - - function down(migration) { - migration.dropTable('table_name'); - } - ``` - -### Testing Guidelines - -1. **Test Structure** - Use Mocha with Chai for testing: - ```javascript - describe('Feature Name', () => { - before(async function() { - // Setup - }); - - it('should perform expected behavior', async () => { - // Test implementation - expect(result).to.equal(expectedValue); - }); - - after(async function() { - // Cleanup - }); - }); - ``` - -2. **Database Testing** - Use test databases and cleanup after tests - -3. **Coverage** - Maintain high test coverage using nyc - -### Security Considerations - -1. **SQL Injection Prevention** - Always use parameterized queries: - ```javascript - // Good - parameterized - query.where('id', userId); - - // Bad - string concatenation - query.where(`id = ${userId}`); - ``` - -2. **Input Validation** - Validate all inputs before processing - -3. **Connection Security** - Use secure connection options and credentials management - -### Performance Guidelines - -1. **Query Optimization**: - - Use appropriate indexes - - Limit result sets with `limit()` and `offset()` - - Use `find()` instead of `select()` for single records - -2. **Connection Pooling**: - - Use connection pools for high-traffic applications - - Configure appropriate pool sizes - -3. **Transaction Scope**: - - Keep transactions as short as possible - - Avoid long-running transactions - -### Error Handling Patterns - -1. **Consistent Error Messages** - Provide clear, actionable error messages - -2. **Error Context** - Include relevant context in error messages: - ```javascript - throw new Error(`Table '${tableName}' does not exist in database '${database}'`); - ``` - -3. **Error Logging** - Use the built-in printer for consistent logging: - ```javascript - const { printer } = require('@axiosleo/cli-tool'); - printer.error('Operation failed').println(); - ``` - -### Migration Best Practices - -1. **Incremental Changes** - Make small, incremental schema changes - -2. **Rollback Support** - Always implement `down()` functions for rollbacks - -3. **Data Migration** - Handle data migration carefully: - ```javascript - // Use insertData for data migrations - migration.insertData('table_name', [ - { column1: 'value1', column2: 'value2' } - ]); - ``` - -4. **Index Management** - Create indexes for frequently queried columns - -### CLI Development - -1. **Command Structure** - Follow the established pattern in `commands/` directory - -2. **Help Documentation** - Provide comprehensive help text for all commands - -3. **Validation** - Validate command arguments and options - -### TypeScript Support - -1. **Type Definitions** - Maintain accurate TypeScript definitions in `index.d.ts` - -2. **Generic Support** - Use generics for type-safe query results: - ```typescript - const users = await query.select(); - ``` - -### Debugging and Development - -1. **Debug Mode** - Use debug flags for verbose output: - ```javascript - const { debug } = require('@axiosleo/cli-tool'); - if (options.debug) { - debug.log('SQL:', builder.sql); - } - ``` - -2. **SQL Logging** - Enable SQL logging for development - -3. **Explain Queries** - Use `explain()` method for query analysis - -## Common Patterns - -### Repository Pattern -```javascript -class UserRepository { - constructor(db) { - this.db = db; - } - - async findById(id) { - return await this.db.table('users').where('id', id).find(); - } - - async create(userData) { - return await this.db.table('users').insert(userData); - } -} -``` - -### Service Layer -```javascript -class UserService { - constructor(userRepository) { - this.userRepository = userRepository; - } - - async createUser(userData) { - // Business logic - const user = await this.userRepository.create(userData); - // Additional processing - return user; - } -} -``` - -## Contribution Guidelines - -1. **Code Review** - All changes require code review -2. **Testing** - New features must include tests -3. **Documentation** - Update documentation for API changes -4. **Backward Compatibility** - Maintain backward compatibility when possible -5. **Performance** - Consider performance impact of changes - -## Troubleshooting - -### Common Issues - -1. **Connection Timeouts** - Check connection pool configuration -2. **Migration Failures** - Verify migration syntax and database permissions -3. **Query Performance** - Use `explain()` to analyze query execution plans -4. **Transaction Deadlocks** - Implement retry logic for deadlock scenarios - -### Debugging Steps - -1. Enable debug mode -2. Check SQL output -3. Verify database connectivity -4. Review error logs -5. Test with minimal reproduction case diff --git a/.cursor/rules/project-context.md b/.cursor/rules/project-context.md deleted file mode 100644 index be2d506..0000000 --- a/.cursor/rules/project-context.md +++ /dev/null @@ -1,306 +0,0 @@ -# Project Context for @axiosleo/orm-mysql - -## Project Overview - -**@axiosleo/orm-mysql** is a comprehensive MySQL ORM (Object-Relational Mapping) library for Node.js applications. It provides a fluent query builder interface, transaction management, database migrations, and connection pooling capabilities. - -### Key Features - -- **Fluent Query Builder**: Chainable methods for building complex SQL queries -- **Transaction Support**: Full transaction management with isolation levels -- **Migration System**: Database schema versioning and migration tools -- **Connection Management**: Support for single connections, connection pools, and promise-based connections -- **Hook System**: Pre and post-operation hooks for extensibility -- **CLI Tools**: Command-line interface for migrations and code generation -- **TypeScript Support**: Complete type definitions for type-safe development - -### Target Use Cases - -1. **Web Applications**: REST APIs and web services requiring database operations -2. **Microservices**: Database layer for microservice architectures -3. **Data Migration**: Tools for database schema and data migrations -4. **Enterprise Applications**: Large-scale applications requiring robust database management - -## Technical Architecture - -### Core Components - -``` -@axiosleo/orm-mysql -├── Query System -│ ├── Query Builder (fluent interface) -│ ├── Condition Builder (WHERE clauses) -│ └── SQL Builder (query compilation) -├── Connection Layer -│ ├── Single Connections -│ ├── Connection Pools -│ └── Promise-based Connections -├── Transaction Management -│ ├── Transaction Handler -│ ├── Isolation Levels -│ └── Rollback Support -├── Migration System -│ ├── Schema Migrations -│ ├── Data Migrations -│ └── Version Control -└── Extension Points - ├── Hook System - ├── Custom Drivers - └── Event System -``` - -### Dependencies - -- **mysql2**: MySQL client for Node.js (primary database driver) -- **@axiosleo/cli-tool**: CLI framework and utilities -- **validatorjs**: Input validation library - -### Development Dependencies - -- **mocha**: Testing framework -- **chai**: Assertion library -- **nyc**: Code coverage tool -- **eslint**: Code linting -- **typescript**: Type checking and definitions - -## Database Support - -### MySQL Versions - -- **MySQL 5.7+**: Full feature support -- **MySQL 8.0+**: Enhanced JSON support and performance optimizations -- **MariaDB 10.2+**: Compatible with most features - -### SQL Features Supported - -1. **Basic Operations**: SELECT, INSERT, UPDATE, DELETE -2. **Advanced Queries**: JOINs, subqueries, CTEs (MySQL 8.0+) -3. **JSON Operations**: JSON_EXTRACT, JSON_CONTAINS, JSON_OVERLAPS -4. **Aggregations**: COUNT, SUM, AVG, GROUP BY, HAVING -5. **Indexing**: Primary keys, unique indexes, composite indexes, full-text indexes -6. **Constraints**: Foreign keys, check constraints -7. **Transactions**: All isolation levels, savepoints - -## Project Structure - -### Source Code Organization - -``` -src/ -├── builder.js # SQL query builders -├── client.js # Database connection management -├── core.js # Core query execution logic -├── hook.js # Event hooks system -├── migration.js # Database migration system -├── operator.js # Query operators and handlers -├── query.js # Query builder classes -├── transaction.js # Transaction management -└── utils.js # Utility functions -``` - -### Supporting Files - -``` -commands/ # CLI command implementations -├── generate.js # Migration generation -└── migrate.js # Migration execution - -tests/ # Test suites -├── builder.tests.js # Query builder tests -├── client.tests.js # Connection tests -├── hook.tests.js # Hook system tests -├── operator.tests.js # Operator tests -├── query.tests.js # Query tests -└── utils.tests.js # Utility tests - -examples/ # Usage examples -├── base_column.js # Column definitions -└── migration/ # Migration examples -``` - -## Development Workflow - -### Code Organization Principles - -1. **Separation of Concerns**: Each module has a specific responsibility -2. **Fluent Interface**: Chainable methods for better developer experience -3. **Error Handling**: Comprehensive error handling with context -4. **Type Safety**: TypeScript definitions for all public APIs -5. **Testing**: High test coverage with unit and integration tests - -### API Design Philosophy - -1. **Intuitive**: Methods should be self-explanatory -2. **Consistent**: Similar operations should have similar interfaces -3. **Flexible**: Support multiple ways to achieve the same result -4. **Safe**: Prevent SQL injection and common security issues -5. **Performant**: Optimize for common use cases - -### Extension Points - -1. **Custom Drivers**: Implement custom query execution logic -2. **Hook System**: Add custom logic before/after operations -3. **Migration Extensions**: Custom migration operations -4. **Connection Middleware**: Custom connection handling - -## Common Usage Patterns - -### Basic CRUD Operations - -```javascript -const { createClient, QueryHandler } = require('@axiosleo/orm-mysql'); - -// Setup -const client = createClient(connectionOptions); -const db = new QueryHandler(client); - -// Create -await db.table('users').insert({ name: 'John', email: 'john@example.com' }); - -// Read -const users = await db.table('users').where('status', 'active').select(); -const user = await db.table('users').where('id', 1).find(); - -// Update -await db.table('users').where('id', 1).update({ name: 'John Doe' }); - -// Delete -await db.table('users').where('id', 1).delete(); -``` - -### Transaction Management - -```javascript -const { TransactionHandler, createPromiseClient } = require('@axiosleo/orm-mysql'); - -const conn = await createPromiseClient(connectionOptions); -const transaction = new TransactionHandler(conn); - -await transaction.begin(); -try { - await transaction.table('users').insert(userData); - await transaction.table('profiles').insert(profileData); - await transaction.commit(); -} catch (error) { - await transaction.rollback(); - throw error; -} -``` - -### Migration System - -```javascript -// Migration file -function up(migration) { - migration.createTable('posts', { - id: { type: 'int', primaryKey: true, autoIncrement: true }, - title: { type: 'varchar', length: 255, allowNull: false }, - content: { type: 'text' }, - user_id: { - type: 'int', - references: { table: 'users', column: 'id' } - } - }); -} - -function down(migration) { - migration.dropTable('posts'); -} -``` - -## Performance Considerations - -### Query Optimization - -1. **Use Appropriate Methods**: `find()` for single records, `select()` for multiple -2. **Limit Result Sets**: Always use `limit()` for large datasets -3. **Index Usage**: Create indexes for frequently queried columns -4. **Join Optimization**: Use appropriate join types and conditions - -### Connection Management - -1. **Connection Pooling**: Use pools for high-concurrency applications -2. **Connection Lifecycle**: Properly close connections to prevent leaks -3. **Pool Configuration**: Tune pool size based on application needs - -### Transaction Best Practices - -1. **Short Transactions**: Keep transaction scope minimal -2. **Deadlock Handling**: Implement retry logic for deadlock scenarios -3. **Isolation Levels**: Choose appropriate isolation levels for use case - -## Security Features - -### SQL Injection Prevention - -- **Parameterized Queries**: All user inputs are parameterized -- **Input Validation**: Built-in validation for common data types -- **Escape Mechanisms**: Proper escaping of special characters - -### Data Protection - -- **Connection Security**: Support for SSL/TLS connections -- **Credential Management**: Secure handling of database credentials -- **Audit Hooks**: Ability to log all database operations - -## Testing Strategy - -### Test Categories - -1. **Unit Tests**: Individual component testing -2. **Integration Tests**: Database interaction testing -3. **Performance Tests**: Query performance benchmarks -4. **Security Tests**: SQL injection and security validation - -### Test Environment - -- **Test Databases**: Isolated test database instances -- **Mock Objects**: Mocked connections for unit tests -- **Coverage Reports**: Comprehensive code coverage analysis - -## Deployment Considerations - -### Production Setup - -1. **Connection Pooling**: Configure appropriate pool sizes -2. **Error Logging**: Implement comprehensive error logging -3. **Performance Monitoring**: Monitor query performance and connection usage -4. **Migration Management**: Automated migration deployment - -### Environment Configuration - -1. **Environment Variables**: Use environment variables for configuration -2. **Configuration Files**: Support for multiple environment configurations -3. **Secret Management**: Secure handling of database credentials - -## Community and Ecosystem - -### Package Ecosystem - -- **NPM Package**: Published as `@axiosleo/orm-mysql` -- **GitHub Repository**: Open source with issue tracking -- **Documentation**: Comprehensive README and examples - -### Contribution Guidelines - -1. **Code Standards**: Follow established coding standards -2. **Testing Requirements**: All changes must include tests -3. **Documentation**: Update documentation for API changes -4. **Backward Compatibility**: Maintain compatibility when possible - -## Future Roadmap - -### Planned Features - -1. **Query Caching**: Built-in query result caching -2. **Read Replicas**: Support for read/write splitting -3. **Schema Validation**: Runtime schema validation -4. **Performance Analytics**: Built-in query performance analysis - -### Version Strategy - -- **Semantic Versioning**: Follow semver for releases -- **LTS Support**: Long-term support for major versions -- **Migration Path**: Clear upgrade paths between versions - -This project context provides the foundation for understanding and contributing to the @axiosleo/orm-mysql library. From 75727591835ab38343fe9d5675afbd9e4712c8a4 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:18:46 +0800 Subject: [PATCH 3/7] docs(README): add detailed transaction management examples and best practices for using connection pools and isolation levels --- README.md | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ef78e7a..af8fab5 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,53 @@ Hook.post(async (options, result) => { ### Transaction +#### Method 1: Using Connection Pool (Recommended) + +```javascript +const mysql = require("mysql2"); +const { QueryHandler } = require("@axiosleo/orm-mysql"); + +// Create connection pool +const pool = mysql.createPool({ + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASS, + database: process.env.MYSQL_DB, + connectionLimit: 10 +}); + +const handler = new QueryHandler(pool); + +// Begin transaction - automatically gets a connection from pool +const transaction = await handler.beginTransaction({ + level: "RC" // READ COMMITTED +}); + +try { + // Insert user info + let row = await transaction.table("users").insert({ + name: "Joe", + age: 18, + }); + const lastInsertId = row.insertId; + + // Insert student info + await transaction.table("students").insert({ + user_id: lastInsertId, + }); + + // Commit transaction - connection automatically released back to pool + await transaction.commit(); +} catch (e) { + // Rollback transaction - connection automatically released back to pool + await transaction.rollback(); + throw e; +} +``` + +#### Method 2: Using TransactionHandler Directly + ```javascript const { TransactionHandler, createPromiseClient } = require("@axiosleo/orm-mysql"); @@ -190,25 +237,25 @@ const conn = await createPromiseClient({ const transaction = new TransactionHandler(conn, { /* - level = 'READ UNCOMMITTED' | 'RU' - | 'READ COMMITTED' | 'RC' - | 'REPEATABLE READ' | 'RR' - | 'SERIALIZABLE' | 'S' + Transaction Isolation Levels: + - 'READ UNCOMMITTED' | 'RU' : Lowest isolation, may read dirty data + - 'READ COMMITTED' | 'RC' : Prevents dirty reads + - 'REPEATABLE READ' | 'RR' : MySQL default, prevents non-repeatable reads + - 'SERIALIZABLE' | 'S' : Highest isolation, full serialization */ level: "SERIALIZABLE", // 'SERIALIZABLE' as default value }); await transaction.begin(); try { - // insert user info - // will not really create a record. + // Insert user info let row = await transaction.table("users").insert({ name: "Joe", age: 18, }); const lastInsertId = row[0].insertId; - // insert student info + // Insert student info await transaction.table("students").insert({ user_id: lastInsertId, }); @@ -219,6 +266,95 @@ try { } ``` +#### Row Locking with FOR UPDATE + +```javascript +const transaction = await handler.beginTransaction({ level: "RC" }); + +try { + // Lock rows for update + const product = await transaction.table("products") + .where("sku", "LAPTOP-001") + .append("FOR UPDATE") // Lock the row + .find(); + + if (product.stock < 1) { + throw new Error("Out of stock"); + } + + // Update stock + await transaction.table("products") + .where("sku", "LAPTOP-001") + .update({ stock: product.stock - 1 }); + + // Create order + await transaction.table("orders").insert({ + product_id: product.id, + quantity: 1, + total: product.price + }); + + await transaction.commit(); +} catch (e) { + await transaction.rollback(); + throw e; +} +``` + +#### Concurrent Transactions + +When using a connection pool, multiple transactions can run concurrently without blocking each other: + +```javascript +const pool = mysql.createPool({ /* ... */ }); +const handler = new QueryHandler(pool); + +// Run 3 transactions concurrently +await Promise.all([ + (async () => { + const tx = await handler.beginTransaction(); + try { + await tx.table("users").insert({ name: "User1" }); + await tx.commit(); + } catch (err) { + await tx.rollback(); + throw err; + } + })(), + + (async () => { + const tx = await handler.beginTransaction(); + try { + await tx.table("users").insert({ name: "User2" }); + await tx.commit(); + } catch (err) { + await tx.rollback(); + throw err; + } + })(), + + (async () => { + const tx = await handler.beginTransaction(); + try { + await tx.table("users").insert({ name: "User3" }); + await tx.commit(); + } catch (err) { + await tx.rollback(); + throw err; + } + })() +]); +``` + +#### Best Practices + +1. **Always use connection pools in production** - Prevents connection exhaustion and enables concurrent transactions +2. **Choose appropriate isolation level** - Balance between consistency and performance +3. **Use try-catch-finally** - Ensure transactions are always committed or rolled back +4. **Keep transactions short** - Avoid long-running operations inside transactions +5. **Use row locking when needed** - `FOR UPDATE` prevents concurrent modifications +6. **Handle errors properly** - Always rollback on errors to maintain data consistency + ### Migration > [Migration examples](./examples/migration/). From a49c0ae28ba9380ba50624ab458e212fbafe5081 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:28:16 +0800 Subject: [PATCH 4/7] feat(tests): add feature tests with MySQL container setup and transaction scenarios --- .github/workflows/ci.yml | 2 +- .github/workflows/feature-test.yml | 60 ++++ docker-compose.yml | 18 ++ package.json | 3 + tests/init-feature-tables.sql | 73 +++++ tests/setup-feature-db.js | 99 ++++++ tests/transaction.ft.js | 497 +++++++++++++++++++++++++++++ 7 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/feature-test.yml create mode 100644 docker-compose.yml create mode 100644 tests/init-feature-tables.sql create mode 100644 tests/setup-feature-db.js create mode 100644 tests/transaction.ft.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5045b2..f973447 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: operating-system: [macos-latest, ubuntu-latest] - node-version: [16, 18, 20] + node-version: [16, 18, 20, 22, 24] name: Node.js ${{ matrix.node-version }} Test on ${{ matrix.operating-system }} steps: diff --git a/.github/workflows/feature-test.yml b/.github/workflows/feature-test.yml new file mode 100644 index 0000000..1606eb1 --- /dev/null +++ b/.github/workflows/feature-test.yml @@ -0,0 +1,60 @@ +# This workflow runs feature tests with MySQL container +name: Feature Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + feature-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20] + + name: Node.js ${{ matrix.node-version }} Feature Tests + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Start MySQL container + run: docker-compose up -d + + - name: Wait for MySQL to be ready + run: | + echo "Waiting for MySQL to be ready..." + timeout 60 sh -c 'until docker-compose exec -T mysql mysqladmin ping -h localhost -uroot -p3AQqZTfmww=Ftj --silent; do echo "Waiting..."; sleep 2; done' + echo "MySQL is ready!" + + - name: Setup test database + run: npm run setup-feature-db + env: + MYSQL_HOST: localhost + MYSQL_PORT: 3306 + MYSQL_USER: root + MYSQL_PASS: 3AQqZTfmww=Ftj + MYSQL_DB: feature_tests + + - name: Run feature tests + run: npm run feature-test + env: + MYSQL_HOST: localhost + MYSQL_PORT: 3306 + MYSQL_USER: root + MYSQL_PASS: 3AQqZTfmww=Ftj + MYSQL_DB: feature_tests + + - name: Cleanup + if: always() + run: docker-compose down -v + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5cfdbe8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' +# Settings and configurations that are common for all containers +services: + mysql: + image: 'mysql:8.0' + container_name: "orm-feature-tests-mysql" + command: mysqld --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + environment: + MYSQL_ROOT_PASSWORD: '3AQqZTfmww=Ftj' + MYSQL_DATABASE: 'feature_tests' + restart: 'always' + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p3AQqZTfmww=Ftj"] + interval: 5s + timeout: 3s + retries: 10 diff --git a/package.json b/package.json index 667ccdd..6511457 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "test": "mocha --reporter spec --timeout 3000 tests/*.tests.js", "test-cov": "nyc -r=lcov -r=html -r=text -r=json mocha -t 10000 -R spec tests/*.tests.js", "test-one": "mocha --reporter spec --timeout 3000 ", + "feature-test": "node tests/transaction.ft.js", + "setup-feature-db": "node tests/setup-feature-db.js", + "feature-test:local": "docker-compose up -d && sleep 10 && npm run setup-feature-db && npm run feature-test && docker-compose down", "ci": "npm run lint && npm run test-cov", "clear": "rm -rf ./nyc_output ./coverage && rm -rf ./node_modules && npm cache clean --force" }, diff --git a/tests/init-feature-tables.sql b/tests/init-feature-tables.sql new file mode 100644 index 0000000..16b20b0 --- /dev/null +++ b/tests/init-feature-tables.sql @@ -0,0 +1,73 @@ +-- 创建测试数据库 +CREATE DATABASE IF NOT EXISTS feature_tests CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE feature_tests; + +-- 用户表 +DROP TABLE IF EXISTS users; +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE, + balance DECIMAL(10, 2) DEFAULT 0.00, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 产品表 +DROP TABLE IF EXISTS products; +CREATE TABLE products ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + sku VARCHAR(50) NOT NULL UNIQUE, + price DECIMAL(10, 2) NOT NULL, + stock INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 订单表 +DROP TABLE IF EXISTS orders; +CREATE TABLE orders ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + total DECIMAL(10, 2) NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (product_id) REFERENCES products(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 账户表(用于测试转账) +DROP TABLE IF EXISTS accounts; +CREATE TABLE accounts ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + account_number VARCHAR(50) NOT NULL UNIQUE, + balance DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 插入测试数据 +INSERT INTO users (name, email, balance) VALUES + ('Alice', 'alice@example.com', 1000.00), + ('Bob', 'bob@example.com', 2000.00), + ('Charlie', 'charlie@example.com', 1500.00); + +INSERT INTO products (name, sku, price, stock) VALUES + ('Laptop', 'LAPTOP-001', 999.99, 10), + ('Mouse', 'MOUSE-001', 29.99, 50), + ('Keyboard', 'KEYBOARD-001', 79.99, 30), + ('Monitor', 'MONITOR-001', 299.99, 15), + ('Headphones', 'HEADPHONES-001', 149.99, 25); + +INSERT INTO accounts (user_id, account_number, balance) VALUES + (1, 'ACC-1001', 5000.00), + (2, 'ACC-1002', 3000.00), + (3, 'ACC-1003', 4000.00); + diff --git a/tests/setup-feature-db.js b/tests/setup-feature-db.js new file mode 100644 index 0000000..34d6f2b --- /dev/null +++ b/tests/setup-feature-db.js @@ -0,0 +1,99 @@ +/* eslint-disable no-console */ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const mysql = require('mysql2/promise'); + +async function setupTestDatabase() { + console.log('=== Setting up feature test database ===\n'); + + // Configuration from environment variables + const config = { + host: process.env.MYSQL_HOST || 'localhost', + port: parseInt(process.env.MYSQL_PORT || '3306'), + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASS || '3AQqZTfmww=Ftj', + multipleStatements: true + }; + + console.log('Connection config:'); + console.log(` Host: ${config.host}`); + console.log(` Port: ${config.port}`); + console.log(` User: ${config.user}`); + console.log(` Database: ${process.env.MYSQL_DB || 'feature_tests'}\n`); + + let connection; + + try { + // Connect to MySQL + console.log('Connecting to MySQL...'); + connection = await mysql.createConnection(config); + console.log('✓ Connected successfully\n'); + + // Read SQL file + const sqlFile = path.join(__dirname, 'init-feature-tables.sql'); + console.log(`Reading SQL file: ${sqlFile}`); + + if (!fs.existsSync(sqlFile)) { + throw new Error(`SQL file not found: ${sqlFile}`); + } + + const sql = fs.readFileSync(sqlFile, 'utf8'); + console.log('✓ SQL file read successfully\n'); + + // Execute SQL + console.log('Executing SQL statements...'); + await connection.query(sql); + console.log('✓ Database and tables created successfully\n'); + + // Verify tables + console.log('Verifying table structure...'); + const [tables] = await connection.query( + 'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?', + [process.env.MYSQL_DB || 'feature_tests'] + ); + + console.log('\nCreated tables:'); + tables.forEach(row => { + console.log(` - ${row.TABLE_NAME}`); + }); + + // Verify data + console.log('\nVerifying test data...'); + await connection.query(`USE ${process.env.MYSQL_DB || 'feature_tests'}`); + + const [users] = await connection.query('SELECT COUNT(*) as count FROM users'); + const [products] = await connection.query('SELECT COUNT(*) as count FROM products'); + const [accounts] = await connection.query('SELECT COUNT(*) as count FROM accounts'); + + console.log(` users table: ${users[0].count} records`); + console.log(` products table: ${products[0].count} records`); + console.log(` accounts table: ${accounts[0].count} records`); + + console.log('\n=== Database setup completed ===\n'); + console.log('✓ All test data is ready!'); + console.log('✓ You can now run feature tests\n'); + + } catch (err) { + console.error('\n✗ Error:', err.message); + console.error('Details:', err); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + console.log('Database connection closed'); + } + } +} + +// Run if executed directly +if (require.main === module) { + setupTestDatabase().catch(err => { + console.error('Fatal error:', err); + process.exit(1); + }); +} + +module.exports = { setupTestDatabase }; + diff --git a/tests/transaction.ft.js b/tests/transaction.ft.js new file mode 100644 index 0000000..84b231b --- /dev/null +++ b/tests/transaction.ft.js @@ -0,0 +1,497 @@ +/* eslint-disable no-console */ +'use strict'; + +const mysql = require('mysql2'); +const { QueryHandler } = require('../src/operator'); + +// 配置 - 使用环境变量(CI 友好) +const config = { + host: process.env.MYSQL_HOST || 'localhost', + port: parseInt(process.env.MYSQL_PORT || '3306'), + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASS || '3AQqZTfmww=Ftj', + database: process.env.MYSQL_DB || 'feature_tests', + connectionLimit: 10 +}; + +// 颜色输出辅助函数 +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m' +}; + +function log(message, color = '') { + console.log(`${color}${message}${colors.reset}`); +} + +function success(message) { + log(`✓ ${message}`, colors.green); +} + +function error(message) { + log(`✗ ${message}`, colors.red); +} + +function info(message) { + log(`ℹ ${message}`, colors.cyan); +} + +function section(message) { + log(`\n${'='.repeat(60)}`, colors.bright); + log(`${message}`, colors.bright); + log(`${'='.repeat(60)}`, colors.bright); +} + +// 测试场景 1: 连接池事务基本操作 +async function test1_basicPoolTransaction() { + section('测试场景 1: 连接池事务基本操作'); + + const pool = mysql.createPool(config); + const queryHandler = new QueryHandler(pool); + + try { + info('创建连接池...'); + success('连接池创建成功'); + + info('开始事务...'); + const transaction = await queryHandler.beginTransaction({ level: 'RC' }); + success('事务已开始(隔离级别: READ COMMITTED)'); + + info('插入测试用户...'); + const result = await transaction.table('users').insert({ + name: 'Test User 1', + email: `test1_${Date.now()}@example.com`, + balance: 100.00 + }); + success(`用户插入成功,ID: ${result.insertId}`); + + info('提交事务...'); + await transaction.commit(); + success('事务提交成功,连接已自动释放回池'); + + // 验证数据 + info('验证插入的数据...'); + const user = await queryHandler.table('users') + .where('id', result.insertId) + .find(); + + if (user && user.name === 'Test User 1') { + success('数据验证成功'); + } else { + error('数据验证失败'); + } + + success('✓ 测试场景 1 完成\n'); + } catch (err) { + error(`测试失败: ${err.message}`); + throw err; + } finally { + await pool.end(); + } +} + +// 测试场景 2: 并发事务不阻塞 +async function test2_concurrentTransactions() { + section('测试场景 2: 并发事务不阻塞'); + + const pool = mysql.createPool(config); + const queryHandler = new QueryHandler(pool); + + try { + info('创建连接池(连接数限制: 10)...'); + success('连接池创建成功'); + + info('同时启动 3 个并发事务...'); + const startTime = Date.now(); + + const transactions = await Promise.all([ + // 事务 1 + (async () => { + const tx = await queryHandler.beginTransaction({ level: 'RC' }); + try { + await tx.table('users').insert({ + name: 'Concurrent User 1', + email: `concurrent1_${Date.now()}@example.com`, + balance: 200.00 + }); + // 模拟耗时操作 + await new Promise(resolve => setTimeout(resolve, 100)); + await tx.commit(); + return '事务 1 完成'; + } catch (err) { + await tx.rollback(); + throw err; + } + })(), + + // 事务 2 + (async () => { + const tx = await queryHandler.beginTransaction({ level: 'RC' }); + try { + await tx.table('users').insert({ + name: 'Concurrent User 2', + email: `concurrent2_${Date.now()}@example.com`, + balance: 300.00 + }); + await new Promise(resolve => setTimeout(resolve, 100)); + await tx.commit(); + return '事务 2 完成'; + } catch (err) { + await tx.rollback(); + throw err; + } + })(), + + // 事务 3 + (async () => { + const tx = await queryHandler.beginTransaction({ level: 'RC' }); + try { + await tx.table('users').insert({ + name: 'Concurrent User 3', + email: `concurrent3_${Date.now()}@example.com`, + balance: 400.00 + }); + await new Promise(resolve => setTimeout(resolve, 100)); + await tx.commit(); + return '事务 3 完成'; + } catch (err) { + await tx.rollback(); + throw err; + } + })() + ]); + + const endTime = Date.now(); + const duration = endTime - startTime; + + transactions.forEach(result => success(result)); + success(`所有事务完成,总耗时: ${duration}ms`); + + if (duration < 300) { + success('✓ 事务并发执行成功(未阻塞)'); + } else { + error('✗ 事务可能串行执行(存在阻塞)'); + } + + success('✓ 测试场景 2 完成\n'); + } catch (err) { + error(`测试失败: ${err.message}`); + throw err; + } finally { + await pool.end(); + } +} + +// 测试场景 3: 事务回滚 +async function test3_rollback() { + section('测试场景 3: 事务回滚'); + + const pool = mysql.createPool(config); + const queryHandler = new QueryHandler(pool); + + try { + info('开始事务...'); + const transaction = await queryHandler.beginTransaction({ level: 'RC' }); + const testEmail = `rollback_test_${Date.now()}@example.com`; + + try { + info('插入测试数据...'); + await transaction.table('users').insert({ + name: 'Rollback Test User', + email: testEmail, + balance: 500.00 + }); + success('数据插入成功(未提交)'); + + info('故意抛出错误以触发回滚...'); + throw new Error('故意的错误'); + } catch (err) { + info(`捕获错误: ${err.message}`); + info('执行回滚...'); + await transaction.rollback(); + success('事务已回滚,连接已释放'); + } + + // 验证数据未插入 + info('验证数据是否已回滚...'); + const user = await queryHandler.table('users') + .where('email', testEmail) + .find(); + + if (!user) { + success('✓ 数据已成功回滚(未插入到数据库)'); + } else { + error('✗ 回滚失败,数据仍然存在'); + } + + success('✓ 测试场景 3 完成\n'); + } catch (err) { + error(`测试失败: ${err.message}`); + throw err; + } finally { + await pool.end(); + } +} + +// 测试场景 4: 库存扣减场景(行锁) +async function test4_stockDeduction() { + section('测试场景 4: 库存扣减场景(使用行锁)'); + + const pool = mysql.createPool(config); + const queryHandler = new QueryHandler(pool); + + try { + info('开始事务...'); + const transaction = await queryHandler.beginTransaction({ level: 'RC' }); + + try { + const productSku = 'LAPTOP-001'; + + info(`查询产品 ${productSku} 并锁定行(FOR UPDATE)...`); + const product = await transaction.table('products') + .where('sku', productSku) + .append('FOR UPDATE') + .find(); + + if (!product) { + throw new Error('产品不存在'); + } + + success(`产品: ${product.name}, 当前库存: ${product.stock}`); + + if (product.stock < 2) { + throw new Error('库存不足'); + } + + info('扣减库存...'); + await transaction.table('products') + .where('sku', productSku) + .update({ stock: product.stock - 2 }); + success('库存扣减成功(-2)'); + + info('创建订单...'); + const orderResult = await transaction.table('orders').insert({ + user_id: 1, + product_id: product.id, + quantity: 2, + total: product.price * 2, + status: 'completed' + }); + success(`订单创建成功,订单ID: ${orderResult.insertId}`); + + info('提交事务...'); + await transaction.commit(); + success('事务提交成功'); + + // 验证库存 + info('验证库存更新...'); + const updatedProduct = await queryHandler.table('products') + .where('sku', productSku) + .find(); + success(`当前库存: ${updatedProduct.stock} (应为 ${product.stock - 2})`); + + if (updatedProduct.stock === product.stock - 2) { + success('✓ 库存扣减验证成功'); + } else { + error('✗ 库存扣减验证失败'); + } + + success('✓ 测试场景 4 完成\n'); + } catch (err) { + await transaction.rollback(); + error(`事务回滚: ${err.message}`); + throw err; + } + } catch (err) { + error(`测试失败: ${err.message}`); + throw err; + } finally { + await pool.end(); + } +} + +// 测试场景 5: 转账场景(多表事务) +async function test5_transfer() { + section('测试场景 5: 转账场景(多表事务)'); + + const pool = mysql.createPool(config); + const queryHandler = new QueryHandler(pool); + + try { + const fromAccount = 'ACC-1001'; + const toAccount = 'ACC-1002'; + const amount = 100.00; + + info('开始事务...'); + const transaction = await queryHandler.beginTransaction({ level: 'RR' }); + + try { + // 查询转出账户 + info(`查询转出账户 ${fromAccount}...`); + const fromAcc = await transaction.table('accounts') + .where('account_number', fromAccount) + .append('FOR UPDATE') + .find(); + + if (!fromAcc) { + throw new Error('转出账户不存在'); + } + + success(`转出账户余额: ${fromAcc.balance}`); + + // 查询转入账户 + info(`查询转入账户 ${toAccount}...`); + const toAcc = await transaction.table('accounts') + .where('account_number', toAccount) + .append('FOR UPDATE') + .find(); + + if (!toAcc) { + throw new Error('转入账户不存在'); + } + + success(`转入账户余额: ${toAcc.balance}`); + + // 检查余额 + if (parseFloat(fromAcc.balance) < amount) { + throw new Error('余额不足'); + } + + const totalBefore = parseFloat(fromAcc.balance) + parseFloat(toAcc.balance); + info(`转账前总额: ${totalBefore.toFixed(2)}`); + + // 扣款 + info(`从账户 ${fromAccount} 扣除 ${amount}...`); + await transaction.table('accounts') + .where('account_number', fromAccount) + .update({ balance: parseFloat(fromAcc.balance) - amount }); + success('扣款成功'); + + // 加款 + info(`向账户 ${toAccount} 增加 ${amount}...`); + await transaction.table('accounts') + .where('account_number', toAccount) + .update({ balance: parseFloat(toAcc.balance) + amount }); + success('加款成功'); + + info('提交事务...'); + await transaction.commit(); + success('转账事务提交成功'); + + // 验证 + info('验证转账结果...'); + const verifyFrom = await queryHandler.table('accounts') + .where('account_number', fromAccount) + .find(); + const verifyTo = await queryHandler.table('accounts') + .where('account_number', toAccount) + .find(); + + const totalAfter = parseFloat(verifyFrom.balance) + parseFloat(verifyTo.balance); + + success(`${fromAccount} 余额: ${verifyFrom.balance}`); + success(`${toAccount} 余额: ${verifyTo.balance}`); + success(`转账后总额: ${totalAfter.toFixed(2)}`); + + if (Math.abs(totalBefore - totalAfter) < 0.01) { + success('✓ 总额验证成功(转账前后总额一致)'); + } else { + error('✗ 总额验证失败'); + } + + success('✓ 测试场景 5 完成\n'); + } catch (err) { + await transaction.rollback(); + error(`事务回滚: ${err.message}`); + throw err; + } + } catch (err) { + error(`测试失败: ${err.message}`); + throw err; + } finally { + await pool.end(); + } +} + +// 主测试函数 +async function runAllTests() { + console.log('\n'); + log('╔═══════════════════════════════════════════════════════════╗', colors.bright + colors.blue); + log('║ MySQL ORM Transaction Feature Tests ║', colors.bright + colors.blue); + log('╚═══════════════════════════════════════════════════════════╝', colors.bright + colors.blue); + console.log('\n'); + + const tests = [ + { name: '测试场景 1: 连接池事务基本操作', fn: test1_basicPoolTransaction }, + { name: '测试场景 2: 并发事务不阻塞', fn: test2_concurrentTransactions }, + { name: '测试场景 3: 事务回滚', fn: test3_rollback }, + { name: '测试场景 4: 库存扣减场景(行锁)', fn: test4_stockDeduction }, + { name: '测试场景 5: 转账场景(多表事务)', fn: test5_transfer } + ]; + + let passed = 0; + let failed = 0; + + for (const test of tests) { + try { + await test.fn(); + passed++; + } catch (err) { + failed++; + error(`${test.name} 失败`); + console.error(err); + } + } + + // 总结 + console.log('\n'); + log('╔═══════════════════════════════════════════════════════════╗', colors.bright); + log('║ 测试总结 ║', colors.bright); + log('╚═══════════════════════════════════════════════════════════╝', colors.bright); + console.log('\n'); + + log(`总测试数: ${tests.length}`, colors.bright); + log(`通过: ${passed}`, colors.green); + log(`失败: ${failed}`, failed > 0 ? colors.red : colors.green); + + if (failed === 0) { + console.log('\n'); + success('🎉 所有测试通过!'); + success('✓ 连接池事务功能正常'); + success('✓ 连接自动获取和释放机制正常'); + success('✓ 并发事务不会相互阻塞'); + } else { + console.log('\n'); + error(`⚠️ 有 ${failed} 个测试失败`); + } + + console.log('\n'); + + // 失败时退出码非零 + if (failed > 0) { + process.exit(1); + } +} + +// 运行测试 +if (require.main === module) { + runAllTests().catch(err => { + console.error('Fatal error:', err); + process.exit(1); + }); +} + +module.exports = { + test1_basicPoolTransaction, + test2_concurrentTransactions, + test3_rollback, + test4_stockDeduction, + test5_transfer, + runAllTests +}; + From 33dfbae35a17b37a5fe9b713c0e199fb5a70033b Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:30:56 +0800 Subject: [PATCH 5/7] chore(ci): update MySQL container commands in feature test workflow to use 'docker compose' syntax --- .github/workflows/feature-test.yml | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/feature-test.yml b/.github/workflows/feature-test.yml index 1606eb1..b774506 100644 --- a/.github/workflows/feature-test.yml +++ b/.github/workflows/feature-test.yml @@ -13,29 +13,29 @@ jobs: strategy: matrix: node-version: [18, 20] - + name: Node.js ${{ matrix.node-version }} Feature Tests - + steps: - uses: actions/checkout@v4 - + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - + - name: Install dependencies run: npm install - + - name: Start MySQL container - run: docker-compose up -d - + run: docker compose up -d + - name: Wait for MySQL to be ready run: | echo "Waiting for MySQL to be ready..." - timeout 60 sh -c 'until docker-compose exec -T mysql mysqladmin ping -h localhost -uroot -p3AQqZTfmww=Ftj --silent; do echo "Waiting..."; sleep 2; done' + timeout 60 sh -c 'until docker compose exec -T mysql mysqladmin ping -h localhost -uroot -p3AQqZTfmww=Ftj --silent; do echo "Waiting..."; sleep 2; done' echo "MySQL is ready!" - + - name: Setup test database run: npm run setup-feature-db env: @@ -44,7 +44,7 @@ jobs: MYSQL_USER: root MYSQL_PASS: 3AQqZTfmww=Ftj MYSQL_DB: feature_tests - + - name: Run feature tests run: npm run feature-test env: @@ -53,8 +53,7 @@ jobs: MYSQL_USER: root MYSQL_PASS: 3AQqZTfmww=Ftj MYSQL_DB: feature_tests - + - name: Cleanup if: always() - run: docker-compose down -v - + run: docker compose down -v From fb5543aa46cf05e3cc45f7e1928ce717ac9e3e3d Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:31:27 +0800 Subject: [PATCH 6/7] chore(package): update feature test command to use 'docker compose' syntax --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6511457..d404a46 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test-one": "mocha --reporter spec --timeout 3000 ", "feature-test": "node tests/transaction.ft.js", "setup-feature-db": "node tests/setup-feature-db.js", - "feature-test:local": "docker-compose up -d && sleep 10 && npm run setup-feature-db && npm run feature-test && docker-compose down", + "feature-test:local": "docker compose up -d && sleep 10 && npm run setup-feature-db && npm run feature-test && docker compose down", "ci": "npm run lint && npm run test-cov", "clear": "rm -rf ./nyc_output ./coverage && rm -rf ./node_modules && npm cache clean --force" }, From 4d595779fe0f05c898c48ab6f86a632f7033c501 Mon Sep 17 00:00:00 2001 From: axiosleo Date: Fri, 19 Dec 2025 15:35:37 +0800 Subject: [PATCH 7/7] fix(ci): improve MySQL readiness check in feature test workflow to include retry attempts --- .github/workflows/feature-test.yml | 13 +++++++++++-- tests/setup-feature-db.js | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/feature-test.yml b/.github/workflows/feature-test.yml index b774506..ca26dba 100644 --- a/.github/workflows/feature-test.yml +++ b/.github/workflows/feature-test.yml @@ -33,8 +33,17 @@ jobs: - name: Wait for MySQL to be ready run: | echo "Waiting for MySQL to be ready..." - timeout 60 sh -c 'until docker compose exec -T mysql mysqladmin ping -h localhost -uroot -p3AQqZTfmww=Ftj --silent; do echo "Waiting..."; sleep 2; done' - echo "MySQL is ready!" + for i in {1..30}; do + if docker compose exec -T mysql mysqladmin ping -h localhost -uroot -p3AQqZTfmww=Ftj --silent 2>/dev/null; then + echo "MySQL is ready!" + sleep 2 + exit 0 + fi + echo "Attempt $i/30: MySQL not ready yet, waiting..." + sleep 2 + done + echo "MySQL failed to become ready in time" + exit 1 - name: Setup test database run: npm run setup-feature-db diff --git a/tests/setup-feature-db.js b/tests/setup-feature-db.js index 34d6f2b..15215c4 100644 --- a/tests/setup-feature-db.js +++ b/tests/setup-feature-db.js @@ -26,10 +26,21 @@ async function setupTestDatabase() { let connection; try { - // Connect to MySQL + // Connect to MySQL with retry console.log('Connecting to MySQL...'); - connection = await mysql.createConnection(config); - console.log('✓ Connected successfully\n'); + let retries = 5; + while (retries > 0) { + try { + connection = await mysql.createConnection(config); + console.log('✓ Connected successfully\n'); + break; + } catch (err) { + retries--; + if (retries === 0) throw err; + console.log(`Connection failed, retrying... (${retries} attempts left)`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } + } // Read SQL file const sqlFile = path.join(__dirname, 'init-feature-tables.sql');