From 892bd1f496436989205ccf3a0c2ca50b8ad21db1 Mon Sep 17 00:00:00 2001 From: vento007 Date: Fri, 3 Oct 2025 21:19:39 +0700 Subject: [PATCH] docs(readme): new docs --- CHANGELOG.md | 6 + NEW_README.md | 1218 ++++++++++++++++++ OLD_README.md | 770 +++++++++++ README.md | 1491 ++++++++++++++-------- example/complete_crud_example.dart | 311 +++++ example/flutter_example/pubspec.lock | 50 +- example/flutter_example/pubspec.yaml | 3 +- example/model_serialization_example.dart | 95 ++ example/readme_intro_example.dart | 15 + example/remote_api_example.dart | 160 +++ example/use_case_1_local_db.dart | 35 + example/use_case_2_api_query.dart | 51 + example/weather_api_example.dart | 321 +++++ pubspec.yaml | 3 +- 14 files changed, 3996 insertions(+), 533 deletions(-) create mode 100644 NEW_README.md create mode 100644 OLD_README.md create mode 100644 example/complete_crud_example.dart create mode 100644 example/model_serialization_example.dart create mode 100644 example/readme_intro_example.dart create mode 100644 example/remote_api_example.dart create mode 100644 example/use_case_1_local_db.dart create mode 100644 example/use_case_2_api_query.dart create mode 100644 example/weather_api_example.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3635c95..423723c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.9.2 + +### Changed +- Updated README with cleaner formatting (removed icons) +- Added more Dart example demos + ## 0.9.1+1 ### Fixed diff --git a/NEW_README.md b/NEW_README.md new file mode 100644 index 0000000..178d3c8 --- /dev/null +++ b/NEW_README.md @@ -0,0 +1,1218 @@ +
+ +

+ Tiny DB ASCII banner +

+
+ +```dart +import 'package:tiny_db/tiny_db.dart'; + +final db = TinyDb(MemoryStorage()); + +await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC'}); +await db.insert({'name': 'Bob', 'age': 25, 'city': 'NYC'}); + +final nycUsers = await db.defaultTable.search(where('city').equals('NYC')); +print(nycUsers.length); // 2 +``` + +
+ +

tiny_db — instant queryable JSON database

+ +

Turn any JSON into a queryable database - no schema, no setup, instant results

+ +

+ + Pub + + + License: MIT + + + Dart Version + + Platform Support + + Open Issues + + + Pull Requests + + + Contributors + + Last Commit +

