diff --git a/packages/example/pubspec.yaml b/packages/example/pubspec.yaml index b35772a..8b29bc6 100644 --- a/packages/example/pubspec.yaml +++ b/packages/example/pubspec.yaml @@ -6,11 +6,15 @@ publish_to: none environment: sdk: '>=3.6.0 <4.0.0' -resolution: workspace +# resolution: workspace dependencies: dart_frog: ^1.2.0 - jao: ^0.2.2 + jao: + git: + url: https://github.com/nexlabstudio/jao.git + ref: no-build-hooks + path: packages/jao jao_cli: ^0.2.0 dev_dependencies: @@ -18,3 +22,10 @@ dev_dependencies: jao_generator: ^0.2.1 test: ^1.24.0 lints: ^6.0.0 + +dependency_overrides: + jao: + git: + url: https://github.com/nexlabstudio/jao.git + ref: no-build-hooks + path: packages/jao diff --git a/packages/jao/lib/src/db/adapters/sqlite.dart b/packages/jao/lib/src/db/adapters/sqlite.dart index a0b87c8..9cfbe04 100644 --- a/packages/jao/lib/src/db/adapters/sqlite.dart +++ b/packages/jao/lib/src/db/adapters/sqlite.dart @@ -1,12 +1,12 @@ /// SQLite database adapter. /// /// Provides SQLite-specific SQL generation and database operations -/// using the `sqlite3` package. +/// using the `sqlite_async` package for async operations. library; import 'dart:async'; import 'dart:io'; -import 'package:sqlite3/sqlite3.dart' as sqlite; +import 'package:sqlite_async/sqlite_async.dart' as sqlite_async; import '../connection.dart'; /// SQLite SQL dialect. @@ -88,9 +88,9 @@ class SqliteDialect implements SqlDialect { } } -/// SQLite database connection implementation. +/// SQLite database connection implementation using sqlite_async. class SqliteConnection implements DatabaseConnection { - final sqlite.Database _db; + final sqlite_async.SqliteDatabase _db; final String _path; bool _isOpen = true; @@ -103,20 +103,13 @@ class SqliteConnection implements DatabaseConnection { static Future connect(DatabaseConfig config) async { final path = config.database; - // Handle in-memory database - if (path == ':memory:') { - final db = sqlite.sqlite3.openInMemory(); - return SqliteConnection._(db, path); - } - - // Open file-based database - final db = sqlite.sqlite3.open(path); + final db = sqlite_async.SqliteDatabase(path: path); // Enable foreign keys - db.execute('PRAGMA foreign_keys = ON'); + await db.execute('PRAGMA foreign_keys = ON'); - // Enable WAL mode for better concurrency - db.execute('PRAGMA journal_mode = DELETE'); + // Use DELETE journal mode (simpler than WAL for compatibility) + await db.execute('PRAGMA journal_mode = DELETE'); return SqliteConnection._(db, path); } @@ -127,53 +120,33 @@ class SqliteConnection implements DatabaseConnection { @override Future execute(String sql, [List? params]) async { try { - // Convert positional params to SQLite format final convertedSql = _convertPlaceholders(sql); final convertedParams = _convertParams(params); - final stmt = _db.prepare(convertedSql); - try { - final result = stmt.select(convertedParams); + final result = await _db.execute(convertedSql, convertedParams); - final rows = >[]; - final columns = result.columnNames; + // Get affected rows and last insert ID + final changesResult = await _db.get('SELECT changes() as changes, last_insert_rowid() as last_id'); + final affectedRows = changesResult['changes'] as int? ?? 0; + final lastInsertId = changesResult['last_id'] as int?; - for (final row in result) { - final map = {}; - for (final column in columns) { - map[column] = _convertValue(row[column]); - } - rows.add(map); - } + final rows = >[]; + final columns = result.columnNames; - return QueryResult( - rows: rows, - columns: columns, - affectedRows: _db.updatedRows, - lastInsertId: _db.lastInsertRowId, - ); - } finally { - stmt.close(); + for (final row in result) { + final map = {}; + for (final column in columns) { + map[column] = _convertValue(row[column]); + } + rows.add(map); } - } catch (e) { - throw SqliteException('Query execution failed: $e', sql: sql, database: _path); - } - } - - /// Execute without returning results (for INSERT/UPDATE/DELETE). - Future executeUpdate(String sql, [List? params]) async { - try { - final convertedSql = _convertPlaceholders(sql); - final convertedParams = _convertParams(params); - - final stmt = _db.prepare(convertedSql); - try { - stmt.execute(convertedParams); - return QueryResult(affectedRows: _db.updatedRows, lastInsertId: _db.lastInsertRowId); - } finally { - stmt.close(); - } + return QueryResult( + rows: rows, + columns: columns, + affectedRows: affectedRows, + lastInsertId: lastInsertId, + ); } catch (e) { throw SqliteException('Query execution failed: $e', sql: sql, database: _path); } @@ -196,13 +169,12 @@ class SqliteConnection implements DatabaseConnection { @override Future beginTransaction() async { - _db.execute('BEGIN TRANSACTION'); return SqliteTransaction._(_db, _path); } @override Future close() async { - _db.close(); + await _db.close(); _isOpen = false; } @@ -240,11 +212,16 @@ class SqliteConnection implements DatabaseConnection { } } -/// SQLite transaction implementation. +/// SQLite transaction implementation using sqlite_async. +/// +/// Note: sqlite_async handles transactions differently - it uses +/// writeTransaction() for atomic operations. This class wraps that +/// behavior to match the Transaction interface. class SqliteTransaction implements Transaction { - final sqlite.Database _db; + final sqlite_async.SqliteDatabase _db; final String _path; bool _isActive = true; + final List<_PendingOperation> _pendingOperations = []; SqliteTransaction._(this._db, this._path); @@ -257,50 +234,39 @@ class SqliteTransaction implements Transaction { throw StateError('Transaction is no longer active'); } - try { - final convertedSql = _convertPlaceholders(sql); - final convertedParams = _convertParams(params); - - final stmt = _db.prepare(convertedSql); - try { - final result = stmt.select(convertedParams); - - final rows = >[]; - final columns = result.columnNames; - - for (final row in result) { - final map = {}; - for (final column in columns) { - map[column] = row[column]; - } - rows.add(map); - } + // Queue the operation - it will be executed on commit + _pendingOperations.add(_PendingOperation(sql, params)); - return QueryResult( - rows: rows, - columns: columns, - affectedRows: _db.updatedRows, - lastInsertId: _db.lastInsertRowId, - ); - } finally { - stmt.close(); - } - } catch (e) { - throw SqliteException('Transaction query failed: $e', sql: sql, database: _path); - } + // Return a placeholder result - actual result comes after commit + return QueryResult(); } @override Future commit() async { if (!_isActive) return; - _db.execute('COMMIT'); - _isActive = false; + + try { + await _db.writeTransaction((tx) async { + for (final op in _pendingOperations) { + final convertedSql = _convertPlaceholders(op.sql); + final convertedParams = _convertParams(op.params); + await tx.execute(convertedSql, convertedParams); + } + }); + } catch (e) { + throw SqliteException('Transaction commit failed: $e', database: _path); + } finally { + _isActive = false; + _pendingOperations.clear(); + } } @override Future rollback() async { if (!_isActive) return; - _db.execute('ROLLBACK'); + // sqlite_async handles rollback automatically on error + // Just clear pending operations + _pendingOperations.clear(); _isActive = false; } @@ -323,11 +289,18 @@ class SqliteTransaction implements Transaction { } } +/// Pending operation in a transaction. +class _PendingOperation { + final String sql; + final List? params; + + _PendingOperation(this.sql, this.params); +} + /// SQLite connection pool implementation. /// -/// Note: SQLite is typically single-connection, but we provide a pool -/// interface for API consistency. For WAL mode, multiple readers are -/// supported but only one writer at a time. +/// Note: sqlite_async handles concurrency internally with write queuing, +/// so this pool is simpler than traditional connection pools. class SqliteConnectionPool implements ConnectionPool { final DatabaseConfig _config; SqliteConnection? _connection; @@ -480,12 +453,11 @@ class SqliteAdapter implements DatabaseAdapter { return; // Nothing to create for in-memory } - // Creating a SQLite database just means creating the file + // Creating a SQLite database just means opening and closing it final file = File(config.database); if (!file.existsSync()) { - // Open and close to create the file - final db = sqlite.sqlite3.open(config.database); - db.close(); + final db = sqlite_async.SqliteDatabase(path: config.database); + await db.close(); } } @@ -515,7 +487,7 @@ class SqliteAdapter implements DatabaseAdapter { @override Future> getTables(DatabaseConnection conn) async { final result = await conn.query(''' - SELECT name FROM sqlite_master + SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name '''); @@ -526,7 +498,7 @@ class SqliteAdapter implements DatabaseAdapter { Future tableExists(DatabaseConnection conn, String table) async { final result = await conn.query( ''' - SELECT COUNT(*) as count FROM sqlite_master + SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name=? ''', [table], diff --git a/packages/jao/pubspec.yaml b/packages/jao/pubspec.yaml index 9c68747..6da67b5 100644 --- a/packages/jao/pubspec.yaml +++ b/packages/jao/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # Database drivers postgres: ^3.1.0 mysql1: ^0.20.0 - sqlite3: ^3.1.1 + sqlite_async: ^0.11.0 dev_dependencies: coverage: ^1.15.0 diff --git a/pubspec.lock b/pubspec.lock index 4b1b77a..feb5e0d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,14 +129,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" code_builder: dependency: transitive description: @@ -249,14 +241,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" hotreloader: dependency: transitive description: @@ -353,22 +337,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mysql1: + mutex: dependency: transitive description: - name: mysql1 - sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9" + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" url: "https://pub.dev" source: hosted - version: "0.20.0" - native_toolchain_c: + version: "3.1.0" + mysql1: dependency: transitive description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + name: mysql1 + sha256: "68aec7003d2abc85769bafa1777af3f4a390a90c31032b89636758ff8eb839e9" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "0.20.0" node_preamble: dependency: transitive description: @@ -501,10 +485,26 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "1d2d2afee96acccabf315e169a4d28019fa21d5217ee014840058a7fbc2008c6" + sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "2.9.4" + sqlite3_web: + dependency: transitive + description: + name: sqlite3_web + sha256: "0f6ebcb4992d1892ac5c8b5ecd22a458ab9c5eb6428b11ae5ecb5d63545844da" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + sqlite_async: + dependency: transitive + description: + name: sqlite_async + sha256: "4da3b035bcce83d10beae879c6539e75230826bba95ed440565b6afbdd8b1550" + url: "https://pub.dev" + source: hosted + version: "0.11.8" stack_trace: dependency: transitive description: @@ -634,4 +634,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.999 <4.0.0" + dart: ">=3.9.0 <4.0.0"