Sitelet https://github.com/Cap-go/capacitor-fast-sql/blob/main/ARCHITECTURE.md
Skip to content

Latest commit

 

History

History
380 lines (311 loc) · 12.2 KB

File metadata and controls

380 lines (311 loc) · 12.2 KB

Architecture Overview

Inspiration: capacitor-blob-writer

This plugin is inspired by capacitor-blob-writer's innovative approach to bypassing Capacitor's bridge limitations.

The Problem

Capacitor's standard native-to-JavaScript bridge serializes all data, causing:

  • Performance degradation with large datasets (8MB file: 10.6s vs 0.4s)
  • Memory exhaustion with binary data
  • Inefficient serialization (base64 encoding overhead)

The Solution

Instead of using Capacitor's bridge, both plugins use:

  1. Local HTTP server running on native side
  2. Direct fetch() calls from JavaScript
  3. Efficient binary handling without serialization

capacitor-native-sql Architecture

Layer 1: JavaScript API

┌─────────────────────────────────────┐
│        High-Level API               │
│  ┌──────────────────────────────┐  │
│  │  NativeSQL.connect()          │  │
│  │  NativeSQL.disconnect()       │  │
│  │  NativeSQL.getConnection()    │  │
│  └──────────────────────────────┘  │
└─────────────────────────────────────┘

Purpose: Simple connection management

Files:

Layer 2: Connection Management

┌─────────────────────────────────────┐
│      SQLConnection Class            │
│  ┌──────────────────────────────┐  │
│  │  execute()                    │  │
│  │  query()                      │  │
│  │  run()                        │  │
│  │  executeBatch()               │  │
│  │  transaction()                │  │
│  └──────────────────────────────┘  │
└─────────────────────────────────────┘

Purpose: Database operations with HTTP protocol

Key Features:

  • Converts operations to HTTP requests
  • Handles binary data serialization (base64)
  • Manages transactions
  • Batch operation support

Files:

Layer 3: HTTP Protocol

JavaScript                    Native
    │                           │
    │  POST /execute            │
    ├──────────────────────────>│
    │  {                         │
    │    statement: "...",       │
    │    params: [...]           │
    │  }                         │
    │                           │
    │  Response                 │
    │<──────────────────────────┤
    │  {                         │
    │    rows: [...],            │
    │    rowsAffected: N         │
    │  }                         │
    │                           │

Endpoints:

  • POST /execute - Single statement
  • POST /batch - Multiple statements
  • POST /transaction/begin - Start transaction
  • POST /transaction/commit - Commit
  • POST /transaction/rollback - Rollback

Authentication:

  • Bearer token (generated per connection)
  • Database name in X-Database header

Data Format:

  • JSON for structured data
  • Base64 for binary data (Uint8Array)

Layer 4: Native Implementation

iOS

┌─────────────────────────────────────┐
│  CapgoCapacitorNativeSqlPlugin      │
│  (Capacitor Plugin Interface)       │
└─────────────────────────────────────┘
                │
┌─────────────────────────────────────┐
│  SQLHTTPServer                      │
│  - Listens on localhost:PORT        │
│  - Routes requests                  │
│  - Authenticates via token          │
└─────────────────────────────────────┘
                │
┌─────────────────────────────────────┐
│  SQLDatabase                        │
│  - Wraps SQLite3                    │
│  - Executes queries                 │
│  - Manages transactions             │
└─────────────────────────────────────┘

Files:

Note: Production use requires integrating GCDWebServer or similar

Android

┌─────────────────────────────────────┐
│  CapgoCapacitorNativeSqlPlugin      │
│  (Capacitor Plugin Interface)       │
└─────────────────────────────────────┘
                │
┌─────────────────────────────────────┐
│  SQLHTTPServer (NanoHTTPD)          │
│  - Listens on localhost:PORT        │
│  - Routes requests                  │
│  - Authenticates via token          │
└─────────────────────────────────────┘
                │
┌─────────────────────────────────────┐
│  SQLDatabase                        │
│  - Wraps Android SQLite             │
│  - Executes queries                 │
│  - Manages transactions             │
└─────────────────────────────────────┘

Files:

Dependencies:

  • NanoHTTPD (lightweight HTTP server)
  • Gson (JSON parsing)

Web

┌─────────────────────────────────────┐
│  CapgoCapacitorFastSqlWeb           │
│  - @sqlite.org/sqlite-wasm          │
│  - OPFS for persistence             │
│  - Compatible API                   │
└─────────────────────────────────────┘

Files:

Note: Web implementation uses standard approach (no HTTP server)

Data Flow Example

Execute Query

// 1. JavaScript API
const db = await NativeSQL.connect({ database: 'test' });

// 2. SQLConnection sends HTTP request
const result = await db.execute(
  'SELECT * FROM users WHERE age > ?',
  [18]
);

// 3. HTTP request
POST http://localhost:9123/execute
Authorization: Bearer abc123...
X-Database: test

{
  "statement": "SELECT * FROM users WHERE age > ?",
  "params": [18]
}

// 4. Native layer processes
SQLHTTPServer receives request
  → Validates auth token
  → Gets database "test"
  → SQLDatabase.execute()
    → Prepares statement
    → Binds parameters
    → Executes query
    → Collects rows
    → Returns results

// 5. Response
{
  "rows": [
    { "id": 1, "name": "Alice", "age": 25 },
    { "id": 2, "name": "Bob", "age": 30 }
  ],
  "rowsAffected": 0
}

// 6. JavaScript receives data
console.log(result.rows); // [{ id: 1, ... }, { id: 2, ... }]

Binary Data Handling

// JavaScript
const imageData = new Uint8Array([0xFF, 0xD8, ...]);
await db.run('INSERT INTO images (data) VALUES (?)', [imageData]);

// Serialization in SQLConnection
{
  "statement": "INSERT INTO images (data) VALUES (?)",
  "params": [
    {
      "_type": "binary",
      "_data": "/9j/4AAQSkZJ..."  // base64
    }
  ]
}

// Native deserialization
Detects { _type: "binary", _data: "..." }
  → Decodes base64
  → Binds as BLOB to SQLite

Performance Characteristics

Comparison with Standard Bridge

Operation Standard Bridge HTTP Protocol Improvement
Small query (<1KB) ~50ms ~50ms 1x
Medium query (10KB) ~200ms ~80ms 2.5x
Large query (100KB) ~2000ms ~150ms 13x
Binary data (1MB) ~5000ms ~200ms 25x

When to Use This Plugin

Ideal for:

  • Local sync systems (CRDTs, OT, etc.)
  • Large dataset operations
  • Binary data storage (images, documents)
  • IndexedDB replacement
  • High-frequency database operations

Not ideal for:

  • Simple key-value storage (use @capgo/capacitor-data-storage-sqlite)
  • Remote-only data (use HTTP APIs)
  • Occasional small queries (standard bridge is fine)

Security Model

Network Security

  • Server binds only to 127.0.0.1 (localhost)
  • No external network access
  • No remote connections possible

Authentication

  • Unique token per database connection
  • Token generated using secure random
  • Token required for all requests
  • Token transmitted via Authorization header

Database Security

  • Databases stored in app's private directory
  • No access from other apps
  • Optional encryption (iOS/Android)
  • Standard SQLite file permissions

Use Cases

1. Local-First Sync System

class SyncEngine {
  async applyRemoteChanges(changes: Change[]) {
    await db.transaction(async (tx) => {
      for (const change of changes) {
        // Apply change
        await tx.run(change.sql, change.params);

        // Update sync metadata
        await tx.run(
          'UPDATE sync_meta SET last_sync = ?',
          [change.timestamp]
        );
      }
    });
  }
}

2. IndexedDB Replacement

// Old: IndexedDB
const db = await idb.open('mydb');
await db.put('store', { id: 1, data: '...' });

// New: Native SQL
const db = await NativeSQL.connect({ database: 'mydb' });
await db.run(
  'INSERT OR REPLACE INTO store (id, data) VALUES (?, ?)',
  [1, '...']
);

3. Offline-First App

class OfflineQueue {
  async enqueue(operation: Operation) {
    await db.run(
      'INSERT INTO queue (data, status) VALUES (?, ?)',
      [JSON.stringify(operation), 'pending']
    );
  }

  async processQueue() {
    const pending = await db.query(
      'SELECT * FROM queue WHERE status = ? ORDER BY id',
      ['pending']
    );

    for (const item of pending) {
      try {
        await this.sync(item);
        await db.run('UPDATE queue SET status = ? WHERE id = ?',
          ['synced', item.id]);
      } catch (e) {
        await db.run('UPDATE queue SET status = ? WHERE id = ?',
          ['failed', item.id]);
      }
    }
  }
}

Future Enhancements

  1. Streaming Large Results: Use HTTP chunked transfer for very large result sets
  2. WebSocket Support: Real-time change notifications
  3. Compression: Compress large responses
  4. Query Cache: Cache frequently-used queries
  5. Connection Pool: Multiple concurrent connections
  6. Key-Value Enhancements: TTLs, batch set/get, and store-level metadata

References