+ +
+ +
+ +## The Magic + +### Use Case 1: Clean Local Database + +```dart +import 'package:tiny_db/tiny_db.dart'; + +final db = TinyDb(MemoryStorage()); + +// Just add your data - plain maps, no schema needed +await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC', 'role': 'developer'}); +await db.insert({'name': 'Bob', 'age': 25, 'city': 'SF', 'role': 'designer'}); +await db.insert({'name': 'Charlie', 'age': 35, 'city': 'NYC', 'role': 'developer'}); + +// Query it like a real database +final nycDevs = await db.defaultTable.search( + where('city').equals('NYC').and(where('role').equals('developer')) +); + +print('NYC developers: ${nycDevs.length}'); // 2 + +for (final dev in nycDevs) { + print('${dev['name']}, age ${dev['age']}'); +} +// Alice, age 30 +// Charlie, age 35 +``` + +**Clean. Simple. No schema.** + +> **Try it:** [example/use_case_1_local_db.dart](example/use_case_1_local_db.dart) + +### Use Case 2: Fetch API Data & Query Instantly + +```dart +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:tiny_db/tiny_db.dart'; + +final db = TinyDb(MemoryStorage()); + +// Fetch JSON from any API +final response = await http.get( + Uri.parse('https://api.github.com/users/vento007/repos') +); +final repos = jsonDecode(response.body) as List; + +// Make it queryable +for (final repo in repos) { + await db.insert(repo); +} + +// Query nested data, complex conditions - instantly +final dartPackages = await db.defaultTable.search( + where('language').equals('Dart').and(where('stargazers_count').greaterThan(3)) +); + +print('Found ${dartPackages.length} popular Dart packages!'); +for (final pkg in dartPackages) { + print('${pkg['name']}: ${pkg['stargazers_count']} ⭐'); +} +// flexible_tree_layout: 6 ⭐ +// riverpod_mvcs_example: 5 ⭐ +// tiny_db: 4 ⭐ +``` + +**Fetch → Insert → Query. Any JSON source becomes a database.** + +> **Try it:** [example/use_case_2_api_query.dart](example/use_case_2_api_query.dart) + +## What is tiny_db? + +A lightweight, document-oriented NoSQL database for Dart & Flutter. Think of it as **SQLite for JSON** - powerful queries without the SQL. + +- 🚀 **Instant Queries**: Any JSON → queryable in seconds (APIs, files, anywhere) +- 🎯 **No Schema**: Works with any JSON structure - dynamic and flexible +- 💪 **20+ Query Operators**: Nested paths, regex, logical operators, list operations +- 💾 **Dual Storage**: In-memory (fast) or persistent JSON files (human-readable) +- 📱 **Pure Dart**: Works on mobile, desktop, web, and server +- 🧪 **Battle-Tested**: 264+ automated tests, 80%+ feature parity with Python TinyDB + +## Table of Contents + +- [Real-World Use Cases](#real-world-use-cases) +- [Installation](#installation) +- [Quick Start Examples](#quick-start-examples) + - [Remote API Caching](#1-remote-api-caching) + - [Basic CRUD Operations](#2-basic-crud-operations) + - [Persistent Storage](#3-persistent-storage) +- [Complete Query Guide](#complete-query-guide) +- [Complete Update Operations](#complete-update-operations) +- [Advanced Examples](#advanced-examples) + - [Nested Path Queries](#nested-path-queries) + - [Complex Logical Conditions](#complex-logical-conditions) + - [List Operations](#list-operations) + - [Regex Searches](#regex-searches) + - [Custom Test Functions](#custom-test-functions) +- [Storage Backends](#storage-backends) +- [API Reference](#api-reference) +- [Practical Patterns](#practical-patterns) +- [Examples Index](#examples-index) +- [Comparison with Alternatives](#comparison-with-alternatives) +- [Credits](#credits) +- [License](#license) + +## Real-World Use Cases + +### 📱 App Data & User Settings +Store local app data with powerful queries: +```dart +final db = TinyDb(MemoryStorage()); + +// User preferences with nested data - just insert! +await db.insert({ + 'theme': 'dark', + 'notifications': { + 'email': true, + 'push': false, + 'frequency': 'daily' + }, + 'features': ['premium', 'beta'] +}); + +// Query nested settings +final emailEnabled = await db.defaultTable.get( + where('notifications.email').equals(true) +); +``` + +### 💾 Local Data Storage +Perfect for to-do apps, note apps, local caches: +```dart +final db = TinyDb(JsonStorage('tasks.json')); + +// Store tasks - simple insert +await db.insert({ + 'title': 'Finish report', + 'priority': 'high', + 'tags': ['work', 'urgent'], + 'dueDate': '2025-10-15' +}); + +// Find high-priority tasks +final urgentTasks = await db.defaultTable.search( + where('priority').equals('high') + .and(where('tags').anyInList(['urgent'])) +); +``` + +### 🎮 Game State & Progress +Track player data, inventory, achievements: +```dart +final db = TinyDb(JsonStorage('game_save.json')); + +// Player inventory - just add it +await db.insert({ + 'itemId': 'sword_001', + 'name': 'Legendary Sword', + 'stats': {'damage': 150, 'durability': 100}, + 'equipped': true +}); + +// Find all equipped legendary items +final equipped = await db.defaultTable.search( + where('equipped').equals(true) + .and(where('name').search('Legendary')) +); +``` + +### 🧪 Testing & Development +Mock database behavior with fixtures: +```dart +// Load test data +final db = TinyDb(MemoryStorage()); +await db.insertMultiple([ + {'role': 'admin', 'name': 'Alice'}, + {'role': 'user', 'name': 'Bob'} +]); + +// Test your business logic +final admins = await db.search(where('role').equals('admin')); +expect(admins, hasLength(1)); +``` + +### 🌐 Bonus: API Data (Optional!) +And yes, you can also fetch and query remote JSON: +```dart +// Fetch from any API +final response = await http.get(Uri.parse('https://api.example.com/data')); +final data = jsonDecode(response.body) as List; + +// Make it queryable +for (final item in data) { + await db.insert(item); +} + +// Query API data offline +final filtered = await db.search(where('category').equals('electronics')); +``` + +## Installation + +Add to your project: + +```sh +flutter pub add tiny_db +``` + +Or add to `pubspec.yaml`: + +```yaml +dependencies: + tiny_db: ^0.9.1 +``` + +Then run: + +```sh +flutter pub get +``` + +## Quick Start Examples + +### 1. Remote API Caching + +Fetch and query data from any HTTP API: + +```dart +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + // Fetch repos from GitHub API + final response = await http.get( + Uri.parse('https://api.github.com/users/dart-lang/repos') + ); + final repos = jsonDecode(response.body) as List; + + // Create database and store repos + final db = TinyDb(MemoryStorage()); + try { + final table = db.table('repos'); + await table.insertMultiple( + repos.cast>() + ); + + // Query: Find popular Dart repositories + final popular = await table.search( + where('language').equals('Dart') + .and(where('stargazers_count').greaterThan(500)) + ); + + print('Found ${popular.length} popular Dart repos:'); + for (final repo in popular) { + print(' • ${repo['name']}: ${repo['stargazers_count']} ⭐'); + } + + // Query: Find recently updated repos + final recentDate = DateTime.now().subtract(Duration(days: 30)); + final recent = await table.search( + where('updated_at').test( + (value) => DateTime.parse(value).isAfter(recentDate) + ) + ); + + print('\nRecently updated: ${recent.length} repos'); + + // Query: Archived vs active + final archived = await table.count(where('archived').equals(true)); + final active = await table.count(where('archived').equals(false)); + print('\nArchived: $archived, Active: $active'); + + } finally { + await db.close(); + } +} +``` + +See [example/remote_api_example.dart](example/remote_api_example.dart) for the complete example. + +### 2. Basic CRUD Operations + +Core database operations - Create, Read, Update, Delete: + +```dart +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + final db = TinyDb(MemoryStorage()); + + try { + // CREATE: Insert documents + final id1 = await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC'}); + final id2 = await db.insert({'name': 'Bob', 'age': 25, 'city': 'SF'}); + + // Batch insert + final ids = await db.insertMultiple([ + {'name': 'Charlie', 'age': 35, 'city': 'NYC'}, + {'name': 'Diana', 'age': 28, 'city': 'LA'} + ]); + + // READ: Query documents + final nycUsers = await db.search(where('city').equals('NYC')); + print('NYC users: ${nycUsers.length}'); // 2 + + final youngUsers = await db.search(where('age').lessThan(30)); + print('Young users: ${youngUsers.length}'); // 2 + + // Get by ID + final alice = await db.getById(id1); + print('Alice: ${alice?['name']}, ${alice?['age']}'); + + // Get first match + final firstNyc = await db.defaultTable.get(where('city').equals('NYC')); + print('First NYC user: ${firstNyc?['name']}'); + + // Count matches + final count = await db.defaultTable.count(where('age').greaterThan(27)); + print('Users over 27: $count'); // 3 + + // UPDATE: Modify documents + await db.defaultTable.update( + UpdateOperations().increment('age', 1), + where('city').equals('NYC') + ); + + final aliceUpdated = await db.getById(id1); + print('Alice new age: ${aliceUpdated?['age']}'); // 31 + + // Complex update + await db.defaultTable.update( + UpdateOperations() + .set('verified', true) + .push('tags', 'premium'), + where('age').greaterThanOrEquals(30) + ); + + // DELETE: Remove documents + await db.defaultTable.remove(where('city').equals('LA')); + + print('Remaining users: ${await db.length}'); // 3 + + // Get all documents + final all = await db.all(); + for (final doc in all) { + print('${doc['name']} (${doc['age']}) from ${doc['city']}'); + } + + } finally { + await db.close(); + } +} +``` + +### 3. Persistent Storage + +Save data to a JSON file that persists across app restarts: + +```dart +import 'dart:io'; +import 'package:tiny_db/tiny_db.dart'; +import 'package:path_provider/path_provider.dart'; // For Flutter apps + +Future main() async { + // For Flutter: Use app documents directory + // final appDir = await getApplicationDocumentsDirectory(); + // final dbPath = '${appDir.path}/my_app.json'; + + // For pure Dart: Use any path + final dbPath = 'data/my_database.json'; + + final db = TinyDb( + JsonStorage( + dbPath, + createDirs: true, // Auto-create parent directories + indentAmount: 2, // Pretty-print with 2-space indent + ) + ); + + try { + // Insert data - automatically saved to file + await db.insert({ + 'type': 'note', + 'title': 'Shopping List', + 'items': ['Milk', 'Eggs', 'Bread'], + 'created': DateTime.now().toIso8601String() + }); + + // Use named tables for organization + final settings = db.table('settings'); + await settings.insert({ + 'theme': 'dark', + 'notifications': true, + 'language': 'en' + }); + + final users = db.table('users'); + await users.insert({ + 'username': 'alice', + 'email': 'alice@example.com', + 'preferences': { + 'fontSize': 14, + 'colorScheme': 'blue' + } + }); + + // Query persisted data + final notes = await db.search(where('type').equals('note')); + print('Found ${notes.length} notes'); + + // The JSON file is human-readable! + print('\nJSON file contents:'); + print(File(dbPath).readAsStringSync()); + + // List all tables + final tables = await db.tables(); + print('\nTables: ${tables.join(', ')}'); + + } finally { + await db.close(); // Important: Save and release file locks + } +} +``` + +**JSON file structure** (`my_database.json`): +```json +{ + "_default": { + "1": { + "type": "note", + "title": "Shopping List", + "items": ["Milk", "Eggs", "Bread"], + "created": "2025-10-03T10:30:00.000" + } + }, + "settings": { + "1": { + "theme": "dark", + "notifications": true, + "language": "en" + } + }, + "users": { + "1": { + "username": "alice", + "email": "alice@example.com", + "preferences": { + "fontSize": 14, + "colorScheme": "blue" + } + } + } +} +``` + +## Complete Query Guide + +All 20+ query methods with examples: + +| Method | Example | Description | +|--------|---------|-------------| +| **Equality** | | | +| `equals(value)` | `where('age').equals(30)` | Exact match | +| `notEquals(value)` | `where('status').notEquals('deleted')` | Not equal to value | +| **Existence** | | | +| `exists()` | `where('email').exists()` | Field exists (even if null) | +| `notExists()` | `where('optionalField').notExists()` | Field doesn't exist | +| **Null Checks** | | | +| `isNull()` | `where('deletedAt').isNull()` | Field is null | +| `isNotNull()` | `where('email').isNotNull()` | Field is not null | +| **Numeric Comparisons** | | | +| `greaterThan(n)` | `where('price').greaterThan(100)` | Greater than number | +| `lessThan(n)` | `where('age').lessThan(18)` | Less than number | +| `greaterThanOrEquals(n)` | `where('score').greaterThanOrEquals(90)` | >= number | +| `lessThanOrEquals(n)` | `where('quantity').lessThanOrEquals(10)` | <= number | +| **String/Regex** | | | +| `matches(regex)` | `where('email').matches(r'.*@gmail\.com')` | Full regex match | +| `search(regex)` | `where('name').search(r'john', caseSensitive: false)` | Partial match (contains) | +| **Custom Tests** | | | +| `test(fn)` | `where('date').test((v) => DateTime.parse(v).isAfter(...))` | Custom function test | +| **List Operations** | | | +| `anyInList([values])` | `where('tags').anyInList(['urgent', 'important'])` | List contains any of these | +| `allInList([values])` | `where('tags').allInList(['reviewed', 'approved'])` | List contains all of these | +| `anyElementSatisfies(cond)` | `where('scores').anyElementSatisfies(where('value').greaterThan(90))` | Any element matches | +| `allElementsSatisfy(cond)` | `where('items').allElementsSatisfy(where('status').equals('ready'))` | All elements match | +| **Logical Operators** | | | +| `.and(condition)` | `where('age').greaterThan(18).and(where('verified').equals(true))` | AND logic | +| `.or(condition)` | `where('role').equals('admin').or(where('role').equals('moderator'))` | OR logic | +| `.not()` | `where('status').equals('active').not()` | NOT logic | + +### Nested Path Examples + +Access nested fields using dot notation: + +```dart +// Document structure +await db.insert({ + 'user': { + 'profile': { + 'name': 'Alice', + 'address': { + 'city': 'NYC', + 'zip': '10001' + } + }, + 'settings': { + 'notifications': true + } + } +}); + +// Query nested fields +final nycUsers = await db.search( + where('user.profile.address.city').equals('NYC') +); + +final notificationsOn = await db.search( + where('user.settings.notifications').equals(true) +); +``` + +## Complete Update Operations + +All 8 update operations with examples: + +| Operation | Example | Description | +|-----------|---------|-------------| +| **Field Operations** | | | +| `set(path, value)` | `.set('status', 'active')` | Set field value | +| `delete(path)` | `.delete('temporaryField')` | Remove field | +| **Numeric Operations** | | | +| `increment(path, n)` | `.increment('views', 1)` | Increment number field | +| `decrement(path, n)` | `.decrement('stock', 5)` | Decrement number field | +| **List Operations** | | | +| `push(path, value)` | `.push('tags', 'new-tag')` | Append to list | +| `pull(path, value)` | `.pull('tags', 'old-tag')` | Remove all instances (deep equality) | +| `pop(path)` | `.pop('items')` | Remove last element | +| `addUnique(path, value)` | `.addUnique('categories', 'premium')` | Add if not exists (deep equality) | + +### Chaining Operations + +Combine multiple operations in one update: + +```dart +await db.defaultTable.update( + UpdateOperations() + .set('lastModified', DateTime.now().toIso8601String()) + .increment('viewCount', 1) + .push('history', {'action': 'viewed', 'timestamp': DateTime.now()}) + .addUnique('tags', 'popular'), + where('id').equals(123) +); +``` + +### Nested Path Updates + +Modify nested fields using dot notation: + +```dart +await db.defaultTable.update( + UpdateOperations() + .set('user.profile.name', 'Alice Smith') + .increment('user.stats.loginCount', 1) + .push('user.notifications', { + 'type': 'info', + 'message': 'Welcome back!' + }), + where('userId').equals(42) +); +``` + +## Advanced Examples + +### Nested Path Queries + +```dart +// Complex nested structure +await db.insert({ + 'order': { + 'id': 'ORD-001', + 'customer': { + 'name': 'Alice', + 'address': { + 'city': 'New York', + 'state': 'NY', + 'zip': '10001' + } + }, + 'items': [ + {'product': 'Widget', 'price': 29.99, 'quantity': 2}, + {'product': 'Gadget', 'price': 49.99, 'quantity': 1} + ], + 'total': 109.97 + } +}); + +// Query nested customer data +final nyOrders = await db.search( + where('order.customer.address.state').equals('NY') +); + +// Query by total amount +final largeOrders = await db.search( + where('order.total').greaterThan(100) +); +``` + +### Complex Logical Conditions + +```dart +// Multiple AND conditions +final results = await db.search( + where('age').greaterThanOrEquals(18) + .and(where('age').lessThan(65)) + .and(where('verified').equals(true)) + .and(where('status').equals('active')) +); + +// OR with nested AND +final vipUsers = await db.search( + where('role').equals('admin') + .or( + where('subscriptionLevel').equals('premium') + .and(where('yearsActive').greaterThan(5)) + ) +); + +// NOT logic +final incompleteProfiles = await db.search( + where('profileComplete').equals(true).not() +); +``` + +### List Operations + +```dart +// Insert documents with lists +await db.insertMultiple([ + {'name': 'Alice', 'tags': ['developer', 'flutter', 'senior']}, + {'name': 'Bob', 'tags': ['designer', 'ui-ux']}, + {'name': 'Charlie', 'tags': ['developer', 'backend', 'junior']} +]); + +// Find documents where list contains ANY of these values +final devOrDesigner = await db.search( + where('tags').anyInList(['developer', 'designer']) +); +print('Developers or designers: ${devOrDesigner.length}'); // 3 + +// Find documents where list contains ALL of these values +final seniorFlutter = await db.search( + where('tags').allInList(['flutter', 'senior']) +); +print('Senior Flutter devs: ${seniorFlutter.length}'); // 1 + +// Complex list element matching +await db.insert({ + 'project': 'Website', + 'tasks': [ + {'name': 'Design', 'status': 'done', 'hours': 20}, + {'name': 'Development', 'status': 'in-progress', 'hours': 40}, + {'name': 'Testing', 'status': 'pending', 'hours': 10} + ] +}); + +// Find projects with any completed tasks +final hasCompletedTasks = await db.search( + where('tasks').anyElementSatisfies( + where('status').equals('done') + ) +); + +// Find projects where all tasks are completed +final fullyCompleted = await db.search( + where('tasks').allElementsSatisfy( + where('status').equals('done') + ) +); +``` + +### Regex Searches + +```dart +await db.insertMultiple([ + {'email': 'alice@gmail.com', 'name': 'Alice'}, + {'email': 'bob@company.com', 'name': 'Bob'}, + {'email': 'charlie@gmail.com', 'name': 'Charlie'} +]); + +// Full regex match (entire string must match) +final gmailUsers = await db.search( + where('email').matches(r'^[a-z]+@gmail\.com$') +); +print('Gmail users: ${gmailUsers.length}'); // 2 + +// Partial match (search/contains) +final hasGmail = await db.search( + where('email').search(r'gmail', caseSensitive: false) +); + +// Case-insensitive name search +final nameContainsBob = await db.search( + where('name').search(r'bob', caseSensitive: false) +); +``` + +### Custom Test Functions + +```dart +// Date comparisons +final recentDocs = await db.search( + where('createdAt').test((value) { + final date = DateTime.parse(value as String); + final thirtyDaysAgo = DateTime.now().subtract(Duration(days: 30)); + return date.isAfter(thirtyDaysAgo); + }) +); + +// Complex validation +final validEmails = await db.search( + where('email').test((value) { + if (value is! String) return false; + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value); + }) +); + +// Numeric ranges with custom logic +final midRange = await db.search( + where('score').test((value) { + if (value is! num) return false; + return value >= 40 && value <= 60; + }) +); +``` + +## Storage Backends + +### Comparison: MemoryStorage vs JsonStorage + +| Feature | MemoryStorage | JsonStorage | +|---------|---------------|-------------| +| **Persistence** | Lost on app restart | Saved to disk | +| **Speed** | Fastest | Fast (with mutex protection) | +| **File I/O** | None | Yes | +| **Dependencies** | Pure Dart | Uses `dart:io` | +| **Best For** | Testing, caching, temp data | Production apps, settings | +| **Human Readable** | No (in-memory) | Yes (JSON format) | +| **Multi-process** | No | No (file-based) | + +### MemoryStorage + +```dart +final db = TinyDb(MemoryStorage()); + +// Fast, ephemeral storage +// Perfect for: +// - Unit tests +// - Temporary caching +// - Development/prototyping +// - Session data + +await db.close(); // No file cleanup needed +``` + +### JsonStorage + +```dart +final db = TinyDb( + JsonStorage( + 'path/to/database.json', + createDirs: true, // Create parent directories automatically + indentAmount: 2, // Pretty-print (null for compact) + ) +); + +// Persistent storage +// Perfect for: +// - User data +// - App settings +// - Offline-first apps +// - Human-readable data inspection + +await db.close(); // Important: Ensures data is flushed to disk +``` + +### Platform-Specific Paths + +**Flutter (Mobile/Desktop):** +```dart +import 'package:path_provider/path_provider.dart'; + +Future createDatabase() async { + final appDir = await getApplicationDocumentsDirectory(); + return TinyDb( + JsonStorage('${appDir.path}/app_data.json', createDirs: true) + ); +} +``` + +**Pure Dart (CLI/Server):** +```dart +// Relative path +final db = TinyDb(JsonStorage('data/db.json', createDirs: true)); + +// Absolute path +final db = TinyDb(JsonStorage('/var/app/database.json')); + +// Temp directory +import 'dart:io'; +final tempDb = TinyDb( + JsonStorage('${Directory.systemTemp.path}/temp_db.json') +); +``` + +### Performance Characteristics + +- **Insert**: O(1) - direct ID assignment +- **Query (simple)**: O(n) - full table scan +- **Query (by ID)**: O(1) - direct lookup +- **Update**: O(n) - scan + modify matched docs +- **Delete**: O(n) - scan + remove matched docs +- **Storage**: All operations protected by mutex for thread safety (JsonStorage) + +For large datasets (10k+ documents), consider: +- Indexing strategies (future plugin support) +- Filtering before insertion +- Batch operations where possible +- MemoryStorage for read-heavy workloads + +## API Reference + +### TinyDb Class + +```dart +final db = TinyDb(storage); +``` + +**Methods:** +- `table(String name)` → `Table` - Get or create named table +- `defaultTable` → `Table` - Get default table (`_default`) +- `tables()` → `Future>` - List all table names +- `dropTable(String name)` → `Future` - Delete a table +- `dropTables()` → `Future` - Delete all tables +- `close()` → `Future` - Close database and release resources + +**Default table proxy methods** (operate on `_default` table): +- `insert(doc)`, `insertMultiple(docs)`, `all()`, `getById(id)`, `length`, `isEmpty`, `isNotEmpty`, `truncate()` + +### Table Class + +**CRUD Operations:** +- `insert(Map doc)` → `Future` - Insert document, returns ID +- `insertMultiple(List> docs)` → `Future>` - Batch insert +- `search(QueryCondition condition)` → `Future>>` - Find all matches +- `get(QueryCondition condition)` → `Future?>` - Find first match +- `getById(int id)` → `Future?>` - Get by ID +- `update(UpdateOperations ops, QueryCondition condition)` → `Future>` - Update matches, returns IDs +- `upsert(Map doc, QueryCondition condition)` → `Future>` - Update or insert +- `remove(QueryCondition condition)` → `Future>` - Delete matches, returns IDs +- `removeByIds(List ids)` → `Future>` - Delete by IDs + +**Utility Methods:** +- `all()` → `Future>` - Get all documents +- `length` → `Future` - Count documents +- `isEmpty` / `isNotEmpty` → `Future` - Check if empty +- `truncate()` → `Future` - Clear all documents +- `count(QueryCondition condition)` → `Future` - Count matches +- `contains(QueryCondition condition)` → `Future` - Check if any match exists +- `containsId(int id)` → `Future` - Check if ID exists + +### Query Class + +**Factory:** +- `where(String fieldPath)` → `Query` - Create query for field path (supports dot notation) + +**Comparison Methods:** +- `equals(value)` - Exact match +- `notEquals(value)` - Not equal +- `greaterThan(num)` - Greater than (numeric) +- `lessThan(num)` - Less than (numeric) +- `greaterThanOrEquals(num)` - >= (numeric) +- `lessThanOrEquals(num)` - <= (numeric) + +**Existence & Null:** +- `exists()` - Field exists +- `notExists()` - Field doesn't exist +- `isNull()` - Field is null +- `isNotNull()` - Field is not null + +**String/Regex:** +- `matches(String pattern, {bool caseSensitive = true})` - Full regex match +- `search(String pattern, {bool caseSensitive = true})` - Partial match + +**List Operations:** +- `anyInList(List values)` - List contains any value +- `allInList(List values)` - List contains all values +- `anyElementSatisfies(QueryCondition)` - Any element matches +- `allElementsSatisfy(QueryCondition)` - All elements match + +**Custom:** +- `test(bool Function(dynamic) fn)` - Custom test function + +**Logical Operators:** +- `.and(QueryCondition)` - Logical AND +- `.or(QueryCondition)` - Logical OR +- `.not()` - Logical NOT + +### UpdateOperations Class + +**Field Operations:** +- `set(String path, dynamic value)` - Set field value +- `delete(String path)` - Remove field + +**Numeric:** +- `increment(String path, num amount)` - Increment number +- `decrement(String path, num amount)` - Decrement number + +**List Operations:** +- `push(String path, dynamic value)` - Append to list +- `pull(String path, dynamic value)` - Remove all instances (deep equality) +- `pop(String path)` - Remove last element +- `addUnique(String path, dynamic value)` - Add if not exists (deep equality) + +All operations support **nested paths** via dot notation and are **chainable**. + +## Practical Patterns + +### Model Serialization + +```dart +class User { + final String name; + final int age; + final String email; + + User({required this.name, required this.age, required this.email}); + + // Convert to JSON for storage + Map toJson() => { + 'name': name, + 'age': age, + 'email': email, + }; + + // Create from JSON + factory User.fromJson(Map json) => User( + name: json['name'] as String, + age: json['age'] as int, + email: json['email'] as String, + ); +} + +// Usage +final user = User(name: 'Alice', age: 30, email: 'alice@example.com'); + +// Store +final id = await db.table('users').insert(user.toJson()); + +// Retrieve +final docs = await db.table('users').search(where('name').equals('Alice')); +final retrievedUser = User.fromJson(docs.first); +``` + +### Dependency Injection with get_it + +```dart +import 'package:get_it/get_it.dart'; +import 'package:tiny_db/tiny_db.dart'; +import 'package:path_provider/path_provider.dart'; + +final getIt = GetIt.instance; + +Future setupDatabase() async { + getIt.registerLazySingletonAsync(() async { + final appDir = await getApplicationDocumentsDirectory(); + return TinyDb( + JsonStorage('${appDir.path}/app_db.json', createDirs: true) + ); + }); + + await getIt.isReady(); +} + +// In main.dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await setupDatabase(); + runApp(MyApp()); +} + +// Use anywhere +final db = getIt(); +final users = db.table('users'); +``` + +### Error Handling + +```dart +try { + final db = TinyDb(JsonStorage('data/db.json', createDirs: true)); + + try { + await db.insert({'data': 'value'}); + } on StorageException catch (e) { + print('Storage error: ${e.message}'); + } on CorruptStorageException catch (e) { + print('Corrupt database: ${e.message}'); + // Handle data recovery + } + +} finally { + await db.close(); +} +``` + +### Resource Cleanup + +```dart +// Always close databases +Future myFunction() async { + final db = TinyDb(JsonStorage('data.json')); + try { + // Use database + await db.insert({'key': 'value'}); + } finally { + await db.close(); // Ensures data is saved and resources released + } +} + +// In Flutter widgets +class MyWidget extends StatefulWidget { + @override + State createState() => _MyWidgetState(); +} + +class _MyWidgetState extends State { + late TinyDb db; + + @override + void initState() { + super.initState(); + db = TinyDb(MemoryStorage()); + } + + @override + void dispose() { + db.close(); + super.dispose(); + } +} +``` + +### Testing Patterns + +```dart +import 'package:test/test.dart'; +import 'package:tiny_db/tiny_db.dart'; + +void main() { + late TinyDb db; + + setUp(() { + // Use MemoryStorage for tests - fast and isolated + db = TinyDb(MemoryStorage()); + }); + + tearDown(() async { + // Clean up after each test + await db.close(); + }); + + test('user creation and retrieval', () async { + final id = await db.insert({'name': 'Test User', 'role': 'admin'}); + + final user = await db.getById(id); + expect(user?['name'], 'Test User'); + expect(user?['role'], 'admin'); + }); + + test('query with complex conditions', () async { + await db.insertMultiple([ + {'name': 'Alice', 'age': 25, 'active': true}, + {'name': 'Bob', 'age': 35, 'active': false}, + {'name': 'Charlie', 'age': 30, 'active': true} + ]); + + final activeUsers = await db.search( + where('active').equals(true).and(where('age').greaterThan(26)) + ); + + expect(activeUsers.length, 1); + expect(activeUsers.first['name'], 'Charlie'); + }); +} +``` + +## Examples Index + +Complete working examples in the [`example/`](example/) directory: + +**Quick Start Examples:** +- **[readme_intro_example.dart](example/readme_intro_example.dart)** - The opening example from top of README +- **[use_case_1_local_db.dart](example/use_case_1_local_db.dart)** - Clean local database usage +- **[use_case_2_api_query.dart](example/use_case_2_api_query.dart)** - Fetch & query API data + +**Comprehensive Examples:** +- **[complete_crud_example.dart](example/complete_crud_example.dart)** - All CRUD operations demo +- **[model_serialization_example.dart](example/model_serialization_example.dart)** - Using typed models (with [products_model.dart](example/products_model.dart)) +- **[remote_api_example.dart](example/remote_api_example.dart)** - Fetch and query GitHub repos +- **[weather_api_example.dart](example/weather_api_example.dart)** - Weather API caching pattern +- **[json_storage_example.dart](example/json_storage_example.dart)** - Persistent JSON storage +- **[flutter_example/](example/flutter_example/)** - Complete Flutter app + +Run any example: +```bash +dart run example/use_case_1_local_db.dart +dart run example/model_serialization_example.dart +dart run example/complete_crud_example.dart +``` + +## Comparison with Alternatives + +| Feature | tiny_db | SharedPreferences | Hive | SQLite | Isar | +|---------|---------|-------------------|------|--------|------| +| **Setup Complexity** | None | None | Minimal | Moderate | Moderate | +| **Schema Required** | No | No | Optional | Yes | Yes | +| **Query Capability** | 20+ operators | Key-value only | Limited | SQL (full) | Advanced | +| **JSON Support** | Native | Manual | Manual | Manual | Manual | +| **Nested Queries** | Yes | No | No | Joins required | Yes | +| **Human Readable** | Yes (JSON files) | Yes (XML/JSON) | No (binary) | No (binary) | No (binary) | +| **Best For** | JSON APIs, prototyping, flexible data | Simple settings | Structured data | Complex relations | Performance-critical | +| **Learning Curve** | Minutes | Minutes | Hours | Days | Days | +| **File Size** | Small | Small | Small | Medium | Large | + +**When to use tiny_db:** +- ✅ Working with API JSON data +- ✅ Rapid prototyping with real data +- ✅ Flexible/evolving data structures +- ✅ Human-readable storage preferred +- ✅ Simple to moderate query complexity +- ✅ < 10,000 documents per table + +**When to use alternatives:** +- ❌ Simple key-value storage → SharedPreferences +- ❌ Millions of records → Isar or SQLite +- ❌ Complex relationships → SQLite +- ❌ Maximum performance → Hive or Isar + +## Credits + +Inspired by the excellent [TinyDB](https://tinydb.readthedocs.io/) for Python by Markus Unterwaditzer and contributors. tiny_db brings the same philosophy of simplicity and power to the Dart ecosystem with 80%+ feature parity and several enhancements. + +## License + +MIT License - see [LICENSE](LICENSE) file for details. + +--- + +
+ +**[Documentation](https://github.com/vento007/tiny_db)** • **[Issues](https://github.com/vento007/tiny_db/issues)** • **[Contributing](https://github.com/vento007/tiny_db/blob/main/CONTRIBUTING.md)** + +Made with ❤️ for the Dart & Flutter community + +
diff --git a/OLD_README.md b/OLD_README.md new file mode 100644 index 0000000..88abf09 --- /dev/null +++ b/OLD_README.md @@ -0,0 +1,770 @@ + + +
+ Tiny DB ASCII banner +
+ +```dart +import 'package:tiny_db/tiny_db.dart'; + +final db = TinyDb(JsonStorage('path/to/db.json')); +final table = db.table('users'); + +await table.insert({'name': 'John', 'age': 22}); +final results = await table.search(where('name').equals('John')); + +print(results); // [{name: John, age: 22}] +``` + +
+ +

📦 Tiny DB ⚡

+ +

Ultra-lightweight, embeddable NoSQL database for Dart & Flutter

+ +

+ + Pub + + + Star on Github + + + License: MIT + + + Flutter Website + + Dart Version + Flutter Version + Platform Support + Codecov + Open Issues + Pull Requests + Contributors + Last Commit +

+ +
+ +
+ +# Tiny DB + +## Overview + +**Tiny DB** is a modern, ultra-lightweight NoSQL database for Dart and Flutter, inspired by the beloved [Python TinyDB](https://tinydb.readthedocs.io/en/latest/). With over 80% feature parity and several unique enhancements, it brings the power and flexibility of document-oriented storage to your Dart and Flutter apps—no server required! + +- 🚀 **Feature-rich:** Supports advanced queries, update operations, and deep equality for robust data handling. +- 🧠 **In-memory & JSON file storage:** Choose blazing-fast ephemeral memory mode or persistent, human-readable JSON file storage—switch at any time. +- 🔄 **Familiar, expressive API:** Inspired by Python TinyDB, but fully Dart-idiomatic and enhanced for Dart and Flutter developers. +- 🎯 **Embeddable & portable:** Works everywhere Dart or Flutter runs. In-memory mode is pure Dart (no native code). JSON file storage for Flutter apps uses the standard `path_provider` plugin for safe device storage. + +Whether you need a simple embedded database for prototyping, testing, or production apps, Tiny DB offers a clean, intuitive, and powerful solution. + +Wondering how Tiny DB compares to SharedPreferences, Isar, Hive, or SQLite? See our [detailed comparison](doc/comparison.md). + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [API Overview](#api-overview) +- [Advanced Usage](#advanced-usage) +- [List & Update Operations](#list--update-operations) +- [Storage Backends](#storage-backends) +- [Dependency Injection & App Integration](#dependency-injection--app-integration) +- [Testing](#testing) +- [Contributing](#contributing) +- [Roadmap](#roadmap) +- [Credits](#credits) +- [License](#license) +- [Comparisons vs SharedPreferences, Isar, Hive, SQLite](#comparisons) + +
+ +--- + +# Features + +- Document-oriented, schema-free data storage +- Powerful query language (`where`, logical operators, deep matching) +- Advanced update operations: `push`, `pull`, `pop`, `addUnique` (deep equality) +- Multiple storage backends: In-memory (pure Dart), JSON file (pretty-print, dirs) +- Batch insert, upsert, and multi-table support +- Defensive copying for safe list/mutation operations +- Portable: Dart & Flutter (mobile, desktop, server, CLI) +- 200+ automated tests for reliability + +
+ +--- + +# Installation + +Add to your project: + +```sh +flutter pub add tiny_db +``` + +Or add to your `pubspec.yaml`: + +```yaml +dependencies: + tiny_db: ^0.9 +``` + +Then run: + +```sh +flutter pub get +``` + +
+ +--- + +# Quick Start + +```dart +// Always remember to properly initialize and close your database +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + // Create the database + final db = TinyDb(MemoryStorage()); + + try { + // Use the database + await db.insert({'name': 'John', 'age': 30}); + final results = await db.search(where('name').equals('John')); + print(results); + } finally { + // Always close the database when done + await db.close(); + } +} +``` + +> **Important**: Always call `db.close()` when you're done with the database to properly release resources, especially when using JsonStorage. + +
+ +--- + +# API Overview + +### Core Classes + +- **TinyDb** + Main database entry point. + - `TinyDb(Storage backend)` + - `table(String name)` → Table + - `defaultTable` + - `close()` - **Important**: Always call this when done with the database + - `truncate()`, `tables()`, `all()`, `length`, `isEmpty`, `isNotEmpty`, etc. + - **Note:** `truncate()` only affects the default table. To clear other tables, call `truncate()` on the respective Table instance, or use `dropTables()` to remove all tables. + +- **Table** + Represents a collection of documents. + - `insert(Map doc)` + - `insertMultiple(List docs)` + - `upsert(Map doc, QueryCondition condition)` + - `update(UpdateOperations ops, QueryCondition condition)` + - `search(QueryCondition condition)` + - `get(QueryCondition condition)` + - `getById(DocumentId id)` + - `remove(QueryCondition condition)` + - `all()`, `length`, `truncate()`, `containsId(id)` + +- **Query & QueryCondition** + Build expressive queries. + - `where('field').equals(value)` + - Logical operators: `.and()`, `.or()`, `.not()` + - List/collection queries: `.anyInList([...])`, `.allInList([...])`, etc. + +- **UpdateOperations** + Chainable update helpers for document mutation: + - `.push(field, value)` + - `.pull(field, value)` + - `.pop(field)` + - `.addUnique(field, value)` + - `.set(field, value)` + - `.delete(field)` + - `.increment(field, amount)`, `.decrement(field, amount)` + +- **Storage Backends** + - `MemoryStorage()` (pure Dart, in-memory) + - `JsonStorage(path, {indentAmount, createDirs})` (persistent, file-based) + +### Example + +```dart +final db = TinyDb(MemoryStorage()); +final users = db.table('users'); + +// Insert +await users.insert({'name': 'Alice', 'age': 30}); + +// Query +final result = await users.search(where('name').equals('Alice')); + +// Update +await users.update( + UpdateOperations().increment('age', 1), + where('name').equals('Alice'), +); + +// Remove +await users.remove(where('age').equals(31)); +``` + +### Notes + +- All operations are async (`Future`-based). +- Data is always deeply copied for safety. +- Table names are strings; documents are `Map`. +- Querying and update APIs are chainable and composable. +- **Resource Management**: Always call `db.close()` when done with the database to properly release resources. +
+ +--- + +# Advanced Usage + +Below are a few advanced patterns and real-world use cases. For a comprehensive set of examples, see [More Examples](doc/examples.md). + +### Multi-Table Usage + +```dart +final db = TinyDb(MemoryStorage()); +final users = db.table('users'); +final products = db.table('products'); + +await users.insert({'username': 'alice', 'active': true}); +await products.insert({'name': 'Widget', 'price': 9.99}); +``` + +### Batch Insert & Upsert + +```dart +await users.insertMultiple([ + {'username': 'bob', 'active': false}, + {'username': 'charlie', 'active': true}, +]); + +await users.upsert( + {'username': 'alice', 'active': false}, + where('username').equals('alice'), +); +``` + +### Complex Queries + +```dart +final results = await users.search( + where('active').equals(true).and(where('username').anyInList(['alice', 'charlie'])) +); +``` + +### Advanced Update Operations + +```dart +await users.update( + UpdateOperations() + .push('tags', 'newbie') + .addUnique('roles', 'admin') + .increment('loginCount', 1), + where('username').equals('alice'), +); +``` + +### Model Serialization + +```dart +class Product { + // ... fields ... + + factory Product.fromJson(Map json) => /* ... */; + Map toJson() => /* ... */; +} + +// Store +await products.insert(product.toJson()); + +// Retrieve +final docs = await products.search(where('price').greaterThan(5)); +final productList = docs.map(Product.fromJson).toList(); +``` + +--- + +**See [doc/examples.md](doc/examples.md) for a full list of advanced and edge-case examples.** + +
+ +--- + +# List & Update Operations + +Tiny DB provides robust list mutation and update operations, all with deep equality and defensive copying for safe, predictable behavior. + +- **addUnique(field, value):** Add to a list only if the value (by deep equality) isn't already present. +- **push(field, value):** Append to a list. +- **pull(field, value):** Remove all occurrences of a value from a list (deep equality). +- **pop(field):** Remove the last element from a list. + +**Deep equality** means: +- Lists: Equal if all elements are deeply equal. +- Maps: Equal if all keys and values are deeply equal. +- Primitives: Standard `==`. + +**Defensive copying** ensures all list operations create a deep copy before mutation, preventing accidental reference bugs. + +### Example + +```dart +// Add unique value to a list +await table.update(UpdateOperations().addUnique('tags', 'flutter'), where('name').equals('Alice')); + +// Remove all occurrences of a value +await table.update(UpdateOperations().pull('tags', 'old'), where('name').equals('Alice')); +``` + +For more details and advanced examples, see +[doc/deep_equality_and_add_unique.md](doc/deep_equality_and_add_unique.md) + + +
+ +--- + +# Storage Backends + +Tiny DB offers two storage backends to suit different needs: + +### MemoryStorage + +```dart +final db = TinyDb(MemoryStorage()); +``` + +- **Pure Dart**: No native dependencies, works on all platforms +- **In-memory only**: Data is lost when the app restarts +- **Fast**: All operations happen in memory +- **Great for**: Testing, prototyping, temporary caches, and ephemeral data + +### JsonStorage + +```dart +final db = TinyDb(JsonStorage('path/to/db.json', + indentAmount: 2, // Pretty-print with 2-space indentation + createDirs: true, // Create parent directories if they don't exist +)); +``` + +- **Persistent**: Data is saved to a JSON file +- **Human-readable**: JSON format is easy to inspect and edit +- **Flutter dependency**: Uses `path_provider` plugin for safe file access on mobile +- **Configuration options**: + - `indentAmount`: Controls JSON formatting (null for compact output) + - `createDirs`: Automatically creates parent directories + +#### JSON File Structure + +The JSON storage format organizes data by tables, with document IDs as keys: + +```json +{ + "_default": { // Default table + "1": { // Document ID 1 + "type": "note", + "title": "Shopping List", + "items": ["Milk", "Eggs", "Bread"] + }, + "2": { ... } // Document ID 2 + }, + "settings": { // Named table "settings" + "1": { + "theme": "dark", + "notifications": true, + "preferences": { + "fontSize": 14, + "language": "en", + "autoSave": true + } + } + }, + "profiles": { // Named table "profiles" + "1": { + "username": "alice", + "email": "alice@example.com", + "tags": ["admin", "verified", "active"] + }, + "2": { ... } // Another document + } +} +``` + +See [json_storage_example.dart](example/json_storage_example.dart) for a complete example. + +### Platform Considerations + +- **Flutter apps**: Use `path_provider` to get the correct app storage directory: + + ```dart + import 'package:path_provider/path_provider.dart'; + + Future main() async { + final appDir = await getApplicationDocumentsDirectory(); + final dbPath = '${appDir.path}/my_db.json'; + final db = TinyDb(JsonStorage(dbPath)); + // ... + } + ``` + +- **Pure Dart (CLI, server)**: Use direct file paths: + + ```dart + final db = TinyDb(JsonStorage('data/my_db.json', createDirs: true)); + ``` + +
+ +--- + +# Dependency Injection & App Integration + +Here are some approaches to integrate Tiny DB into your application architecture: + +### Simple Global Instance + +For smaller apps or prototypes, a global instance can be simple and effective: + +```dart +// db_provider.dart +import 'package:tiny_db/tiny_db.dart'; +import 'package:path_provider/path_provider.dart'; + +class DbProvider { + static TinyDb? _instance; + + static Future get instance async { + if (_instance == null) { + final appDir = await getApplicationDocumentsDirectory(); + final dbPath = '${appDir.path}/app_database.json'; + _instance = TinyDb(JsonStorage(dbPath, createDirs: true)); + } + return _instance!; + } +} +``` + +Then in your main.dart file, initialize it early: + +```dart +// main.dart +import 'package:flutter/material.dart'; +import 'db_provider.dart'; + +Future main() async { + // Ensure Flutter is initialized + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize the database early + final db = await DbProvider.instance; + + // Now you can run your app + runApp(MyApp(db: db)); +} + +class MyApp extends StatelessWidget { + final TinyDb db; + + const MyApp({super.key, required this.db}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'TinyDB Demo', + home: HomeScreen(db: db), + ); + } +} + +// Usage in any screen or service: +class HomeScreen extends StatelessWidget { + final TinyDb db; + + const HomeScreen({super.key, required this.db}); + + Future _addUser() async { + final users = db.table('users'); + await users.insert({'name': 'Alice', 'joined': DateTime.now().toIso8601String()}); + } + + // Rest of your widget... +} +``` + +### Using get_it (Service Locator) + +For more structured dependency injection, [get_it](https://pub.dev/packages/get_it) is a popular choice: + +```dart +// service_locator.dart +import 'package:tiny_db/tiny_db.dart'; +import 'package:get_it/get_it.dart'; +import 'package:path_provider/path_provider.dart'; + +final getIt = GetIt.instance; + +Future setupServices() async { + // Register as a lazy singleton + getIt.registerLazySingletonAsync(() async { + final appDir = await getApplicationDocumentsDirectory(); + final dbPath = '${appDir.path}/app_database.json'; + return TinyDb(JsonStorage(dbPath, createDirs: true)); + }); + + // Initialize the database + await getIt.isReady(); +} + +// In main.dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await setupServices(); + runApp(MyApp()); +} + +// Usage anywhere in your app +final db = getIt(); +final products = db.table('products'); +``` + +### With Provider or Riverpod + +For Flutter apps using Provider or Riverpod: + +```dart +// With Provider +final dbProvider = Provider((ref) { + final db = TinyDb(MemoryStorage()); // Or JsonStorage + ref.onDispose(() => db.close()); + return db; +}); + +// In a widget +final db = ref.watch(dbProvider); +``` + +### Closing the Database + +**IMPORTANT**: Always close the database when you're done with it, regardless of how it was created. This ensures proper resource cleanup and data integrity. + +```dart +// For global instances +Future closeDatabase() async { + final db = await DbProvider.instance; + await db.close(); +} + +// With get_it, in your app's dispose method +getIt().close(); + +// In a stateful widget +@override +void dispose() { + db.close(); + super.dispose(); +} + +// With direct usage, use try/finally +Future someFunction() async { + final db = TinyDb(MemoryStorage()); + try { + // Use the database + } finally { + await db.close(); // Always called, even if an exception occurs + } +} +``` + +Failure to close the database properly may result in resource leaks or data integrity issues, especially with JsonStorage. + +
+ +--- + +# Testing + +Tiny DB includes comprehensive tests to help ensure reliability and correctness. + +### Running the Tests + +To run the full test suite: + +```bash +flutter test +``` + +The package includes over 200 automated tests covering core functionality, edge cases, and defensive copying behavior. + +> **Note about test warnings**: When running tests, you may see warnings like `Cannot increment field "name" in document. Value is "Alice". Not a number...`. These are expected and intentional - they're part of tests that verify the library correctly handles invalid operations (like trying to increment a string) by warning rather than crashing. + +### Common Testing Patterns + +When writing tests for your app that uses Tiny DB: + +```dart +// 1. Always use MemoryStorage for tests +setUp(() { + db = TinyDb(MemoryStorage()); +}); + +// 2. Always clean up after tests +tearDown(() async { + await db.close(); +}); + +// 3. Test document equality with deep comparisons +test('document equality', () async { + final doc = {'nested': {'list': [1, 2, {'key': 'value'}]}}; + final id = await db.insert(doc); + final retrieved = await db.getById(id); + + // Remove doc_id for comparison + retrieved?.remove('doc_id'); + expect(retrieved, equals(doc)); // Deep equality works automatically +}); +``` + +
+ +--- + +# Contributing + +Contributions to Tiny DB are welcome and appreciated! This project aims to maintain a high standard of code quality and test coverage. + +### Pull Request Guidelines + +When submitting a PR, please ensure: + +1. **Test Coverage**: All new features or bug fixes include appropriate tests +2. **Documentation**: Update relevant documentation for any changes +3. **Code Style**: Follow the existing code style and Dart conventions +4. **Focused Changes**: Keep PRs focused on a single issue/feature + +### Writing Tests + +Tiny DB uses Dart's built-in testing framework. Here's a simple example of how to write a test: + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:tiny_db/tiny_db.dart'; + +void main() { + late TinyDb db; + + setUp(() { + // Use MemoryStorage for tests to avoid file system operations + db = TinyDb(MemoryStorage()); + }); + + tearDown(() async { + // Always clean up after tests + await db.close(); + }); + + test('insert and retrieve document', () async { + // Arrange + final doc = {'name': 'Test', 'value': 42}; + + // Act + final id = await db.insert(doc); + final result = await db.getById(id); + + // Assert + expect(result?['name'], equals('Test')); + expect(result?['value'], equals(42)); + }); + + test('update operations work correctly', () async { + // Arrange + final id = await db.insert({'tags': ['a', 'b']}); + + // Act + await db.update( + UpdateOperations().addUnique('tags', 'c'), + where('doc_id').equals(id) + ); + final result = await db.getById(id); + + // Assert + expect(result?['tags'], containsAll(['a', 'b', 'c'])); + }); +} +``` + +### Edge Case Testing + +When fixing bugs or adding features, consider these edge cases: + +- Empty collections/documents +- Null values and optional fields +- Deep nesting of objects and arrays +- Concurrent operations (if applicable) +- Resource cleanup (especially with JsonStorage) + +Thank you for contributing to Tiny DB! +
+ +--- + +# Roadmap + +Features under consideration for future releases: + +### Plugin Support +- Index plugins for faster queries on large datasets +- Custom storage backends +- Schema validation plugins + +### Performance Optimizations +- Batch operations for JsonStorage +- Streaming query results for large datasets + +### Other Considerations + +Some features like query caching and middleware were considered but may not be implemented due to architectural decisions favoring simplicity and resource management. The current design emphasizes proper database closing and clean resource management over persistent middleware chains. + +
+ +--- + +# Credits + +This package is heavily inspired by the outstanding [TinyDB](https://tinydb.readthedocs.io/en/latest/) project for Python, created by Markus Unterwaditzer and contributors. Many thanks to the TinyDB community for their elegant design and documentation. + +# License + +Tiny DB is available under the MIT License. See the [LICENSE](LICENSE) file for more information. + +
+ +--- + +# Comparisons vs SharedPreferences, Isar, Hive, SQLite + +Wondering how Tiny DB compares to other storage solutions? + +We've created a detailed comparison with SharedPreferences, Isar, Hive, and SQLite to help you choose the right tool for your needs. + +**[Read the full comparison here](doc/comparison.md)** + +Tiny DB positions itself as the "just right" option between simple key-value stores and full-featured databases - powerful enough for real applications but simple enough to learn in minutes. + + diff --git a/README.md b/README.md index 88abf09..c330aab 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,36 @@ - -
+ +

Tiny DB ASCII banner +

-```dart -import 'package:tiny_db/tiny_db.dart'; - -final db = TinyDb(JsonStorage('path/to/db.json')); -final table = db.table('users'); - -await table.insert({'name': 'John', 'age': 22}); -final results = await table.search(where('name').equals('John')); - -print(results); // [{name: John, age: 22}] -``` -
-

📦 Tiny DB ⚡

+

Tiny DB — simple document DB for Dart, zero setup

-

Ultra-lightweight, embeddable NoSQL database for Dart & Flutter

+

The Dart port of TinyDB — simple documents, powerful queries, zero setup. Store Maps, query anything, works in memory or on JSON files.

Pub - - Star on Github - License: MIT - - Flutter Website + + Dart Version - Dart Version - Flutter Version Platform Support - Codecov - Open Issues - Pull Requests - Contributors + + Open Issues + + + Pull Requests + + + Contributors + Last Commit

@@ -49,58 +38,219 @@ print(results); // [{name: John, age: 22}]
-# Tiny DB +## Quick Preview + +```dart +import 'package:tiny_db/tiny_db.dart'; + +final db = TinyDb(MemoryStorage()); + +await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC'}); +await db.insert({'name': 'Bob', 'age': 25, 'city': 'NYC'}); + +final nycUsers = await db.defaultTable.search(where('city').equals('NYC')); +print(nycUsers.length); // 2 +``` + +## The Magic + +### Use Case 1: Clean Local Database + +```dart +import 'package:tiny_db/tiny_db.dart'; + +final db = TinyDb(MemoryStorage()); + +// Just add your data - plain maps, no schema needed +await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC', 'role': 'developer'}); +await db.insert({'name': 'Bob', 'age': 25, 'city': 'SF', 'role': 'designer'}); +await db.insert({'name': 'Charlie', 'age': 35, 'city': 'NYC', 'role': 'developer'}); + +// Query it like a real database +final nycDevs = await db.defaultTable.search( + where('city').equals('NYC').and(where('role').equals('developer')) +); + +print('NYC developers: ${nycDevs.length}'); // 2 -## Overview +for (final dev in nycDevs) { +} +// Alice, age 30 +// Charlie, age 35 +``` + **Clean. Simple. No schema.** + + > **Try it:** [example/use_case_1_local_db.dart](example/use_case_1_local_db.dart) -**Tiny DB** is a modern, ultra-lightweight NoSQL database for Dart and Flutter, inspired by the beloved [Python TinyDB](https://tinydb.readthedocs.io/en/latest/). With over 80% feature parity and several unique enhancements, it brings the power and flexibility of document-oriented storage to your Dart and Flutter apps—no server required! + ### Use Case 2: Optional — Fetch API JSON and Query Instantly -- 🚀 **Feature-rich:** Supports advanced queries, update operations, and deep equality for robust data handling. -- 🧠 **In-memory & JSON file storage:** Choose blazing-fast ephemeral memory mode or persistent, human-readable JSON file storage—switch at any time. -- 🔄 **Familiar, expressive API:** Inspired by Python TinyDB, but fully Dart-idiomatic and enhanced for Dart and Flutter developers. -- 🎯 **Embeddable & portable:** Works everywhere Dart or Flutter runs. In-memory mode is pure Dart (no native code). JSON file storage for Flutter apps uses the standard `path_provider` plugin for safe device storage. + Note: TinyDb is Maps-first and works without any preexisting JSON. You can optionally ingest JSON from APIs or files when needed. -Whether you need a simple embedded database for prototyping, testing, or production apps, Tiny DB offers a clean, intuitive, and powerful solution. + ```dart + import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:tiny_db/tiny_db.dart'; -Wondering how Tiny DB compares to SharedPreferences, Isar, Hive, or SQLite? See our [detailed comparison](doc/comparison.md). +final db = TinyDb(MemoryStorage()); + +// Fetch JSON from any API +final response = await http.get( + Uri.parse('https://api.github.com/users/vento007/repos') +); +final repos = jsonDecode(response.body) as List; + +// Make it queryable +for (final repo in repos) { + await db.insert(repo); +} + +// Query nested data, complex conditions - instantly +final dartPackages = await db.defaultTable.search( + where('language').equals('Dart').and(where('stargazers_count').greaterThan(3)) +); + +print('Found ${dartPackages.length} popular Dart packages!'); +for (final pkg in dartPackages) { + print('${pkg['name']}: ${pkg['stargazers_count']}'); +} +// flexible_tree_layout: 6 +// riverpod_mvcs_example: 5 +// tiny_db: 4 +``` + + +## What is tiny_db? + + A lightweight, document-oriented NoSQL database for Dart & Flutter. Think of it as **SQLite for JSON** - powerful queries without the SQL. + +- **Instant Queries**: Insert Maps and query immediately; optionally ingest JSON from APIs or files +- **No Schema**: Works with any JSON structure - dynamic and flexible +- **20+ Query Operators**: Nested paths, regex, logical operators, list operations +- **Dual Storage**: In-memory (fast) or persistent JSON files (human-readable) +- **Pure Dart**: Works on mobile, desktop, web, and server +- **Battle-Tested**: 264+ automated tests, 80%+ feature parity with Python TinyDB +{{ ... }} ## Table of Contents -- [Features](#features) +- [Real-World Use Cases](#real-world-use-cases) - [Installation](#installation) -- [Quick Start](#quick-start) -- [API Overview](#api-overview) -- [Advanced Usage](#advanced-usage) -- [List & Update Operations](#list--update-operations) +- [Quick Start Examples](#quick-start-examples) + - [Remote API Caching](#1-remote-api-caching) + - [Basic CRUD Operations](#2-basic-crud-operations) + - [Persistent Storage](#3-persistent-storage) +- [Complete Query Guide](#complete-query-guide) +- [Complete Update Operations](#complete-update-operations) +- [Advanced Examples](#advanced-examples) + - [Nested Path Queries](#nested-path-queries) + - [Complex Logical Conditions](#complex-logical-conditions) + - [List Operations](#list-operations) + - [Regex Searches](#regex-searches) + - [Custom Test Functions](#custom-test-functions) - [Storage Backends](#storage-backends) -- [Dependency Injection & App Integration](#dependency-injection--app-integration) -- [Testing](#testing) -- [Contributing](#contributing) -- [Roadmap](#roadmap) +- [API Reference](#api-reference) +- [Practical Patterns](#practical-patterns) +- [Examples Index](#examples-index) +- [Comparison with Alternatives](#comparison-with-alternatives) - [Credits](#credits) - [License](#license) -- [Comparisons vs SharedPreferences, Isar, Hive, SQLite](#comparisons) -
+## Real-World Use Cases ---- +### App Data & User Settings +Store local app data with powerful queries: +```dart +final db = TinyDb(MemoryStorage()); -# Features +// User preferences with nested data - just insert! +await db.insert({ + 'theme': 'dark', + 'notifications': { + 'email': true, + 'push': false, + 'frequency': 'daily' + }, + 'features': ['premium', 'beta'] +}); -- Document-oriented, schema-free data storage -- Powerful query language (`where`, logical operators, deep matching) -- Advanced update operations: `push`, `pull`, `pop`, `addUnique` (deep equality) -- Multiple storage backends: In-memory (pure Dart), JSON file (pretty-print, dirs) -- Batch insert, upsert, and multi-table support -- Defensive copying for safe list/mutation operations -- Portable: Dart & Flutter (mobile, desktop, server, CLI) -- 200+ automated tests for reliability +// Query nested settings +final emailEnabled = await db.defaultTable.get( + where('notifications.email').equals(true) +); +``` -
+### Local Data Storage +Perfect for to-do apps, note apps, local caches: +```dart +final db = TinyDb(JsonStorage('tasks.json')); + +// Store tasks - simple insert +await db.insert({ + 'title': 'Finish report', + 'priority': 'high', + 'tags': ['work', 'urgent'], + 'dueDate': '2025-10-15' +}); ---- +// Find high-priority tasks +final urgentTasks = await db.defaultTable.search( + where('priority').equals('high') + .and(where('tags').anyInList(['urgent'])) +); +``` -# Installation +### Game State & Progress +Track player data, inventory, achievements: +```dart +final db = TinyDb(JsonStorage('game_save.json')); + +// Player inventory - just add it +await db.insert({ + 'itemId': 'sword_001', + 'name': 'Legendary Sword', + 'stats': {'damage': 150, 'durability': 100}, + 'equipped': true +}); + +// Find all equipped legendary items +final equipped = await db.defaultTable.search( + where('equipped').equals(true) + .and(where('name').search('Legendary')) +); +``` + +### Testing & Development +Mock database behavior with fixtures: +```dart +// Load test data +final db = TinyDb(MemoryStorage()); +await db.insertMultiple([ + {'role': 'admin', 'name': 'Alice'}, + {'role': 'user', 'name': 'Bob'} +]); + +// Test your business logic +final admins = await db.search(where('role').equals('admin')); +expect(admins, hasLength(1)); +``` + +### Bonus: API Data (Optional!) +And yes, you can also fetch and query remote JSON: +```dart +// Fetch from any API +final response = await http.get(Uri.parse('https://api.example.com/data')); +final data = jsonDecode(response.body) as List; + +// Make it queryable +for (final item in data) { + await db.insert(item); +} + +// Query API data offline +final filtered = await db.search(where('category').equals('electronics')); +``` + +## Installation Add to your project: @@ -108,11 +258,11 @@ Add to your project: flutter pub add tiny_db ``` -Or add to your `pubspec.yaml`: +Or add to `pubspec.yaml`: ```yaml dependencies: - tiny_db: ^0.9 + tiny_db: ^0.9.2 ``` Then run: @@ -121,650 +271,947 @@ Then run: flutter pub get ``` -
+## Quick Start Examples ---- +### 1. Remote API Caching -# Quick Start +Fetch and query data from any HTTP API: ```dart -// Always remember to properly initialize and close your database +import 'dart:convert'; +import 'package:http/http.dart' as http; import 'package:tiny_db/tiny_db.dart'; Future main() async { - // Create the database + // Fetch repos from GitHub API + final response = await http.get( + Uri.parse('https://api.github.com/users/dart-lang/repos') + ); + final repos = jsonDecode(response.body) as List; + + // Create database and store repos final db = TinyDb(MemoryStorage()); - try { - // Use the database - await db.insert({'name': 'John', 'age': 30}); - final results = await db.search(where('name').equals('John')); - print(results); + final table = db.table('repos'); + await table.insertMultiple( + repos.cast>() + ); + + // Query: Find popular Dart repositories + final popular = await table.search( + where('language').equals('Dart') + .and(where('stargazers_count').greaterThan(500)) + ); + + print('Found ${popular.length} popular Dart repos:'); + for (final repo in popular) { + print(' - ${repo['name']}: ${repo['stargazers_count']}'); + } + + // Query: Find recently updated repos + final recentDate = DateTime.now().subtract(Duration(days: 30)); + final recent = await table.search( + where('updated_at').test( + (value) => DateTime.parse(value).isAfter(recentDate) + ) + ); + + print('\nRecently updated: ${recent.length} repos'); + + // Query: Archived vs active + final archived = await table.count(where('archived').equals(true)); + final active = await table.count(where('archived').equals(false)); + print('\nArchived: $archived, Active: $active'); + } finally { - // Always close the database when done await db.close(); } } ``` -> **Important**: Always call `db.close()` when you're done with the database to properly release resources, especially when using JsonStorage. - -
+See [example/remote_api_example.dart](example/remote_api_example.dart) for the complete example. ---- +### 2. Basic CRUD Operations -# API Overview - -### Core Classes - -- **TinyDb** - Main database entry point. - - `TinyDb(Storage backend)` - - `table(String name)` → Table - - `defaultTable` - - `close()` - **Important**: Always call this when done with the database - - `truncate()`, `tables()`, `all()`, `length`, `isEmpty`, `isNotEmpty`, etc. - - **Note:** `truncate()` only affects the default table. To clear other tables, call `truncate()` on the respective Table instance, or use `dropTables()` to remove all tables. - -- **Table** - Represents a collection of documents. - - `insert(Map doc)` - - `insertMultiple(List docs)` - - `upsert(Map doc, QueryCondition condition)` - - `update(UpdateOperations ops, QueryCondition condition)` - - `search(QueryCondition condition)` - - `get(QueryCondition condition)` - - `getById(DocumentId id)` - - `remove(QueryCondition condition)` - - `all()`, `length`, `truncate()`, `containsId(id)` - -- **Query & QueryCondition** - Build expressive queries. - - `where('field').equals(value)` - - Logical operators: `.and()`, `.or()`, `.not()` - - List/collection queries: `.anyInList([...])`, `.allInList([...])`, etc. - -- **UpdateOperations** - Chainable update helpers for document mutation: - - `.push(field, value)` - - `.pull(field, value)` - - `.pop(field)` - - `.addUnique(field, value)` - - `.set(field, value)` - - `.delete(field)` - - `.increment(field, amount)`, `.decrement(field, amount)` - -- **Storage Backends** - - `MemoryStorage()` (pure Dart, in-memory) - - `JsonStorage(path, {indentAmount, createDirs})` (persistent, file-based) - -### Example +Core database operations - Create, Read, Update, Delete: ```dart -final db = TinyDb(MemoryStorage()); -final users = db.table('users'); +import 'package:tiny_db/tiny_db.dart'; -// Insert -await users.insert({'name': 'Alice', 'age': 30}); +Future main() async { + final db = TinyDb(MemoryStorage()); -// Query -final result = await users.search(where('name').equals('Alice')); + try { + // CREATE: Insert documents + final id1 = await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC'}); + final id2 = await db.insert({'name': 'Bob', 'age': 25, 'city': 'SF'}); + + // Batch insert + final ids = await db.insertMultiple([ + {'name': 'Charlie', 'age': 35, 'city': 'NYC'}, + {'name': 'Diana', 'age': 28, 'city': 'LA'} + ]); + + // READ: Query documents + final nycUsers = await db.search(where('city').equals('NYC')); + print('NYC users: ${nycUsers.length}'); // 2 + + final youngUsers = await db.search(where('age').lessThan(30)); + print('Young users: ${youngUsers.length}'); // 2 + + // Get by ID + final alice = await db.getById(id1); + print('Alice: ${alice?['name']}, ${alice?['age']}'); + + // Get first match + final firstNyc = await db.defaultTable.get(where('city').equals('NYC')); + print('First NYC user: ${firstNyc?['name']}'); + + // Count matches + final count = await db.defaultTable.count(where('age').greaterThan(27)); + print('Users over 27: $count'); // 3 + + // UPDATE: Modify documents + await db.defaultTable.update( + UpdateOperations().increment('age', 1), + where('city').equals('NYC') + ); -// Update -await users.update( - UpdateOperations().increment('age', 1), - where('name').equals('Alice'), -); + final aliceUpdated = await db.getById(id1); + print('Alice new age: ${aliceUpdated?['age']}'); // 31 -// Remove -await users.remove(where('age').equals(31)); -``` + // Complex update + await db.defaultTable.update( + UpdateOperations() + .set('verified', true) + .push('tags', 'premium'), + where('age').greaterThanOrEquals(30) + ); -### Notes + // DELETE: Remove documents + await db.defaultTable.remove(where('city').equals('LA')); -- All operations are async (`Future`-based). -- Data is always deeply copied for safety. -- Table names are strings; documents are `Map`. -- Querying and update APIs are chainable and composable. -- **Resource Management**: Always call `db.close()` when done with the database to properly release resources. -
+ print('Remaining users: ${await db.length}'); // 3 ---- + // Get all documents + final all = await db.all(); + for (final doc in all) { + print('${doc['name']} (${doc['age']}) from ${doc['city']}'); + } -# Advanced Usage + } finally { + await db.close(); + } +} +``` -Below are a few advanced patterns and real-world use cases. For a comprehensive set of examples, see [More Examples](doc/examples.md). +### 3. Persistent Storage -### Multi-Table Usage +Save data to a JSON file that persists across app restarts: ```dart -final db = TinyDb(MemoryStorage()); -final users = db.table('users'); -final products = db.table('products'); +import 'dart:io'; +import 'package:tiny_db/tiny_db.dart'; +import 'package:path_provider/path_provider.dart'; // For Flutter apps + +Future main() async { + // For Flutter: Use app documents directory + // final appDir = await getApplicationDocumentsDirectory(); + // final dbPath = '${appDir.path}/my_app.json'; + + // For pure Dart: Use any path + final dbPath = 'data/my_database.json'; + + final db = TinyDb( + JsonStorage( + dbPath, + createDirs: true, // Auto-create parent directories + indentAmount: 2, // Pretty-print with 2-space indent + ) + ); -await users.insert({'username': 'alice', 'active': true}); -await products.insert({'name': 'Widget', 'price': 9.99}); + try { + // Insert data - automatically saved to file + await db.insert({ + 'type': 'note', + 'title': 'Shopping List', + 'items': ['Milk', 'Eggs', 'Bread'], + 'created': DateTime.now().toIso8601String() + }); + + // Use named tables for organization + final settings = db.table('settings'); + await settings.insert({ + 'theme': 'dark', + 'notifications': true, + 'language': 'en' + }); + + final users = db.table('users'); + await users.insert({ + 'username': 'alice', + 'email': 'alice@example.com', + 'preferences': { + 'fontSize': 14, + 'colorScheme': 'blue' + } + }); + + // Query persisted data + final notes = await db.search(where('type').equals('note')); + print('Found ${notes.length} notes'); + + // The JSON file is human-readable! + print('\nJSON file contents:'); + print(File(dbPath).readAsStringSync()); + + // List all tables + final tables = await db.tables(); + print('\nTables: ${tables.join(', ')}'); + + } finally { + await db.close(); // Important: Save and release file locks + } +} ``` -### Batch Insert & Upsert +**JSON file structure** (`my_database.json`): +```json +{ + "_default": { + "1": { + "type": "note", + "title": "Shopping List", + "items": ["Milk", "Eggs", "Bread"], + "created": "2025-10-03T10:30:00.000" + } + }, + "settings": { + "1": { + "theme": "dark", + "notifications": true, + "language": "en" + } + }, + "users": { + "1": { + "username": "alice", + "email": "alice@example.com", + "preferences": { + "fontSize": 14, + "colorScheme": "blue" + } + } + } +} +``` + +## Complete Query Guide + +All 20+ query methods with examples: + +| Method | Example | Description | +|--------|---------|-------------| +| **Equality** | | | +| `equals(value)` | `where('age').equals(30)` | Exact match | +| `notEquals(value)` | `where('status').notEquals('deleted')` | Not equal to value | +| **Existence** | | | +| `exists()` | `where('email').exists()` | Field exists (even if null) | +| `notExists()` | `where('optionalField').notExists()` | Field doesn't exist | +| **Null Checks** | | | +| `isNull()` | `where('deletedAt').isNull()` | Field is null | +| `isNotNull()` | `where('email').isNotNull()` | Field is not null | +| **Numeric Comparisons** | | | +| `greaterThan(n)` | `where('price').greaterThan(100)` | Greater than number | +| `lessThan(n)` | `where('age').lessThan(18)` | Less than number | +| `greaterThanOrEquals(n)` | `where('score').greaterThanOrEquals(90)` | >= number | +| `lessThanOrEquals(n)` | `where('quantity').lessThanOrEquals(10)` | <= number | +| **String/Regex** | | | +| `matches(regex)` | `where('email').matches(r'.*@gmail\.com')` | Full regex match | +| `search(regex)` | `where('name').search(r'john', caseSensitive: false)` | Partial match (contains) | +| **Custom Tests** | | | +| `test(fn)` | `where('date').test((v) => DateTime.parse(v).isAfter(...))` | Custom function test | +| **List Operations** | | | +| `anyInList([values])` | `where('tags').anyInList(['urgent', 'important'])` | List contains any of these | +| `allInList([values])` | `where('tags').allInList(['reviewed', 'approved'])` | List contains all of these | +| `anyElementSatisfies(cond)` | `where('scores').anyElementSatisfies(where('value').greaterThan(90))` | Any element matches | +| `allElementsSatisfy(cond)` | `where('items').allElementsSatisfy(where('status').equals('ready'))` | All elements match | +| **Logical Operators** | | | +| `.and(condition)` | `where('age').greaterThan(18).and(where('verified').equals(true))` | AND logic | +| `.or(condition)` | `where('role').equals('admin').or(where('role').equals('moderator'))` | OR logic | +| `.not()` | `where('status').equals('active').not()` | NOT logic | + +### Nested Path Examples + +Access nested fields using dot notation: ```dart -await users.insertMultiple([ - {'username': 'bob', 'active': false}, - {'username': 'charlie', 'active': true}, -]); +// Document structure +await db.insert({ + 'user': { + 'profile': { + 'name': 'Alice', + 'address': { + 'city': 'NYC', + 'zip': '10001' + } + }, + 'settings': { + 'notifications': true + } + } +}); + +// Query nested fields +final nycUsers = await db.search( + where('user.profile.address.city').equals('NYC') +); -await users.upsert( - {'username': 'alice', 'active': false}, - where('username').equals('alice'), +final notificationsOn = await db.search( + where('user.settings.notifications').equals(true) ); ``` -### Complex Queries +## Complete Update Operations + +All 8 update operations with examples: + +| Operation | Example | Description | +|-----------|---------|-------------| +| **Field Operations** | | | +| `set(path, value)` | `.set('status', 'active')` | Set field value | +| `delete(path)` | `.delete('temporaryField')` | Remove field | +| **Numeric Operations** | | | +| `increment(path, n)` | `.increment('views', 1)` | Increment number field | +| `decrement(path, n)` | `.decrement('stock', 5)` | Decrement number field | +| **List Operations** | | | +| `push(path, value)` | `.push('tags', 'new-tag')` | Append to list | +| `pull(path, value)` | `.pull('tags', 'old-tag')` | Remove all instances (deep equality) | +| `pop(path)` | `.pop('items')` | Remove last element | +| `addUnique(path, value)` | `.addUnique('categories', 'premium')` | Add if not exists (deep equality) | + +### Chaining Operations + +Combine multiple operations in one update: ```dart -final results = await users.search( - where('active').equals(true).and(where('username').anyInList(['alice', 'charlie'])) +await db.defaultTable.update( + UpdateOperations() + .set('lastModified', DateTime.now().toIso8601String()) + .increment('viewCount', 1) + .push('history', {'action': 'viewed', 'timestamp': DateTime.now()}) + .addUnique('tags', 'popular'), + where('id').equals(123) ); ``` -### Advanced Update Operations +### Nested Path Updates + +Modify nested fields using dot notation: ```dart -await users.update( +await db.defaultTable.update( UpdateOperations() - .push('tags', 'newbie') - .addUnique('roles', 'admin') - .increment('loginCount', 1), - where('username').equals('alice'), + .set('user.profile.name', 'Alice Smith') + .increment('user.stats.loginCount', 1) + .push('user.notifications', { + 'type': 'info', + 'message': 'Welcome back!' + }), + where('userId').equals(42) ); ``` -### Model Serialization +## Advanced Examples -```dart -class Product { - // ... fields ... +### Nested Path Queries - factory Product.fromJson(Map json) => /* ... */; - Map toJson() => /* ... */; -} +```dart +// Complex nested structure +await db.insert({ + 'order': { + 'id': 'ORD-001', + 'customer': { + 'name': 'Alice', + 'address': { + 'city': 'New York', + 'state': 'NY', + 'zip': '10001' + } + }, + 'items': [ + {'product': 'Widget', 'price': 29.99, 'quantity': 2}, + {'product': 'Gadget', 'price': 49.99, 'quantity': 1} + ], + 'total': 109.97 + } +}); -// Store -await products.insert(product.toJson()); +// Query nested customer data +final nyOrders = await db.search( + where('order.customer.address.state').equals('NY') +); -// Retrieve -final docs = await products.search(where('price').greaterThan(5)); -final productList = docs.map(Product.fromJson).toList(); +// Query by total amount +final largeOrders = await db.search( + where('order.total').greaterThan(100) +); ``` ---- +### Complex Logical Conditions -**See [doc/examples.md](doc/examples.md) for a full list of advanced and edge-case examples.** +```dart +// Multiple AND conditions +final results = await db.search( + where('age').greaterThanOrEquals(18) + .and(where('age').lessThan(65)) + .and(where('verified').equals(true)) + .and(where('status').equals('active')) +); -
+// OR with nested AND +final vipUsers = await db.search( + where('role').equals('admin') + .or( + where('subscriptionLevel').equals('premium') + .and(where('yearsActive').greaterThan(5)) + ) +); ---- +// NOT logic +final incompleteProfiles = await db.search( + where('profileComplete').equals(true).not() +); +``` -# List & Update Operations +### List Operations -Tiny DB provides robust list mutation and update operations, all with deep equality and defensive copying for safe, predictable behavior. +```dart +// Insert documents with lists +await db.insertMultiple([ + {'name': 'Alice', 'tags': ['developer', 'flutter', 'senior']}, + {'name': 'Bob', 'tags': ['designer', 'ui-ux']}, + {'name': 'Charlie', 'tags': ['developer', 'backend', 'junior']} +]); -- **addUnique(field, value):** Add to a list only if the value (by deep equality) isn't already present. -- **push(field, value):** Append to a list. -- **pull(field, value):** Remove all occurrences of a value from a list (deep equality). -- **pop(field):** Remove the last element from a list. +// Find documents where list contains ANY of these values +final devOrDesigner = await db.search( + where('tags').anyInList(['developer', 'designer']) +); +print('Developers or designers: ${devOrDesigner.length}'); // 3 -**Deep equality** means: -- Lists: Equal if all elements are deeply equal. -- Maps: Equal if all keys and values are deeply equal. -- Primitives: Standard `==`. +// Find documents where list contains ALL of these values +final seniorFlutter = await db.search( + where('tags').allInList(['flutter', 'senior']) +); +print('Senior Flutter devs: ${seniorFlutter.length}'); // 1 + +// Complex list element matching +await db.insert({ + 'project': 'Website', + 'tasks': [ + {'name': 'Design', 'status': 'done', 'hours': 20}, + {'name': 'Development', 'status': 'in-progress', 'hours': 40}, + {'name': 'Testing', 'status': 'pending', 'hours': 10} + ] +}); + +// Find projects with any completed tasks +final hasCompletedTasks = await db.search( + where('tasks').anyElementSatisfies( + where('status').equals('done') + ) +); -**Defensive copying** ensures all list operations create a deep copy before mutation, preventing accidental reference bugs. +// Find projects where all tasks are completed +final fullyCompleted = await db.search( + where('tasks').allElementsSatisfy( + where('status').equals('done') + ) +); +``` -### Example +### Regex Searches ```dart -// Add unique value to a list -await table.update(UpdateOperations().addUnique('tags', 'flutter'), where('name').equals('Alice')); +await db.insertMultiple([ + {'email': 'alice@gmail.com', 'name': 'Alice'}, + {'email': 'bob@company.com', 'name': 'Bob'}, + {'email': 'charlie@gmail.com', 'name': 'Charlie'} +]); + +// Full regex match (entire string must match) +final gmailUsers = await db.search( + where('email').matches(r'^[a-z]+@gmail\.com$') +); +print('Gmail users: ${gmailUsers.length}'); // 2 -// Remove all occurrences of a value -await table.update(UpdateOperations().pull('tags', 'old'), where('name').equals('Alice')); +// Partial match (search/contains) +final hasGmail = await db.search( + where('email').search(r'gmail', caseSensitive: false) +); + +// Case-insensitive name search +final nameContainsBob = await db.search( + where('name').search(r'bob', caseSensitive: false) +); ``` -For more details and advanced examples, see -[doc/deep_equality_and_add_unique.md](doc/deep_equality_and_add_unique.md) +### Custom Test Functions +```dart +// Date comparisons +final recentDocs = await db.search( + where('createdAt').test((value) { + final date = DateTime.parse(value as String); + final thirtyDaysAgo = DateTime.now().subtract(Duration(days: 30)); + return date.isAfter(thirtyDaysAgo); + }) +); -
+// Complex validation +final validEmails = await db.search( + where('email').test((value) { + if (value is! String) return false; + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value); + }) +); ---- +// Numeric ranges with custom logic +final midRange = await db.search( + where('score').test((value) { + if (value is! num) return false; + return value >= 40 && value <= 60; + }) +); +``` + +## Storage Backends -# Storage Backends +### Comparison: MemoryStorage vs JsonStorage -Tiny DB offers two storage backends to suit different needs: +| Feature | MemoryStorage | JsonStorage | +|---------|---------------|-------------| +| **Persistence** | Lost on app restart | Saved to disk | +| **Speed** | Fastest | Fast (with mutex protection) | +| **File I/O** | None | Yes | +| **Dependencies** | Pure Dart | Uses `dart:io` | +| **Best For** | Testing, caching, temp data | Production apps, settings | +| **Human Readable** | No (in-memory) | Yes (JSON format) | +| **Multi-process** | No | No (file-based) | ### MemoryStorage ```dart final db = TinyDb(MemoryStorage()); -``` -- **Pure Dart**: No native dependencies, works on all platforms -- **In-memory only**: Data is lost when the app restarts -- **Fast**: All operations happen in memory -- **Great for**: Testing, prototyping, temporary caches, and ephemeral data +// Fast, ephemeral storage +// Perfect for: +// - Unit tests +// - Temporary caching +// - Development/prototyping +// - Session data -### JsonStorage - -```dart -final db = TinyDb(JsonStorage('path/to/db.json', - indentAmount: 2, // Pretty-print with 2-space indentation - createDirs: true, // Create parent directories if they don't exist -)); +await db.close(); // No file cleanup needed ``` -- **Persistent**: Data is saved to a JSON file -- **Human-readable**: JSON format is easy to inspect and edit -- **Flutter dependency**: Uses `path_provider` plugin for safe file access on mobile -- **Configuration options**: - - `indentAmount`: Controls JSON formatting (null for compact output) - - `createDirs`: Automatically creates parent directories +### JsonStorage -#### JSON File Structure +```dart +final db = TinyDb( + JsonStorage( + 'path/to/database.json', + createDirs: true, // Create parent directories automatically + indentAmount: 2, // Pretty-print (null for compact) + ) +); -The JSON storage format organizes data by tables, with document IDs as keys: +// Persistent storage +// Perfect for: +// - User data +// - App settings +// - Offline-first apps +// - Human-readable data inspection -```json -{ - "_default": { // Default table - "1": { // Document ID 1 - "type": "note", - "title": "Shopping List", - "items": ["Milk", "Eggs", "Bread"] - }, - "2": { ... } // Document ID 2 - }, - "settings": { // Named table "settings" - "1": { - "theme": "dark", - "notifications": true, - "preferences": { - "fontSize": 14, - "language": "en", - "autoSave": true - } - } - }, - "profiles": { // Named table "profiles" - "1": { - "username": "alice", - "email": "alice@example.com", - "tags": ["admin", "verified", "active"] - }, - "2": { ... } // Another document - } -} +await db.close(); // Important: Ensures data is flushed to disk ``` -See [json_storage_example.dart](example/json_storage_example.dart) for a complete example. - -### Platform Considerations +### Platform-Specific Paths -- **Flutter apps**: Use `path_provider` to get the correct app storage directory: +**Flutter (Mobile/Desktop):** +```dart +import 'package:path_provider/path_provider.dart'; - ```dart - import 'package:path_provider/path_provider.dart'; - - Future main() async { - final appDir = await getApplicationDocumentsDirectory(); - final dbPath = '${appDir.path}/my_db.json'; - final db = TinyDb(JsonStorage(dbPath)); - // ... - } - ``` +Future createDatabase() async { + final appDir = await getApplicationDocumentsDirectory(); + return TinyDb( + JsonStorage('${appDir.path}/app_data.json', createDirs: true) + ); +} +``` -- **Pure Dart (CLI, server)**: Use direct file paths: +**Pure Dart (CLI/Server):** +```dart +// Relative path +final db = TinyDb(JsonStorage('data/db.json', createDirs: true)); - ```dart - final db = TinyDb(JsonStorage('data/my_db.json', createDirs: true)); - ``` +// Absolute path +final db = TinyDb(JsonStorage('/var/app/database.json')); -
+// Temp directory +import 'dart:io'; +final tempDb = TinyDb( + JsonStorage('${Directory.systemTemp.path}/temp_db.json') +); +``` ---- +### Performance Characteristics -# Dependency Injection & App Integration +- **Insert**: O(1) - direct ID assignment +- **Query (simple)**: O(n) - full table scan +- **Query (by ID)**: O(1) - direct lookup +- **Update**: O(n) - scan + modify matched docs +- **Delete**: O(n) - scan + remove matched docs +- **Storage**: All operations protected by mutex for thread safety (JsonStorage) -Here are some approaches to integrate Tiny DB into your application architecture: +For large datasets (10k+ documents), consider: +- Indexing strategies (future plugin support) +- Filtering before insertion +- Batch operations where possible +- MemoryStorage for read-heavy workloads -### Simple Global Instance +## API Reference -For smaller apps or prototypes, a global instance can be simple and effective: +### TinyDb Class ```dart -// db_provider.dart -import 'package:tiny_db/tiny_db.dart'; -import 'package:path_provider/path_provider.dart'; - -class DbProvider { - static TinyDb? _instance; - - static Future get instance async { - if (_instance == null) { - final appDir = await getApplicationDocumentsDirectory(); - final dbPath = '${appDir.path}/app_database.json'; - _instance = TinyDb(JsonStorage(dbPath, createDirs: true)); - } - return _instance!; - } -} +final db = TinyDb(storage); ``` -Then in your main.dart file, initialize it early: +**Methods:** +- `table(String name)` → `Table` - Get or create named table +- `defaultTable` → `Table` - Get default table (`_default`) +- `tables()` → `Future>` - List all table names +- `dropTable(String name)` → `Future` - Delete a table +- `dropTables()` → `Future` - Delete all tables +- `close()` → `Future` - Close database and release resources + +**Default table proxy methods** (operate on `_default` table): +- `insert(doc)`, `insertMultiple(docs)`, `all()`, `getById(id)`, `length`, `isEmpty`, `isNotEmpty`, `truncate()` + +### Table Class + +**CRUD Operations:** +- `insert(Map doc)` → `Future` - Insert document, returns ID +- `insertMultiple(List> docs)` → `Future>` - Batch insert +- `search(QueryCondition condition)` → `Future>>` - Find all matches +- `get(QueryCondition condition)` → `Future?>` - Find first match +- `getById(int id)` → `Future?>` - Get by ID +- `update(UpdateOperations ops, QueryCondition condition)` → `Future>` - Update matches, returns IDs +- `upsert(Map doc, QueryCondition condition)` → `Future>` - Update or insert +- `remove(QueryCondition condition)` → `Future>` - Delete matches, returns IDs +- `removeByIds(List ids)` → `Future>` - Delete by IDs + +**Utility Methods:** +- `all()` → `Future>` - Get all documents +- `length` → `Future` - Count documents +- `isEmpty` / `isNotEmpty` → `Future` - Check if empty +- `truncate()` → `Future` - Clear all documents +- `count(QueryCondition condition)` → `Future` - Count matches +- `contains(QueryCondition condition)` → `Future` - Check if any match exists +- `containsId(int id)` → `Future` - Check if ID exists + +### Query Class + +**Factory:** +- `where(String fieldPath)` → `Query` - Create query for field path (supports dot notation) + +**Comparison Methods:** +- `equals(value)` - Exact match +- `notEquals(value)` - Not equal +- `greaterThan(num)` - Greater than (numeric) +- `lessThan(num)` - Less than (numeric) +- `greaterThanOrEquals(num)` - >= (numeric) +- `lessThanOrEquals(num)` - <= (numeric) + +**Existence & Null:** +- `exists()` - Field exists +- `notExists()` - Field doesn't exist +- `isNull()` - Field is null +- `isNotNull()` - Field is not null + +**String/Regex:** +- `matches(String pattern, {bool caseSensitive = true})` - Full regex match +- `search(String pattern, {bool caseSensitive = true})` - Partial match + +**List Operations:** +- `anyInList(List values)` - List contains any value +- `allInList(List values)` - List contains all values +- `anyElementSatisfies(QueryCondition)` - Any element matches +- `allElementsSatisfy(QueryCondition)` - All elements match + +**Custom:** +- `test(bool Function(dynamic) fn)` - Custom test function + +**Logical Operators:** +- `.and(QueryCondition)` - Logical AND +- `.or(QueryCondition)` - Logical OR +- `.not()` - Logical NOT + +### UpdateOperations Class + +**Field Operations:** +- `set(String path, dynamic value)` - Set field value +- `delete(String path)` - Remove field + +**Numeric:** +- `increment(String path, num amount)` - Increment number +- `decrement(String path, num amount)` - Decrement number + +**List Operations:** +- `push(String path, dynamic value)` - Append to list +- `pull(String path, dynamic value)` - Remove all instances (deep equality) +- `pop(String path)` - Remove last element +- `addUnique(String path, dynamic value)` - Add if not exists (deep equality) + +All operations support **nested paths** via dot notation and are **chainable**. + +## Practical Patterns -```dart -// main.dart -import 'package:flutter/material.dart'; -import 'db_provider.dart'; +### Model Serialization -Future main() async { - // Ensure Flutter is initialized - WidgetsFlutterBinding.ensureInitialized(); - - // Initialize the database early - final db = await DbProvider.instance; - - // Now you can run your app - runApp(MyApp(db: db)); +```dart +class User { + final String name; + final int age; + final String email; + + User({required this.name, required this.age, required this.email}); + + // Convert to JSON for storage + Map toJson() => { + 'name': name, + 'age': age, + 'email': email, + }; + + // Create from JSON + factory User.fromJson(Map json) => User( + name: json['name'] as String, + age: json['age'] as int, + email: json['email'] as String, + ); } -class MyApp extends StatelessWidget { - final TinyDb db; - - const MyApp({super.key, required this.db}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'TinyDB Demo', - home: HomeScreen(db: db), - ); - } -} +// Usage +final user = User(name: 'Alice', age: 30, email: 'alice@example.com'); -// Usage in any screen or service: -class HomeScreen extends StatelessWidget { - final TinyDb db; - - const HomeScreen({super.key, required this.db}); - - Future _addUser() async { - final users = db.table('users'); - await users.insert({'name': 'Alice', 'joined': DateTime.now().toIso8601String()}); - } - - // Rest of your widget... -} -``` +// Store +final id = await db.table('users').insert(user.toJson()); -### Using get_it (Service Locator) +// Retrieve +final docs = await db.table('users').search(where('name').equals('Alice')); +final retrievedUser = User.fromJson(docs.first); +``` -For more structured dependency injection, [get_it](https://pub.dev/packages/get_it) is a popular choice: +### Dependency Injection with get_it ```dart -// service_locator.dart -import 'package:tiny_db/tiny_db.dart'; import 'package:get_it/get_it.dart'; +import 'package:tiny_db/tiny_db.dart'; import 'package:path_provider/path_provider.dart'; final getIt = GetIt.instance; -Future setupServices() async { - // Register as a lazy singleton +Future setupDatabase() async { getIt.registerLazySingletonAsync(() async { final appDir = await getApplicationDocumentsDirectory(); - final dbPath = '${appDir.path}/app_database.json'; - return TinyDb(JsonStorage(dbPath, createDirs: true)); + return TinyDb( + JsonStorage('${appDir.path}/app_db.json', createDirs: true) + ); }); - - // Initialize the database + await getIt.isReady(); } // In main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); - await setupServices(); + await setupDatabase(); runApp(MyApp()); } -// Usage anywhere in your app +// Use anywhere final db = getIt(); -final products = db.table('products'); +final users = db.table('users'); ``` -### With Provider or Riverpod - -For Flutter apps using Provider or Riverpod: +### Error Handling ```dart -// With Provider -final dbProvider = Provider((ref) { - final db = TinyDb(MemoryStorage()); // Or JsonStorage - ref.onDispose(() => db.close()); - return db; -}); +try { + final db = TinyDb(JsonStorage('data/db.json', createDirs: true)); -// In a widget -final db = ref.watch(dbProvider); -``` - -### Closing the Database - -**IMPORTANT**: Always close the database when you're done with it, regardless of how it was created. This ensures proper resource cleanup and data integrity. + try { + await db.insert({'data': 'value'}); + } on StorageException catch (e) { + print('Storage error: ${e.message}'); + } on CorruptStorageException catch (e) { + print('Corrupt database: ${e.message}'); + // Handle data recovery + } -```dart -// For global instances -Future closeDatabase() async { - final db = await DbProvider.instance; +} finally { await db.close(); } +``` -// With get_it, in your app's dispose method -getIt().close(); - -// In a stateful widget -@override -void dispose() { - db.close(); - super.dispose(); -} +### Resource Cleanup -// With direct usage, use try/finally -Future someFunction() async { - final db = TinyDb(MemoryStorage()); +```dart +// Always close databases +Future myFunction() async { + final db = TinyDb(JsonStorage('data.json')); try { - // Use the database + // Use database + await db.insert({'key': 'value'}); } finally { - await db.close(); // Always called, even if an exception occurs + await db.close(); // Ensures data is saved and resources released } } -``` - -Failure to close the database properly may result in resource leaks or data integrity issues, especially with JsonStorage. - -
- ---- -# Testing - -Tiny DB includes comprehensive tests to help ensure reliability and correctness. - -### Running the Tests - -To run the full test suite: - -```bash -flutter test -``` - -The package includes over 200 automated tests covering core functionality, edge cases, and defensive copying behavior. - -> **Note about test warnings**: When running tests, you may see warnings like `Cannot increment field "name" in document. Value is "Alice". Not a number...`. These are expected and intentional - they're part of tests that verify the library correctly handles invalid operations (like trying to increment a string) by warning rather than crashing. - -### Common Testing Patterns - -When writing tests for your app that uses Tiny DB: +// In Flutter widgets +class MyWidget extends StatefulWidget { + @override + State createState() => _MyWidgetState(); +} -```dart -// 1. Always use MemoryStorage for tests -setUp(() { - db = TinyDb(MemoryStorage()); -}); +class _MyWidgetState extends State { + late TinyDb db; -// 2. Always clean up after tests -tearDown(() async { - await db.close(); -}); + @override + void initState() { + super.initState(); + db = TinyDb(MemoryStorage()); + } -// 3. Test document equality with deep comparisons -test('document equality', () async { - final doc = {'nested': {'list': [1, 2, {'key': 'value'}]}}; - final id = await db.insert(doc); - final retrieved = await db.getById(id); - - // Remove doc_id for comparison - retrieved?.remove('doc_id'); - expect(retrieved, equals(doc)); // Deep equality works automatically -}); + @override + void dispose() { + db.close(); + super.dispose(); + } +} ``` -
- ---- - -# Contributing - -Contributions to Tiny DB are welcome and appreciated! This project aims to maintain a high standard of code quality and test coverage. - -### Pull Request Guidelines - -When submitting a PR, please ensure: - -1. **Test Coverage**: All new features or bug fixes include appropriate tests -2. **Documentation**: Update relevant documentation for any changes -3. **Code Style**: Follow the existing code style and Dart conventions -4. **Focused Changes**: Keep PRs focused on a single issue/feature - -### Writing Tests - -Tiny DB uses Dart's built-in testing framework. Here's a simple example of how to write a test: +### Testing Patterns ```dart -import 'package:flutter_test/flutter_test.dart'; +import 'package:test/test.dart'; import 'package:tiny_db/tiny_db.dart'; void main() { late TinyDb db; - + setUp(() { - // Use MemoryStorage for tests to avoid file system operations + // Use MemoryStorage for tests - fast and isolated db = TinyDb(MemoryStorage()); }); - + tearDown(() async { - // Always clean up after tests + // Clean up after each test await db.close(); }); - - test('insert and retrieve document', () async { - // Arrange - final doc = {'name': 'Test', 'value': 42}; - - // Act - final id = await db.insert(doc); - final result = await db.getById(id); - - // Assert - expect(result?['name'], equals('Test')); - expect(result?['value'], equals(42)); - }); - - test('update operations work correctly', () async { - // Arrange - final id = await db.insert({'tags': ['a', 'b']}); - - // Act - await db.update( - UpdateOperations().addUnique('tags', 'c'), - where('doc_id').equals(id) - ); - final result = await db.getById(id); - - // Assert - expect(result?['tags'], containsAll(['a', 'b', 'c'])); - }); -} -``` -### Edge Case Testing + test('user creation and retrieval', () async { + final id = await db.insert({'name': 'Test User', 'role': 'admin'}); -When fixing bugs or adding features, consider these edge cases: + final user = await db.getById(id); + expect(user?['name'], 'Test User'); + expect(user?['role'], 'admin'); + }); -- Empty collections/documents -- Null values and optional fields -- Deep nesting of objects and arrays -- Concurrent operations (if applicable) -- Resource cleanup (especially with JsonStorage) + test('query with complex conditions', () async { + await db.insertMultiple([ + {'name': 'Alice', 'age': 25, 'active': true}, + {'name': 'Bob', 'age': 35, 'active': false}, + {'name': 'Charlie', 'age': 30, 'active': true} + ]); -Thank you for contributing to Tiny DB! -
+ final activeUsers = await db.search( + where('active').equals(true).and(where('age').greaterThan(26)) + ); ---- + expect(activeUsers.length, 1); + expect(activeUsers.first['name'], 'Charlie'); + }); +} +``` -# Roadmap +## Examples Index -Features under consideration for future releases: +Complete working examples in the [`example/`](example/) directory: -### Plugin Support -- Index plugins for faster queries on large datasets -- Custom storage backends -- Schema validation plugins +**Quick Start Examples:** +- **[readme_intro_example.dart](example/readme_intro_example.dart)** - The opening example from top of README +- **[use_case_1_local_db.dart](example/use_case_1_local_db.dart)** - Clean local database usage +- **[use_case_2_api_query.dart](example/use_case_2_api_query.dart)** - Fetch & query API data -### Performance Optimizations -- Batch operations for JsonStorage -- Streaming query results for large datasets +**Comprehensive Examples:** +- **[complete_crud_example.dart](example/complete_crud_example.dart)** - All CRUD operations demo +- **[model_serialization_example.dart](example/model_serialization_example.dart)** - Using typed models (with [products_model.dart](example/products_model.dart)) +- **[remote_api_example.dart](example/remote_api_example.dart)** - Fetch and query GitHub repos +- **[weather_api_example.dart](example/weather_api_example.dart)** - Weather API caching pattern +- **[json_storage_example.dart](example/json_storage_example.dart)** - Persistent JSON storage +- **[flutter_example/](example/flutter_example/)** - Complete Flutter app -### Other Considerations +Run any example: +```bash +dart run example/use_case_1_local_db.dart +dart run example/model_serialization_example.dart +dart run example/complete_crud_example.dart +``` -Some features like query caching and middleware were considered but may not be implemented due to architectural decisions favoring simplicity and resource management. The current design emphasizes proper database closing and clean resource management over persistent middleware chains. +## Comparison with Alternatives -
+| Feature | tiny_db | SharedPreferences | Hive | SQLite | Isar | +|---------|---------|-------------------|------|--------|------| +| **Setup Complexity** | None | None | Minimal | Moderate | Moderate | +| **Schema Required** | No | No | Optional | Yes | Yes | +| **Query Capability** | 20+ operators | Key-value only | Limited | SQL (full) | Advanced | +| **JSON Support** | Native | Manual | Manual | Manual | Manual | +| **Nested Queries** | Yes | No | No | Joins required | Yes | +| **Human Readable** | Yes (JSON files) | Yes (XML/JSON) | No (binary) | No (binary) | No (binary) | +| **Best For** | JSON APIs, prototyping, flexible data | Simple settings | Structured data | Complex relations | Performance-critical | +| **Learning Curve** | Minutes | Minutes | Hours | Days | Days | +| **File Size** | Small | Small | Small | Medium | Large | ---- +**When to use tiny_db:** +- Working with API JSON data +- Rapid prototyping with real data +- Flexible/evolving data structures +- Human-readable storage preferred +- Simple to moderate query complexity +- < 10,000 documents per table -# Credits +**When to use alternatives:** +- Simple key-value storage → SharedPreferences +- Millions of records → Isar or SQLite +- Complex relationships → SQLite +- Maximum performance → Hive or Isar -This package is heavily inspired by the outstanding [TinyDB](https://tinydb.readthedocs.io/en/latest/) project for Python, created by Markus Unterwaditzer and contributors. Many thanks to the TinyDB community for their elegant design and documentation. +## Credits -# License +Inspired by the excellent [TinyDB](https://tinydb.readthedocs.io/) for Python by Markus Unterwaditzer and contributors. tiny_db brings the same philosophy of simplicity and power to the Dart ecosystem with 80%+ feature parity and several enhancements. -Tiny DB is available under the MIT License. See the [LICENSE](LICENSE) file for more information. +## License -
+MIT License - see [LICENSE](LICENSE) file for details. --- -# Comparisons vs SharedPreferences, Isar, Hive, SQLite - -Wondering how Tiny DB compares to other storage solutions? - -We've created a detailed comparison with SharedPreferences, Isar, Hive, and SQLite to help you choose the right tool for your needs. - -**[Read the full comparison here](doc/comparison.md)** +
-Tiny DB positions itself as the "just right" option between simple key-value stores and full-featured databases - powerful enough for real applications but simple enough to learn in minutes. +**[Documentation](https://github.com/vento007/tiny_db)** • **[Issues](https://github.com/vento007/tiny_db/issues)** • **[Contributing](https://github.com/vento007/tiny_db/blob/main/CONTRIBUTING.md)** +
diff --git a/example/complete_crud_example.dart b/example/complete_crud_example.dart new file mode 100644 index 0000000..4725d0e --- /dev/null +++ b/example/complete_crud_example.dart @@ -0,0 +1,311 @@ +// Complete CRUD operations example +// Demonstrates all core database operations + +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + print('🗄️ Complete CRUD Operations Example\n'); + print('${'=' * 60}\n'); + + final db = TinyDb(MemoryStorage()); + + try { + // ======================================== + // CREATE Operations + // ======================================== + print('📝 CREATE Operations\n'); + + // Single insert + print('1. Inserting single document...'); + final id1 = await db.insert({ + 'name': 'Alice', + 'age': 30, + 'city': 'New York', + 'email': 'alice@example.com', + 'tags': ['developer', 'flutter', 'senior'], + }); + print(' ✅ Inserted with ID: $id1\n'); + + // Multiple inserts + print('2. Batch inserting multiple documents...'); + final ids = await db.insertMultiple([ + { + 'name': 'Bob', + 'age': 25, + 'city': 'San Francisco', + 'email': 'bob@example.com', + 'tags': ['designer', 'ui-ux'], + }, + { + 'name': 'Charlie', + 'age': 35, + 'city': 'New York', + 'email': 'charlie@example.com', + 'tags': ['developer', 'backend', 'senior'], + }, + { + 'name': 'Diana', + 'age': 28, + 'city': 'Los Angeles', + 'email': 'diana@example.com', + 'tags': ['developer', 'mobile', 'flutter'], + }, + { + 'name': 'Eve', + 'age': 32, + 'city': 'Seattle', + 'email': 'eve@example.com', + 'tags': ['manager', 'agile'], + }, + ]); + print(' ✅ Inserted ${ids.length} documents with IDs: $ids\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // READ Operations + // ======================================== + print('🔍 READ Operations\n'); + + // Get by ID + print('1. Get document by ID ($id1)...'); + final alice = await db.getById(id1); + print(' 📄 ${alice?['name']}, age ${alice?['age']}, from ${alice?['city']}'); + print(' 📧 Email: ${alice?['email']}\n'); + + // Get all documents + print('2. Get all documents...'); + final all = await db.all(); + print(' 📊 Total documents: ${all.length}\n'); + + // Search with simple condition + print('3. Search: Find users in New York...'); + final nycUsers = await db.defaultTable.search(where('city').equals('New York')); + print(' 🏙️ Found ${nycUsers.length} NYC users:'); + for (final user in nycUsers) { + print(' • ${user['name']}'); + } + print(''); + + // Search with numeric comparison + print('4. Search: Find users under 30...'); + final youngUsers = await db.defaultTable.search(where('age').lessThan(30)); + print(' 👶 Found ${youngUsers.length} young users:'); + for (final user in youngUsers) { + print(' • ${user['name']}, age ${user['age']}'); + } + print(''); + + // Search with AND condition + print('5. Search: Find senior developers (age >= 30 AND has tag "developer")...'); + final seniorDevs = await db.defaultTable.search( + where('age').greaterThanOrEquals(30).and( + where('tags').anyInList(['developer']), + ), + ); + print(' 👨‍💻 Found ${seniorDevs.length} senior developers:'); + for (final user in seniorDevs) { + print(' • ${user['name']}, age ${user['age']}'); + } + print(''); + + // Get first match + print('6. Get first: Find any user in San Francisco...'); + final sfUser = await db.defaultTable.get(where('city').equals('San Francisco')); + print(' 🌉 First SF user: ${sfUser?['name']}\n'); + + // Count matches + print('7. Count: How many users are developers?'); + final devCount = await db.defaultTable.count( + where('tags').anyInList(['developer']), + ); + print(' 🔢 $devCount developers\n'); + + // Check existence + print('8. Contains: Any Flutter developers?'); + final hasFlutter = await db.defaultTable.contains( + where('tags').anyInList(['flutter']), + ); + print(' ✓ Has Flutter devs: $hasFlutter\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // UPDATE Operations + // ======================================== + print('✏️ UPDATE Operations\n'); + + // Simple update + print('1. Update: Increment age for NYC users...'); + final updated1 = await db.defaultTable.update( + UpdateOperations().increment('age', 1), + where('city').equals('New York'), + ); + print(' ✅ Updated ${updated1.length} documents\n'); + + // Verify update + final aliceUpdated = await db.getById(id1); + print(' Alice new age: ${aliceUpdated?['age']}\n'); + + // Complex chained update + print('2. Update: Add metadata to all developers...'); + final updated2 = await db.defaultTable.update( + UpdateOperations() + .set('verified', true) + .set('lastUpdated', DateTime.now().toIso8601String()) + .push('tags', 'active') + .increment('loginCount', 1), + where('tags').anyInList(['developer']), + ); + print(' ✅ Updated ${updated2.length} developers\n'); + + // List operations + print('3. Update: Add unique tag to senior users...'); + final updated3 = await db.defaultTable.update( + UpdateOperations().addUnique('tags', 'senior-member'), + where('age').greaterThanOrEquals(30), + ); + print(' ✅ Updated ${updated3.length} senior users\n'); + + // Nested field update + print('4. Update: Add profile data...'); + final updated4 = await db.defaultTable.update( + UpdateOperations() + .set('profile.status', 'online') + .set('profile.lastSeen', DateTime.now().toIso8601String()), + where('name').equals('Alice'), + ); + print(' ✅ Updated ${updated4.length} user profile\n'); + + // Upsert (update or insert) + print('5. Upsert: Update existing or insert new...'); + final upsertIds = await db.defaultTable.upsert( + {'name': 'Frank', 'age': 40, 'city': 'Boston'}, + where('name').equals('Frank'), + ); + print(' ✅ Upserted with ID(s): $upsertIds\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // DELETE Operations + // ======================================== + print('🗑️ DELETE Operations\n'); + + // Delete by condition + print('1. Delete: Remove users from Los Angeles...'); + final deleted1 = await db.defaultTable.remove( + where('city').equals('Los Angeles'), + ); + print(' ✅ Deleted ${deleted1.length} documents\n'); + + // Delete by IDs + print('2. Delete: Remove specific user by ID...'); + final deleted2 = await db.defaultTable.removeByIds([id1 + 1]); // Bob + print(' ✅ Deleted ${deleted2.length} documents\n'); + + // Check remaining count + final remaining = await db.length; + print(' 📊 Remaining documents: $remaining\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // UTILITY Operations + // ======================================== + print('🔧 UTILITY Operations\n'); + + print('1. Database stats:'); + print(' • Length: ${await db.length}'); + print(' • Is empty: ${await db.isEmpty}'); + print(' • Is not empty: ${await db.isNotEmpty}\n'); + + print('2. Table operations:'); + final userTable = db.table('users'); + await userTable.insert({'name': 'Test User', 'role': 'tester'}); + + final tableNames = await db.tables(); + print(' • Tables: ${tableNames.join(', ')}\n'); + + print('3. Truncate default table...'); + await db.truncate(); + print(' ✅ Default table cleared'); + print(' • Length after truncate: ${await db.length}\n'); + + print('4. Drop specific table...'); + final dropped = await db.dropTable('users'); + print(' ✅ Table dropped: $dropped\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // Advanced Queries + // ======================================== + print('🎯 ADVANCED Queries\n'); + + // Re-populate for advanced queries + await db.insertMultiple([ + { + 'name': 'User1', + 'age': 22, + 'skills': ['dart', 'flutter', 'firebase'], + 'experience': 2, + }, + { + 'name': 'User2', + 'age': 28, + 'skills': ['dart', 'python'], + 'experience': 5, + }, + { + 'name': 'User3', + 'age': 35, + 'skills': ['flutter', 'react'], + 'experience': 8, + }, + ]); + + // List contains all + print('1. Find users with both Dart AND Flutter skills...'); + final fullStack = await db.defaultTable.search( + where('skills').allInList(['dart', 'flutter']), + ); + print(' 🎯 Found ${fullStack.length} full-stack Dart developers\n'); + + // List contains any + print('2. Find users with Dart OR Python skills...'); + final programmers = await db.defaultTable.search( + where('skills').anyInList(['dart', 'python']), + ); + print(' 💻 Found ${programmers.length} programmers\n'); + + // Complex OR logic + print('3. Find junior OR very experienced users...'); + final extremes = await db.defaultTable.search( + where('experience') + .lessThanOrEquals(2) + .or(where('experience').greaterThanOrEquals(8)), + ); + print(' ⚡ Found ${extremes.length} users\n'); + + // NOT logic + print('4. Find users without Flutter skills...'); + final noFlutter = await db.defaultTable.search( + where('skills').anyInList(['flutter']).not(), + ); + print(' ❌ Found ${noFlutter.length} non-Flutter users\n'); + + // Exists check + print('5. Find users with experience field defined...'); + final hasExperience = await db.defaultTable.search( + where('experience').exists(), + ); + print(' ✓ Found ${hasExperience.length} users with experience data\n'); + + print('${'=' * 60}\n'); + print('\n✅ Complete CRUD example finished!\n'); + print('All core database operations demonstrated successfully.\n'); + } finally { + await db.close(); + } +} diff --git a/example/flutter_example/pubspec.lock b/example/flutter_example/pubspec.lock index 4877fe1..cc3e727 100644 --- a/example/flutter_example/pubspec.lock +++ b/example/flutter_example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -91,14 +91,30 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -286,7 +302,15 @@ packages: path: "../.." relative: true source: path - version: "0.9.0" + version: "0.9.1+1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -299,10 +323,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" xdg_directories: dependency: transitive description: diff --git a/example/flutter_example/pubspec.yaml b/example/flutter_example/pubspec.yaml index 5c4f5a8..64147ab 100644 --- a/example/flutter_example/pubspec.yaml +++ b/example/flutter_example/pubspec.yaml @@ -35,7 +35,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 cr_json_widget: ^1.1.1 - + http: ^1.2.0 + # Local reference to tiny_db tiny_db: path: ../../ diff --git a/example/model_serialization_example.dart b/example/model_serialization_example.dart new file mode 100644 index 0000000..23b034f --- /dev/null +++ b/example/model_serialization_example.dart @@ -0,0 +1,95 @@ +// Example: Using typed models with tiny_db +// Shows how to serialize/deserialize custom classes + +import 'package:tiny_db/tiny_db.dart'; +import 'products_model.dart'; // Import the Product model + +Future main() async { + final db = TinyDb(MemoryStorage()); + + try { + print('Model Serialization Example\n'); + + // Create Product instances + final product1 = Product( + name: 'Laptop', + price: 999.99, + tags: ['electronics', 'computers'], + lastUpdated: DateTime.now(), + ); + + final product2 = Product( + name: 'Mouse', + price: 29.99, + tags: ['electronics', 'accessories'], + lastUpdated: DateTime.now(), + ); + + final product3 = Product( + name: 'Keyboard', + price: 79.99, + tags: ['electronics', 'accessories', 'gaming'], + lastUpdated: DateTime.now(), + ); + + // Store using toJson() + print('Storing products...'); + await db.insert(product1.toJson()); + await db.insert(product2.toJson()); + await db.insert(product3.toJson()); + print('✓ Stored 3 products\n'); + + // Query and retrieve as typed objects + print('Querying expensive products (price > 50)...'); + final expensiveDocs = await db.defaultTable.search( + where('price').greaterThan(50), + ); + + // Convert back to Product objects using fromJson() + final expensiveProducts = expensiveDocs.map(Product.fromJson).toList(); + + print('Found ${expensiveProducts.length} expensive products:'); + for (final product in expensiveProducts) { + print(' • ${product.name}: \$${product.price}'); + print(' Tags: ${product.tags.join(', ')}'); + } + print(''); + + // Query by tags (list operation) + print('Querying products with "gaming" tag...'); + final gamingDocs = await db.defaultTable.search( + where('tags').anyInList(['gaming']), + ); + + final gamingProducts = gamingDocs.map(Product.fromJson).toList(); + print('Found ${gamingProducts.length} gaming products:'); + for (final product in gamingProducts) { + print(' • ${product.name}: \$${product.price}'); + } + print(''); + + // Update using models + print('Updating laptop price...'); + await db.defaultTable.update( + UpdateOperations() + .set('price', 899.99) + .set('lastUpdated', DateTime.now().toIso8601String()), + where('name').equals('Laptop'), + ); + + final updatedDocs = await db.defaultTable.search( + where('name').equals('Laptop'), + ); + final updatedLaptop = Product.fromJson(updatedDocs.first); + print('✓ Updated price: \$${updatedLaptop.price}\n'); + + print('Benefits of using models:'); + print(' • Type safety'); + print(' • IDE autocomplete'); + print(' • Clear data structure'); + print(' • Easy validation in fromJson()'); + print(' • Reusable across your app'); + } finally { + await db.close(); + } +} diff --git a/example/readme_intro_example.dart b/example/readme_intro_example.dart new file mode 100644 index 0000000..be1dc85 --- /dev/null +++ b/example/readme_intro_example.dart @@ -0,0 +1,15 @@ +// The opening code example from the top of the README + +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + final db = TinyDb(MemoryStorage()); + + await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC'}); + await db.insert({'name': 'Bob', 'age': 25, 'city': 'NYC'}); + + final nycUsers = await db.defaultTable.search(where('city').equals('NYC')); + print(nycUsers.length); // 2 + + await db.close(); +} diff --git a/example/remote_api_example.dart b/example/remote_api_example.dart new file mode 100644 index 0000000..0a7824c --- /dev/null +++ b/example/remote_api_example.dart @@ -0,0 +1,160 @@ +// Example: Fetch and query data from GitHub API +// This demonstrates tiny_db's killer feature: making any JSON instantly queryable + +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + print('Fetching repositories from GitHub API...\n'); + + // 1. Fetch JSON data from GitHub API + final response = await http.get( + Uri.parse('https://api.github.com/users/dart-lang/repos'), + ); + + if (response.statusCode != 200) { + print('Failed to fetch data: ${response.statusCode}'); + return; + } + + final repos = jsonDecode(response.body) as List; + print('Fetched ${repos.length} repositories\n'); + + // 2. Create database and make the JSON instantly queryable + final db = TinyDb(MemoryStorage()); + + try { + final table = db.table('repos'); + await table.insertMultiple(repos.cast>()); + + print('✅ Data loaded into tiny_db!\n'); + print('${'=' * 60}\n'); + + // 3. Query: Find popular Dart repositories + print('🔍 Finding popular Dart repositories (500+ stars)...\n'); + final popularDart = await table.search( + where('language').equals('Dart').and( + where('stargazers_count').greaterThan(500), + ), + ); + + print('Found ${popularDart.length} popular Dart repos:'); + for (final repo in popularDart) { + print(' ⭐ ${repo['name']}: ${repo['stargazers_count']} stars'); + print(' ${repo['description'] ?? 'No description'}'); + } + + print('\n${'=' * 60}\n'); + + // 4. Query: Recently updated repositories + print('🔍 Finding recently updated repos (last 60 days)...\n'); + final sixtyDaysAgo = DateTime.now().subtract(Duration(days: 60)); + + final recentlyUpdated = await table.search( + where('updated_at').test((value) { + if (value == null) return false; + final updateDate = DateTime.parse(value as String); + return updateDate.isAfter(sixtyDaysAgo); + }), + ); + + print('Found ${recentlyUpdated.length} recently updated repos:'); + for (final repo in recentlyUpdated.take(5)) { + final updated = DateTime.parse(repo['updated_at'] as String); + final daysAgo = DateTime.now().difference(updated).inDays; + print(' 📅 ${repo['name']}: updated $daysAgo days ago'); + } + + print('\n${'=' * 60}\n'); + + // 5. Query: Archived vs Active repositories + print('📊 Repository statistics:\n'); + + final archived = await table.count(where('archived').equals(true)); + final active = await table.count(where('archived').equals(false)); + final hasIssues = await table.count(where('has_issues').equals(true)); + final forked = await table.count(where('fork').equals(true)); + + print(' • Archived: $archived'); + print(' • Active: $active'); + print(' • With issues enabled: $hasIssues'); + print(' • Forks: $forked'); + + print('\n${'=' * 60}\n'); + + // 6. Query: Find repositories by topic/language + print('🔍 Querying by language...\n'); + + final languages = {}; + final allRepos = await table.all(); + for (final repo in allRepos) { + if (repo['language'] != null) { + languages.add(repo['language'] as String); + } + } + + print('Languages found: ${languages.join(', ')}\n'); + + for (final lang in languages.take(3)) { + final count = await table.count(where('language').equals(lang)); + print(' • $lang: $count repos'); + } + + print('\n${'=' * 60}\n'); + + // 7. Complex query: High quality repos + print('🔍 Finding high-quality repositories...\n'); + print('(Criteria: 100+ stars, not archived, not a fork)\n'); + + final highQuality = await table.search( + where('stargazers_count') + .greaterThan(100) + .and(where('archived').equals(false)) + .and(where('fork').equals(false)), + ); + + print('Found ${highQuality.length} high-quality repos:'); + for (final repo in highQuality.take(10)) { + print(' ✨ ${repo['name']}'); + print(' ⭐ ${repo['stargazers_count']} stars, ' + '🍴 ${repo['forks_count']} forks'); + } + + print('\n${'=' * 60}\n'); + + // 8. Demonstrate nested path queries + print('🔍 Querying nested data (owner information)...\n'); + + final firstRepo = await table.get(where('name').isNotNull()); + if (firstRepo != null && firstRepo['owner'] != null) { + final owner = firstRepo['owner'] as Map; + print('Owner info from first repo:'); + print(' • Login: ${owner['login']}'); + print(' • Type: ${owner['type']}'); + print(' • URL: ${owner['url']}'); + } + + print('\n${'=' * 60}\n'); + + // 9. Show how easy it is to add/update data + print('📝 Adding custom metadata to repos...\n'); + + await table.update( + UpdateOperations() + .set('analyzed', true) + .set('analysis_date', DateTime.now().toIso8601String()) + .push('tags', 'dart-ecosystem'), + where('language').equals('Dart'), + ); + + final analyzed = await table.count(where('analyzed').equals(true)); + print('✅ Added metadata to $analyzed Dart repositories'); + + print('\n${'=' * 60}'); + print('\n✨ That\'s the magic of tiny_db!'); + print(' Fetch any JSON → Instantly queryable → No schema needed\n'); + } finally { + await db.close(); + } +} diff --git a/example/use_case_1_local_db.dart b/example/use_case_1_local_db.dart new file mode 100644 index 0000000..1fa5e55 --- /dev/null +++ b/example/use_case_1_local_db.dart @@ -0,0 +1,35 @@ +// Use Case 1 from README: Clean Local Database +// This matches the example shown in the "The Magic" section + +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + final db = TinyDb(MemoryStorage()); + + try { + // Just add your data - plain maps, no schema needed + await db.insert({'name': 'Alice', 'age': 30, 'city': 'NYC', 'role': 'developer'}); + await db.insert({'name': 'Bob', 'age': 25, 'city': 'SF', 'role': 'designer'}); + await db.insert({'name': 'Charlie', 'age': 35, 'city': 'NYC', 'role': 'developer'}); + + // Query it like a real database + final nycDevs = await db.defaultTable.search( + where('city').equals('NYC').and(where('role').equals('developer')), + ); + + print('NYC developers: ${nycDevs.length}'); // 2 + + for (final dev in nycDevs) { + print('${dev['name']}, age ${dev['age']}'); + } + // Alice, age 30 + // Charlie, age 35 + + print('\nExpected output:'); + print('NYC developers: 2'); + print('Alice, age 30'); + print('Charlie, age 35'); + } finally { + await db.close(); + } +} diff --git a/example/use_case_2_api_query.dart b/example/use_case_2_api_query.dart new file mode 100644 index 0000000..549fb73 --- /dev/null +++ b/example/use_case_2_api_query.dart @@ -0,0 +1,51 @@ +// Use Case 2 from README: Fetch API Data & Query Instantly +// This matches the example shown in the "The Magic" section + +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:tiny_db/tiny_db.dart'; + +Future main() async { + final db = TinyDb(MemoryStorage()); + + try { + print('Fetching from GitHub API...\n'); + + // Fetch JSON from any API + final response = await http.get( + Uri.parse('https://api.github.com/users/vento007/repos'), + ); + + if (response.statusCode != 200) { + print('Failed to fetch: ${response.statusCode}'); + return; + } + + final repos = jsonDecode(response.body) as List; + print('Fetched ${repos.length} repositories\n'); + + // Make it queryable + for (final repo in repos) { + await db.insert(repo); + } + + // Query nested data, complex conditions - instantly + final dartPackages = await db.defaultTable.search( + where('language').equals('Dart').and( + where('stargazers_count').greaterThan(3), + ), + ); + + print('Found ${dartPackages.length} popular Dart packages!'); + for (final pkg in dartPackages) { + print('${pkg['name']}: ${pkg['stargazers_count']} ⭐'); + } + + print('\nExpected output:'); + print('flexible_tree_layout: 6 ⭐'); + print('riverpod_mvcs_example: 5 ⭐'); + print('tiny_db: 4 ⭐'); + } finally { + await db.close(); + } +} diff --git a/example/weather_api_example.dart b/example/weather_api_example.dart new file mode 100644 index 0000000..a44e41e --- /dev/null +++ b/example/weather_api_example.dart @@ -0,0 +1,321 @@ +// Weather API caching example +// Demonstrates offline-first pattern with API data caching + +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:tiny_db/tiny_db.dart'; + +// Mock weather API response structure +class WeatherData { + final String city; + final double temperature; + final String condition; + final int humidity; + final DateTime timestamp; + + WeatherData({ + required this.city, + required this.temperature, + required this.condition, + required this.humidity, + required this.timestamp, + }); + + Map toJson() => { + 'city': city, + 'temperature': temperature, + 'condition': condition, + 'humidity': humidity, + 'timestamp': timestamp.toIso8601String(), + }; + + factory WeatherData.fromJson(Map json) => WeatherData( + city: json['city'] as String, + temperature: (json['temperature'] as num).toDouble(), + condition: json['condition'] as String, + humidity: json['humidity'] as int, + timestamp: DateTime.parse(json['timestamp'] as String), + ); +} + +class WeatherCache { + final TinyDb db; + final Duration cacheExpiry; + + WeatherCache(this.db, {this.cacheExpiry = const Duration(hours: 1)}); + + Future getWeather(String city) async { + final table = db.table('weather'); + + // Try to get from cache first + final cached = await table.get( + where('city').equals(city), + ); + + if (cached != null) { + final timestamp = DateTime.parse(cached['timestamp'] as String); + final age = DateTime.now().difference(timestamp); + + if (age < cacheExpiry) { + print('📦 Cache HIT for $city (age: ${age.inMinutes}m)'); + return WeatherData.fromJson(cached); + } else { + print('⏰ Cache EXPIRED for $city (age: ${age.inHours}h)'); + await table.remove(where('city').equals(city)); + } + } else { + print('❌ Cache MISS for $city'); + } + + // Fetch from API + return await _fetchFromApi(city); + } + + Future _fetchFromApi(String city) async { + print('🌐 Fetching from API for $city...'); + + // In a real app, you'd call an actual weather API + // For this example, we'll simulate it + final simulatedData = _simulateApiCall(city); + + if (simulatedData != null) { + // Cache the result + final table = db.table('weather'); + await table.insert(simulatedData.toJson()); + print('✅ Cached weather data for $city'); + } + + return simulatedData; + } + + // Simulate API call (in real app, use http.get) + WeatherData? _simulateApiCall(String city) { + // Simulate some cities being unavailable + if (city.toLowerCase() == 'unknown') { + return null; + } + + // Simulated weather data + final conditions = ['Sunny', 'Cloudy', 'Rainy', 'Snowy']; + final temp = 15.0 + (city.hashCode % 20); + final condition = conditions[city.hashCode % conditions.length]; + + return WeatherData( + city: city, + temperature: temp, + condition: condition, + humidity: 50 + (city.hashCode % 40), + timestamp: DateTime.now(), + ); + } + + Future> getCachedCities() async { + final table = db.table('weather'); + final all = await table.all(); + return all.map((doc) => WeatherData.fromJson(doc)).toList(); + } + + Future clearOldCache() async { + final table = db.table('weather'); + final cutoff = DateTime.now().subtract(cacheExpiry); + + final removed = await table.remove( + where('timestamp').test( + (value) { + if (value == null) return true; + return DateTime.parse(value as String).isBefore(cutoff); + }, + ), + ); + + print('🧹 Cleared $removed expired cache entries'); + } + + Future> getCacheStats() async { + final table = db.table('weather'); + final total = await table.length; + + if (total == 0) { + return {'total': 0, 'fresh': 0, 'stale': 0}; + } + + final cutoff = DateTime.now().subtract(cacheExpiry); + final fresh = await table.count( + where('timestamp').test( + (value) => DateTime.parse(value as String).isAfter(cutoff), + ), + ); + + return { + 'total': total, + 'fresh': fresh, + 'stale': total - fresh, + }; + } +} + +Future main() async { + print('🌤️ Weather API Caching Example\n'); + print('${'=' * 60}\n'); + + // Create database with JSON storage for persistence + final dbPath = '${Directory.systemTemp.path}/weather_cache.json'; + final db = TinyDb( + JsonStorage(dbPath, createDirs: true, indentAmount: 2), + ); + + try { + final cache = WeatherCache(db, cacheExpiry: Duration(minutes: 30)); + + // ======================================== + // Simulate user checking weather + // ======================================== + print('1️⃣ User checks weather for multiple cities\n'); + + final cities = ['New York', 'London', 'Tokyo', 'Paris', 'Sydney']; + + for (final city in cities) { + final weather = await cache.getWeather(city); + if (weather != null) { + print(' $city: ${weather.temperature}°C, ${weather.condition}'); + } + } + + print('\n${'=' * 60}\n'); + + // ======================================== + // Check cache statistics + // ======================================== + print('2️⃣ Cache Statistics\n'); + + final stats = await cache.getCacheStats(); + print(' • Total cached: ${stats['total']}'); + print(' • Fresh entries: ${stats['fresh']}'); + print(' • Stale entries: ${stats['stale']}\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // Simulate cache hits (fast!) + // ======================================== + print('3️⃣ Retrieving from cache (should be instant)\n'); + + final start = DateTime.now(); + for (final city in cities.take(3)) { + await cache.getWeather(city); + } + final elapsed = DateTime.now().difference(start); + print(' ⚡ Retrieved 3 cities in ${elapsed.inMilliseconds}ms\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // Query cached data + // ======================================== + print('4️⃣ Querying cached weather data\n'); + + final table = db.table('weather'); + + // Find hot cities + print(' 🔥 Hot cities (temp > 25°C):'); + final hotCities = await table.search( + where('temperature').greaterThan(25), + ); + for (final city in hotCities) { + print(' • ${city['city']}: ${city['temperature']}°C'); + } + print(''); + + // Find cities with specific conditions + print(' ☀️ Sunny cities:'); + final sunnyCities = await table.search( + where('condition').equals('Sunny'), + ); + for (final city in sunnyCities) { + print(' • ${city['city']} (${city['temperature']}°C)'); + } + print(''); + + // Find recently updated + final fiveMinutesAgo = DateTime.now().subtract(Duration(minutes: 5)); + print(' ⏱️ Recently updated (last 5 min):'); + final recent = await table.search( + where('timestamp').test( + (value) => DateTime.parse(value as String).isAfter(fiveMinutesAgo), + ), + ); + print(' • ${recent.length} cities\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // Offline mode simulation + // ======================================== + print('5️⃣ Offline Mode Simulation\n'); + + print(' 📶 Simulating offline mode...'); + print(' 📦 Available cached cities:\n'); + + final cached = await cache.getCachedCities(); + for (final weather in cached) { + final age = DateTime.now().difference(weather.timestamp); + print(' • ${weather.city}: ${weather.temperature}°C'); + print(' (cached ${age.inMinutes} minutes ago)'); + } + print(''); + + print('${'=' * 60}\n'); + + // ======================================== + // Cache management + // ======================================== + print('6️⃣ Cache Management\n'); + + // Update weather for specific city + print(' 🔄 Refreshing weather for London...'); + await table.remove(where('city').equals('London')); + await cache.getWeather('London'); + print(' ✅ London weather updated\n'); + + // Clear old cache + print(' 🧹 Cleaning up old cache entries...'); + await cache.clearOldCache(); + + final finalStats = await cache.getCacheStats(); + print(' 📊 Final cache stats:'); + print(' • Total: ${finalStats['total']}'); + print(' • Fresh: ${finalStats['fresh']}'); + print(' • Stale: ${finalStats['stale']}\n'); + + print('${'=' * 60}\n'); + + // ======================================== + // Show persistent storage + // ======================================== + print('7️⃣ Persistent Storage\n'); + + print(' 💾 Cache saved to: $dbPath'); + print(' 📄 JSON file contents:\n'); + + if (File(dbPath).existsSync()) { + final jsonContent = File(dbPath).readAsStringSync(); + // Pretty print first 500 chars + final preview = jsonContent.length > 500 + ? '${jsonContent.substring(0, 500)}...' + : jsonContent; + print(preview); + } + + print('\n${'=' * 60}'); + print('\n✅ Weather caching example completed!'); + print(' Key benefits demonstrated:'); + print(' • Offline-first data access'); + print(' • Reduced API calls'); + print(' • Complex queries on cached data'); + print(' • Automatic cache expiry'); + print(' • Persistent storage\n'); + } finally { + await db.close(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 3521621..fb9fd5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: tiny_db description: "Ultra-lightweight, embeddable NoSQL database for Dart & Flutter. Inspired by Python TinyDB, with advanced queries, update operations, and persistent or in-memory storage." -version: 0.9.1+1 +version: 0.9.2 homepage: https://github.com/vento007/tiny_db repository: https://github.com/vento007/tiny_db issue_tracker: https://github.com/vento007/tiny_db/issues @@ -27,6 +27,7 @@ dev_dependencies: build_runner: mockito: coverage: ^1.6.3 + http: ^1.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec