Laconic
A Laravel-style SQL query builder for Dart, supporting MySQL, SQLite, and PostgreSQL databases. Provides a fluent, chainable API for elegant database queries.
Features
- Laravel-style API - Familiar query builder syntax with 57 methods covering ~75% of Laravel Query Builder's core functionality
- Multi-database Support - Support for MySQL, SQLite, and PostgreSQL
- Complete JOIN Support - INNER, LEFT, RIGHT, CROSS JOIN with comprehensive condition methods
- Chainable Methods - Fluent query building experience
- Parameterized Queries - Automatic SQL injection prevention
- Transaction Support - Complete transaction management
- Query Listener - Built-in debugging and logging
Installation
Add the dependency to your pubspec.yaml:
dependencies:
laconic: ^1.0.0
Then run:
dart pub get
Quick Start
Database Connection
MySQL
import 'package:laconic/laconic.dart';
var config = MysqlConfig(
database: 'my_database',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: 'password',
);
var laconic = Laconic.mysql(config);
SQLite
import 'package:laconic/laconic.dart';
var config = SqliteConfig('database.db');
var laconic = Laconic.sqlite(config);
PostgreSQL
import 'package:laconic/laconic.dart';
var config = PostgresqlConfig(
database: 'my_database',
host: '127.0.0.1',
port: 5432,
username: 'postgres',
password: 'password',
);
var laconic = Laconic.postgresql(config);
Query Listener (for debugging)
var laconic = Laconic.mysql(
config,
listen: (query) {
print('SQL: ${query.sql}');
print('Bindings: ${query.bindings}');
},
);
Basic Usage
Raw SQL Queries
// SELECT query
var users = await laconic.select('SELECT * FROM users WHERE age > ?', [18]);
// INSERT/UPDATE/DELETE statements
await laconic.statement(
'INSERT INTO users (name, age) VALUES (?, ?)',
['John', 25],
);
Query Builder
Basic Queries
// Get all records
var users = await laconic.table('users').get();
// Get first record
var user = await laconic.table('users').first();
// Select specific columns
var names = await laconic.table('users').select(['name', 'age']).get();
// Count records
var count = await laconic.table('users').count();
// Check if records exist
var exists = await laconic.table('users').where('id', 1).exists();
WHERE Clauses
// Basic where
var adults = await laconic.table('users')
.where('age', 18, comparator: '>=')
.get();
// Multiple conditions (AND)
var results = await laconic.table('users')
.where('age', 18, comparator: '>')
.where('status', 'active')
.get();
// OR conditions
var users = await laconic.table('users')
.where('role', 'admin')
.orWhere('role', 'moderator')
.get();
// WHERE IN
var users = await laconic.table('users')
.whereIn('id', [1, 2, 3])
.get();
// WHERE NOT IN
var users = await laconic.table('users')
.whereNotIn('status', ['banned', 'deleted'])
.get();
// WHERE NULL / NOT NULL
var usersWithEmail = await laconic.table('users')
.whereNotNull('email')
.get();
var usersWithoutEmail = await laconic.table('users')
.whereNull('email')
.get();
// WHERE BETWEEN
var users = await laconic.table('users')
.whereBetween('age', min: 18, max: 30)
.get();
// WHERE NOT BETWEEN
var users = await laconic.table('users')
.whereNotBetween('age', min: 18, max: 30)
.get();
// Column comparison
var users = await laconic.table('users')
.whereColumn('created_at', 'updated_at', operator: '<')
.get();
// All columns must match
var users = await laconic.table('users')
.whereAll(['name', 'email'], '%john%', operator: 'like')
.get();
// Any column can match
var users = await laconic.table('users')
.whereAny(['name', 'email', 'phone'], 'john', operator: 'like')
.get();
// No columns should match
var users = await laconic.table('users')
.whereNone(['name', 'email'], '%spam%', operator: 'like')
.get();
JOIN Operations
// INNER JOIN
var results = await laconic.table('users u')
.select(['u.name', 'p.title'])
.join('posts p', (join) => join.on('u.id', 'p.user_id'))
.get();
// LEFT JOIN
var results = await laconic.table('users u')
.select(['u.name', 'p.title'])
.leftJoin('posts p', (join) => join.on('u.id', 'p.user_id'))
.get();
// RIGHT JOIN
var results = await laconic.table('users u')
.select(['u.name', 'p.title'])
.rightJoin('posts p', (join) => join.on('u.id', 'p.user_id'))
.get();
// CROSS JOIN
var results = await laconic.table('users u')
.select(['u.name', 'p.title'])
.crossJoin('posts p')
.get();
// Complex JOIN conditions
var results = await laconic.table('users u')
.select(['u.name', 'p.title'])
.join(
'posts p',
(join) => join
.on('u.id', 'p.user_id')
.whereIn('p.status', ['published', 'draft'])
.whereNotNull('p.content'),
)
.get();
// Advanced JOIN conditions
var results = await laconic.table('users u')
.select(['u.name', 'p.title'])
.leftJoin(
'posts p',
(join) => join
.on('u.id', 'p.user_id')
.orOn('u.email', 'p.author_email')
.where('p.status', 'published')
.orWhere('p.featured', true),
)
.get();
Ordering, Grouping, and Pagination
// Order by
var users = await laconic.table('users')
.orderBy('name')
.orderBy('age', direction: 'desc')
.get();
// Group by
var counts = await laconic.table('posts')
.select(['user_id'])
.groupBy('user_id')
.having('user_id', 1, operator: '>')
.get();
// Distinct
var ages = await laconic.table('users')
.select(['age'])
.distinct()
.get();
// Pagination
var users = await laconic.table('users')
.limit(10)
.offset(20)
.get();
Aggregate Functions
// Average
var avgAge = await laconic.table('users').avg('age');
// Sum
var totalAge = await laconic.table('users').sum('age');
// Max
var maxAge = await laconic.table('users').max('age');
// Min
var minAge = await laconic.table('users').min('age');
// Aggregates with conditions
var avgMaleAge = await laconic.table('users')
.where('gender', 'male')
.avg('age');
Insert Operations
// Insert single record
await laconic.table('users').insert([
{'name': 'John', 'age': 25, 'gender': 'male'},
]);
// Insert multiple records
await laconic.table('users').insert([
{'name': 'Jane', 'age': 30, 'gender': 'female'},
{'name': 'Bob', 'age': 28, 'gender': 'male'},
]);
// Insert and get ID
var id = await laconic.table('users').insertGetId({
'name': 'Alice',
'age': 22,
'gender': 'female',
});
print('New user ID: $id');
Update Operations
// Basic update
await laconic.table('users')
.where('id', 1)
.update({'name': 'New Name'});
// Batch update
await laconic.table('users')
.where('status', 'pending')
.update({'status': 'active'});
// Increment
await laconic.table('users')
.where('id', 1)
.increment('login_count');
// Increment with amount
await laconic.table('users')
.where('id', 1)
.increment('points', amount: 10);
// Increment with extra columns
await laconic.table('users')
.where('id', 1)
.increment(
'age',
extra: {'updated_at': DateTime.now().toIso8601String()},
);
// Decrement
await laconic.table('users')
.where('id', 1)
.decrement('balance', amount: 100);
Delete Operations
// Delete with condition
await laconic.table('users')
.where('id', 99)
.delete();
// Batch delete
await laconic.table('users')
.where('status', 'inactive')
.delete();
Utility Methods
// pluck - Get array of column values
var names = await laconic.table('users').pluck('name') as List<Object?>;
// pluck - Get key-value map
var idNameMap = await laconic.table('users').pluck('name', key: 'id')
as Map<Object?, Object?>;
// value - Get single value
var name = await laconic.table('users')
.where('id', 1)
.value('name');
// addSelect - Add columns to existing select
var users = await laconic.table('users')
.select(['name'])
.addSelect(['age', 'email'])
.get();
// when - Conditional query building
var role = 'admin';
var users = await laconic.table('users')
.when(
role == 'admin',
(query) => query.where('is_admin', true),
otherwise: (query) => query.where('is_active', true),
)
.get();
// sole - Ensure exactly one result
try {
var user = await laconic.table('users')
.where('email', 'unique@example.com')
.sole();
} catch (e) {
print('Result not unique or does not exist');
}
Transactions
try {
await laconic.transaction(() async {
// Insert user
var userId = await laconic.table('users').insertGetId({
'name': 'Test User',
'age': 30,
});
// Insert related data
await laconic.table('posts').insert([
{'user_id': userId, 'title': 'First Post'},
]);
// If any operation fails, the entire transaction will be rolled back
});
print('Transaction successful');
} catch (e) {
print('Transaction failed: $e');
}
Close Connection
// Always close the connection when done
await laconic.close();
Architecture Overview
Core Components
- Laconic - Main entry point, manages database connections
- QueryBuilder - Fluent query builder
- Grammar - SQL generation core
- SqlGrammar: MySQL/SQLite implementation (uses
?placeholders) - PostgresqlGrammar: PostgreSQL implementation (uses
$1, $2, ...placeholders)
- SqlGrammar: MySQL/SQLite implementation (uses
- JoinClause - JOIN condition builder
Design Patterns
- Grammar Pattern: QueryBuilder collects query components, Grammar compiles them into concrete SQL
- Parameter Binding: All queries use parameterized bindings to prevent SQL injection
- Lazy Connection: Database connections are established on first use
Dependencies
mysql_client: ^0.0.27- MySQL connectivitysqlite3: ^2.7.5- SQLite supportpostgres: ^3.5.5- PostgreSQL support
Testing
# Run all tests
dart test
# Run database-specific tests
dart test test/sqlite_test.dart
dart test test/mysql_test.dart
dart test test/postgresql_test.dart
# Start Docker containers for MySQL/PostgreSQL testing
docker-compose up -d
dart test
docker-compose down
License
MIT License
Contributing
Issues and Pull Requests are welcome!