Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions packages/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@ 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:
build_runner: ^2.4.0
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
172 changes: 72 additions & 100 deletions packages/jao/lib/src/db/adapters/sqlite.dart
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;

Expand All @@ -103,20 +103,13 @@ class SqliteConnection implements DatabaseConnection {
static Future<SqliteConnection> 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);
}
Expand All @@ -127,53 +120,33 @@ class SqliteConnection implements DatabaseConnection {
@override
Future<QueryResult> execute(String sql, [List<Object?>? 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 = <Map<String, dynamic>>[];
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 = <String, dynamic>{};
for (final column in columns) {
map[column] = _convertValue(row[column]);
}
rows.add(map);
}
final rows = <Map<String, dynamic>>[];
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 = <String, dynamic>{};
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<QueryResult> executeUpdate(String sql, [List<Object?>? 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);
}
Expand All @@ -196,13 +169,12 @@ class SqliteConnection implements DatabaseConnection {

@override
Future<SqliteTransaction> beginTransaction() async {
_db.execute('BEGIN TRANSACTION');
return SqliteTransaction._(_db, _path);
}

@override
Future<void> close() async {
_db.close();
await _db.close();
_isOpen = false;
}

Expand Down Expand Up @@ -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);

Expand All @@ -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 = <Map<String, dynamic>>[];
final columns = result.columnNames;

for (final row in result) {
final map = <String, dynamic>{};
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<void> 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<void> rollback() async {
if (!_isActive) return;
_db.execute('ROLLBACK');
// sqlite_async handles rollback automatically on error
// Just clear pending operations
_pendingOperations.clear();
_isActive = false;
}

Expand All @@ -323,11 +289,18 @@ class SqliteTransaction implements Transaction {
}
}

/// Pending operation in a transaction.
class _PendingOperation {
final String sql;
final List<Object?>? 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;
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -515,7 +487,7 @@ class SqliteAdapter implements DatabaseAdapter {
@override
Future<List<String>> 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
''');
Expand All @@ -526,7 +498,7 @@ class SqliteAdapter implements DatabaseAdapter {
Future<bool> 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],
Expand Down
2 changes: 1 addition & 1 deletion packages/jao/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading