npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

maiasql

v1.3.0

Published

Pure JavaScript SQL engine with a WebSQL-compatible adapter over IndexedDB or memory storage.

Readme

MaiaSQL

MaiaSQL

SQLite-inspired SQL runtime and WebSQL-compatible adapter written in pure JavaScript.

Not SQLite compiled to WebAssembly — a browser-native SQL engine designed for the Web.

MaiaSQL is a browser-native SQL engine written entirely in JavaScript. It aims for practical SQLite-style language and API compatibility where feasible, while remaining explicit about unsupported dialect features and preview-level behavior.

It provides its own lexer, parser, Concrete Syntax Tree (CST), Abstract Syntax Tree (AST), semantic analyzer, execution engine, transaction manager, and storage abstraction. Its default persistent backend is IndexedDB.

The SQL parser is generated by MaiaCC, the self-hosted compiler compiler used by the Maia language ecosystem.

MaiaSQL is designed around two public APIs:

  1. a modern, Promise-based native API;
  2. a WebSQL compatibility API implemented as an adapter over the native API.

This separation allows legacy WebSQL applications to run on MaiaSQL without forcing the core engine to inherit WebSQL's callback-oriented architecture.


Project goals

  • Implement a browser-native SQL engine in pure JavaScript.
  • Provide broad SQLite syntax and behavioral compatibility.
  • Provide a WebSQL-compatible openDatabase() API.
  • Use IndexedDB as a relational storage backend rather than as a container for SQLite database snapshots.
  • Keep parsing, semantic analysis, execution, transactions, and persistence independently testable.
  • Expose tokens, CST, AST, query plans, diagnostics, and execution profiles through an optional native introspection API.
  • Support future storage backends such as in-memory storage and OPFS without changing the SQL interpreter.
  • Remain usable in browsers, Progressive Web Applications, Web Workers, and test environments.

Non-goals

MaiaSQL does not initially aim to provide:

  • SQLite binary file compatibility;
  • SQLite ABI compatibility;
  • a byte-for-byte reimplementation of SQLite;
  • synchronous IndexedDB access;
  • every SQLite extension in the first release;
  • an SQL optimizer comparable to mature native database systems.

The project targets API and language compatibility where practical, while preserving a clean JavaScript architecture.


Current status and reality check

This project is in a solid prototype-to-early-product stage. The runtime and the browser integration work, and the core interpreter is now validated under the project’s own runtime suite. It is still not a drop-in SQLite replacement or a full production-grade SQL administration platform, but the current runtime path is substantially more reliable than earlier snapshots.

Verified status as of 2026-08-28 from the runtime suite and the current regeneration workflow:

  • node --test interpreter/tests/runtime-engine.test.js passes 28 of 28 checks
  • the current regression for NOT IN and NOT LIKE predicates has been fixed in the runtime layer without changing the generated parser or MaiaCC grammar files
  • the parser regeneration path remains a slow/optional validation workflow, not the default execution gate
  • the default memory backend remains stable, and the optional IndexedDB path remains available for Node-based validation scenarios
  • the generation pipeline is not considered broken; it is still a time-intensive operation that should be treated as a separate validation task rather than a required step for every runtime test run
  • the repository now exposes an explicit slow parser validation command, npm run test:parser:slow, to exercise the grammar regeneration path with a realistic timeout budget

For the current supported SQL subset, practical examples, and known limitations, see docs/MANUAL.md.

| Component | Status | Notes | |---|---|---| | Lexer and tokenization | Working | Case-insensitive SQL scanning is in place and stable | | Parser generation from checked-in grammar | Working | Parser used in tests is valid and stable | | Parser regeneration from SQL.ebnf | Slow but valid | Regeneration is computationally expensive and can take several minutes; the test timeout is currently too aggressive for this workflow | | CST collection | Working | Available in the prototype components | | AST and semantic analysis | Working | Core execution features are covered by tests | | DDL support (CREATE/ALTER/DROP TABLE/INDEX/VIEW/TRIGGER) | Working | Broad subset is implemented and tested | | DML support (INSERT/UPDATE/DELETE/SELECT) | Working | CRUD and many relational features work | | Transactions and rollback semantics | Working | Covered by runtime and WebSQL tests | | IndexedDB-backed browser storage | Working | Demo and adapter rely on IndexedDB successfully | | WebSQL compatibility layer | Working | API-level compatibility tests pass | | Native Promise API | Working | Stable enough for app-level usage | | SQLite compatibility depth | Partial | Not a full SQLite clone; missing broad dialect and edge-case coverage | | Browser admin UI | Prototype only | Demo exists, but not yet a full MySQL/phpMyAdmin-style SPA | | Production-hardening and tooling | Partial | Missing CI, coverage, performance profiling, and admin UX polish |

What is already implemented in the interpreter

The runtime already supports a meaningful subset of SQL for browser-native usage:

  • table creation and modification
  • indexes, views and triggers
  • insert/update/delete with parameter binding
  • select with joins, subqueries, grouping, having, order by and limits
  • compound select operators such as UNION, UNION ALL, INTERSECT and EXCEPT
  • foreign key checks and constraint-style enforcement
  • transaction boundaries, savepoints and rollback semantics
  • embedded SQL function support such as CASE, COALESCE, LOWER, UPPER, ABS, ROUND, substr, LIKE and aggregate functions
  • metadata pragmas for table/index/foreign key inspection

What is still missing or limited

The current prototype is strong enough for teaching, demo apps, and local browser data tooling, but it still has important gaps compared to a mature SQL engine:

  • full SQLite compatibility for edge cases and special syntax
  • broader support for advanced DML and DDL variants
  • stronger diagnostic coverage for invalid AST/semantic states
  • safer and more deterministic parser generation workflow
  • deeper schema introspection and metadata editing
  • more complete SQL function coverage and type coercion rules
  • query planner optimization and execution profiling for large datasets
  • robust persistence for multi-database-admin workflows
  • full professional browser UI with table editing, row CRUD, schema management, saved queries and export/import

Browser admin app feasibility

Yes: a professional single-page database manager is feasible with the current architecture.

The architecture is already compatible with a browser GUI that:

  • lists databases stored in IndexedDB
  • shows tables, indexes, views and triggers
  • executes SQL statements in an editor
  • presents result tables with sorting, pagination and CSV export
  • allows row CRUD for simple tables
  • tracks schema metadata and query history
  • supports a light WebSQL-like compatibility mode for older browser code

A MySQL/phpMyAdmin-style experience is realistic as a SPA, but it should be designed as a local-first tool rather than a server-backed admin console. The ideal architecture is:

  • backend: MaiaSQL runtime + IndexedDB storage
  • frontend: single-page app with schema sidebar, SQL editor, results panel, metadata explorer
  • features: database switcher, table inspector, schema designer, query history, saved scripts, import/export, visual table browsing

This would not replace server-side database management, but it is a very good fit for browser-only or local-first database orchestration.


Architecture

System context

flowchart LR
    LegacyApp[Legacy WebSQL application]
    ModernApp[Modern JavaScript application]
    DevTools[Developer tools and SQL inspectors]

    WebSQLAPI[WebSQL compatibility API]
    NativeAPI[MaiaSQL native API]
    IntrospectionAPI[Introspection API]

    Core[MaiaSQL core engine]
    IndexedDB[(IndexedDB)]
    Memory[(In-memory backend)]
    OPFS[(Future OPFS backend)]

    LegacyApp --> WebSQLAPI
    ModernApp --> NativeAPI
    DevTools --> IntrospectionAPI

    WebSQLAPI --> NativeAPI
    NativeAPI --> Core
    IntrospectionAPI --> Core

    Core --> IndexedDB
    Core -. optional .-> Memory
    Core -. future .-> OPFS

The WebSQL adapter is a client of the native API. It does not access the parser, executor, or IndexedDB directly.


Layered architecture

flowchart TB
    subgraph Public_APIs[Public APIs]
        W[WebSQL API]
        N[Native Promise API]
        I[Introspection API]
    end

    subgraph Database_Runtime[Database runtime]
        DB[MaiaDatabase]
        TX[MaiaTransaction]
        STMT[MaiaStatement]
        RESULT[MaiaResult]
    end

    subgraph SQL_Front_End[SQL front end]
        LEX[Lexer]
        PARSER[MaiaCC-generated parser]
        CST[CST]
        ASTB[SqlAstBuilder]
        AST[AST]
        SEM[SemanticAnalyzer]
    end

    subgraph Execution[Execution]
        PLANNER[QueryPlanner]
        PLAN[ExecutionPlan]
        EXEC[SqlExecutor]
        EXPR[ExpressionEvaluator]
        FUNC[FunctionRegistry]
    end

    subgraph Persistence[Persistence]
        TM[TransactionManager]
        CATALOG[CatalogManager]
        STORAGE[StorageEngine interface]
        IDB[IndexedDbStorageEngine]
    end

    W --> N
    N --> DB
    I --> DB

    DB --> TX
    DB --> STMT
    DB --> RESULT

    STMT --> LEX
    LEX --> PARSER
    PARSER --> CST
    CST --> ASTB
    ASTB --> AST
    AST --> SEM
    SEM --> PLANNER
    PLANNER --> PLAN
    PLAN --> EXEC
    EXEC --> EXPR
    EXEC --> FUNC
    EXEC --> TM
    EXEC --> CATALOG
    TM --> STORAGE
    CATALOG --> STORAGE
    STORAGE --> IDB

Component diagram

flowchart LR
    subgraph websql[websql]
        OpenDatabase[openDatabase]
        WebSqlDatabase[WebSqlDatabase]
        WebSqlTransaction[WebSqlTransaction]
        WebSqlResultAdapter[WebSqlResultAdapter]
        WebSqlErrorAdapter[WebSqlErrorAdapter]
    end

    subgraph api[api]
        MaiaSQLFacade[MaiaSQL]
        MaiaDatabase[MaiaDatabase]
        MaiaTransaction[MaiaTransaction]
        MaiaStatement[MaiaStatement]
        MaiaResult[MaiaResult]
    end

    subgraph frontend[frontend]
        Lexer[Lexer]
        Parser[Generated Parser]
        Collector[ParseTreeCollector]
        AstBuilder[SqlAstBuilder]
        SemanticAnalyzer[SemanticAnalyzer]
    end

    subgraph planner[planner]
        QueryPlanner[QueryPlanner]
        PlanNodes[Execution plan nodes]
    end

    subgraph executor[executor]
        SqlExecutor[SqlExecutor]
        ExpressionEvaluator[ExpressionEvaluator]
        FunctionRegistry[FunctionRegistry]
        AggregateRegistry[AggregateRegistry]
    end

    subgraph storage[storage]
        StorageEngine[StorageEngine]
        IndexedDbStorageEngine[IndexedDbStorageEngine]
        CatalogManager[CatalogManager]
        TransactionManager[TransactionManager]
        LockManager[DatabaseLockManager]
    end

    OpenDatabase --> WebSqlDatabase
    WebSqlDatabase --> WebSqlTransaction
    WebSqlTransaction --> WebSqlResultAdapter
    WebSqlTransaction --> WebSqlErrorAdapter
    WebSqlDatabase --> MaiaDatabase

    MaiaSQLFacade --> MaiaDatabase
    MaiaDatabase --> MaiaTransaction
    MaiaDatabase --> MaiaStatement
    MaiaStatement --> Lexer
    MaiaStatement --> Parser
    Parser --> Collector
    Collector --> AstBuilder
    AstBuilder --> SemanticAnalyzer
    SemanticAnalyzer --> QueryPlanner
    QueryPlanner --> PlanNodes
    PlanNodes --> SqlExecutor
    SqlExecutor --> ExpressionEvaluator
    SqlExecutor --> FunctionRegistry
    SqlExecutor --> AggregateRegistry
    SqlExecutor --> TransactionManager
    TransactionManager --> StorageEngine
    CatalogManager --> StorageEngine
    StorageEngine --> IndexedDbStorageEngine
    IndexedDbStorageEngine --> LockManager

Public API design

Native API

The native API is asynchronous and Promise-based.

const db = await MaiaSQL.open({
  name: "application.db",
  version: 1,
  storage: "indexeddb"
});

await db.exec(`
  CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL
  );
`);

const result = await db.exec(
  "SELECT id, name FROM users WHERE id >= ?",
  [1]
);

console.log(result.rows);

The native API is the canonical interface. WebSQL compatibility is built on top of it.

Proposed native interfaces

interface MaiaSQLOpenOptions {
  name: string;
  version?: number;
  storage?: "indexeddb" | "memory";
  readOnly?: boolean;
  pragmas?: Record<string, unknown>;
}

interface MaiaExecOptions {
  transaction?: MaiaTransaction;
  rowMode?: "object" | "array";
  signal?: AbortSignal;
  profile?: boolean;
}

interface MaiaExecutionResult {
  columns: MaiaColumnMetadata[];
  rows: unknown[] | Record<string, unknown>[];
  rowsAffected: number;
  insertId: number | bigint | null;
  statementType: string;
  warnings: MaiaDiagnostic[];
  profile?: MaiaExecutionProfile;
}

WebSQL compatibility API

The compatibility layer exposes:

const db = openDatabase(
  "application.db",
  "1.0",
  "Application database",
  50 * 1024 * 1024
);

db.transaction(
  function (tx) {
    tx.executeSql(
      "SELECT id, name FROM users WHERE id >= ?",
      [1],
      function (_tx, result) {
        for (let i = 0; i < result.rows.length; i += 1) {
          console.log(result.rows.item(i));
        }
      }
    );
  }
);

The adapter must implement:

  • openDatabase();
  • Database.transaction();
  • Database.readTransaction();
  • Database.changeVersion();
  • SQLTransaction.executeSql();
  • SQLResultSet;
  • SQLResultSetRowList;
  • SQLError;
  • transaction success and error callbacks;
  • statement-level error callback behavior;
  • automatic commit or rollback.

Current prototype usage

The current prototype can already be consumed directly from this repository as a CommonJS package entrypoint.

Node.js

const {
  Parser,
  MaiaSQL,
  openDatabase,
  installWebSqlGlobals
} = require('./interpreter');

async function main() {
  const db = await MaiaSQL.open({
    name: 'demo',
    storage: 'memory',
    parser: Parser
  });

  await db.exec(`
    CREATE TABLE users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL
    );
    INSERT INTO users (name) VALUES ('Ada');
  `);

  console.log((await db.exec('SELECT * FROM users')).rows);

  const legacy = openDatabase('legacy-demo', '1.0', 'Legacy demo', 1024 * 1024);
  legacy.transaction(function (tx) {
    tx.executeSql('CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY AUTOINCREMENT, message TEXT)');
  });

  installWebSqlGlobals();
}

main().catch(console.error);

Browser

Load these files in order:

<script src="./interpreter/sql-parser.js"></script>
<script src="./interpreter/maiasql-core.js"></script>
<script src="./interpreter/maiasql-websql.js"></script>

Then use either API:

const db = await MaiaSQLRuntime.MaiaSQL.open({
  name: 'browser-demo',
  storage: 'indexeddb',
  parser: MaiaSQLParser
});

const websqlDb = MaiaSQLWebSQL.openDatabase(
  'browser-demo-websql',
  '1.0',
  'Browser demo',
  5 * 1024 * 1024
);

There is also a ready-to-use interactive demo page at interpreter/browser-demo.html with:

  • an in-browser SQL console;
  • a switch between the native Promise API and the WebSQL compatibility adapter;
  • result tables and execution logs;
  • reset support for the IndexedDB-backed demo database.

If you want a drop-in global replacement in environments where native WebSQL no longer exists, use the package helper:

const { installWebSqlGlobals } = require('./interpreter');
installWebSqlGlobals();

Class model

End-to-end class diagram

classDiagram
    class MaiaSQL {
        +open(options) Promise~MaiaDatabase~
        +openMemory(options) Promise~MaiaDatabase~
        +version string
    }

    class MaiaDatabase {
        +name string
        +version number
        +ready Promise
        +exec(sql, params, options) Promise~MaiaResult~
        +prepare(sql, options) Promise~MaiaStatement~
        +transaction(callback, options) Promise
        +readTransaction(callback, options) Promise
        +close() Promise
        +getCatalog() Promise~DatabaseCatalog~
    }

    class MaiaStatement {
        +sql string
        +parameters ParameterBindings
        +bind(values) MaiaStatement
        +reset() MaiaStatement
        +step() Promise~boolean~
        +get() unknown[]
        +getObject() object
        +all() Promise~MaiaResult~
        +run() Promise~MaiaResult~
        +finalize() Promise
        +tokens() Token[]
        +cst() CSTNode
        +ast() AstNode
        +plan() ExecutionPlan
        +profile() MaiaExecutionProfile
    }

    class MaiaTransaction {
        +id string
        +mode TransactionMode
        +state TransactionState
        +exec(sql, params, options) Promise~MaiaResult~
        +prepare(sql, options) Promise~MaiaStatement~
        +commit() Promise
        +rollback(reason) Promise
    }

    class MaiaResult {
        +columns MaiaColumnMetadata[]
        +rows unknown[]
        +rowsAffected number
        +insertId number|bigint|null
        +statementType string
        +warnings MaiaDiagnostic[]
    }

    class SqlCompiler {
        +compile(sql, options) CompiledStatement
    }

    class CompiledStatement {
        +sql string
        +tokens Token[]
        +cst CSTNode
        +ast AstNode
        +diagnostics MaiaDiagnostic[]
        +statementType string
    }

    class SemanticAnalyzer {
        +analyze(ast, context) AnalyzedStatement
    }

    class QueryPlanner {
        +plan(statement, context) ExecutionPlan
    }

    class SqlExecutor {
        +execute(plan, context) Promise~MaiaResult~
    }

    class TransactionManager {
        +begin(options) Promise~StorageTransaction~
        +commit(transaction) Promise
        +rollback(transaction) Promise
    }

    class CatalogManager {
        +load(transaction) Promise~DatabaseCatalog~
        +createTable(definition, transaction) Promise
        +dropTable(name, transaction) Promise
        +createIndex(definition, transaction) Promise
        +resolveTable(name) TableSchema
        +resolveColumn(table, name) ColumnSchema
    }

    class StorageEngine {
        <<interface>>
        +open(options) Promise
        +close() Promise
        +beginTransaction(options) Promise~StorageTransaction~
        +scan(request, transaction) AsyncIterable
        +get(request, transaction) Promise
        +insert(request, transaction) Promise
        +update(request, transaction) Promise
        +delete(request, transaction) Promise
        +createTable(definition, transaction) Promise
        +dropTable(name, transaction) Promise
        +createIndex(definition, transaction) Promise
    }

    class IndexedDbStorageEngine {
        +database IDBDatabase
        +open(options) Promise
        +beginTransaction(options) Promise~IndexedDbTransaction~
    }

    MaiaSQL --> MaiaDatabase
    MaiaDatabase --> SqlCompiler
    MaiaDatabase --> MaiaStatement
    MaiaDatabase --> MaiaTransaction
    MaiaDatabase --> TransactionManager
    MaiaStatement --> CompiledStatement
    MaiaStatement --> QueryPlanner
    MaiaStatement --> SqlExecutor
    SqlCompiler --> CompiledStatement
    CompiledStatement --> SemanticAnalyzer
    SemanticAnalyzer --> QueryPlanner
    QueryPlanner --> SqlExecutor
    SqlExecutor --> MaiaResult
    SqlExecutor --> CatalogManager
    SqlExecutor --> TransactionManager
    TransactionManager --> StorageEngine
    StorageEngine <|.. IndexedDbStorageEngine

WebSQL adapter class diagram

classDiagram
    class WebSqlFactory {
        +openDatabase(name, version, displayName, estimatedSize, creationCallback) WebSqlDatabase
    }

    class WebSqlDatabase {
        +version string
        +transaction(callback, errorCallback, successCallback)
        +readTransaction(callback, errorCallback, successCallback)
        +changeVersion(oldVersion, newVersion, callback, errorCallback, successCallback)
    }

    class WebSqlTransaction {
        +executeSql(sql, arguments, callback, errorCallback)
        -enqueue(command)
        -runQueue() Promise
        -markFailed(error)
    }

    class WebSqlCommand {
        +sql string
        +params unknown[]
        +success Function
        +error Function
    }

    class WebSqlResultSet {
        +insertId number|bigint|null
        +rowsAffected number
        +rows WebSqlRowList
    }

    class WebSqlRowList {
        +length number
        +item(index) object
    }

    class WebSqlError {
        +code number
        +message string
    }

    class WebSqlResultAdapter {
        +fromMaiaResult(result) WebSqlResultSet
    }

    class WebSqlErrorAdapter {
        +fromMaiaError(error) WebSqlError
    }

    class MaiaDatabase {
        +transaction(callback, options) Promise
        +readTransaction(callback, options) Promise
    }

    WebSqlFactory --> WebSqlDatabase
    WebSqlDatabase --> MaiaDatabase
    WebSqlDatabase --> WebSqlTransaction
    WebSqlTransaction o-- WebSqlCommand
    WebSqlTransaction --> WebSqlResultAdapter
    WebSqlTransaction --> WebSqlErrorAdapter
    WebSqlResultAdapter --> WebSqlResultSet
    WebSqlResultSet --> WebSqlRowList
    WebSqlErrorAdapter --> WebSqlError

The WebSQL transaction queues executeSql() calls synchronously during the transaction callback, then executes the queue asynchronously within one native MaiaSQL transaction.

This design preserves the callback shape of WebSQL while allowing IndexedDB operations to remain asynchronous.


SQL front-end class diagram

classDiagram
    class Lexer {
        +input string
        +tokens Token[]
        +tokenize() Token[]
    }

    class Parser {
        +tokens Token[]
        +position number
        +parse() void
        +getErrorMessage() string
    }

    class ParseTreeCollector {
        +root CSTNode
        +checkpoint() object
        +restore(mark)
        +startNonterminal(name, tokenIndex)
        +terminal(type, value, tokenIndex)
        +endNonterminal(name, tokenIndex)
        +abortNonterminal(name, tokenIndex)
        +toJSON() string
        +toXml() string
    }

    class SqlAstBuilder {
        +build(cst) AstNode
        +buildStatement(node) AstStatement
        +buildExpression(node) AstExpression
    }

    class SemanticAnalyzer {
        +analyze(ast, context) AnalyzedStatement
        +resolveNames(ast, catalog)
        +inferTypes(ast)
        +validateConstraints(ast)
    }

    class DiagnosticCollector {
        +errors MaiaDiagnostic[]
        +warnings MaiaDiagnostic[]
        +error(code, message, location)
        +warning(code, message, location)
    }

    Lexer --> Parser
    Parser --> ParseTreeCollector
    ParseTreeCollector --> SqlAstBuilder
    SqlAstBuilder --> SemanticAnalyzer
    SemanticAnalyzer --> DiagnosticCollector

Execution plan class diagram

classDiagram
    class PlanNode {
        <<abstract>>
        +id string
        +execute(context) AsyncIterable
    }

    class TableScanNode {
        +table string
        +alias string
        +predicate AstExpression
    }

    class IndexScanNode {
        +table string
        +index string
        +range KeyRange
    }

    class FilterNode {
        +predicate AstExpression
    }

    class ProjectionNode {
        +expressions ProjectionItem[]
    }

    class NestedLoopJoinNode {
        +joinType string
        +condition AstExpression
    }

    class AggregateNode {
        +groupExpressions AstExpression[]
        +aggregates AggregateCall[]
    }

    class SortNode {
        +terms OrderingTerm[]
    }

    class LimitNode {
        +limit AstExpression
        +offset AstExpression
    }

    class InsertNode {
        +table string
        +columns string[]
        +source PlanNode
    }

    class UpdateNode {
        +table string
        +assignments Assignment[]
    }

    class DeleteNode {
        +table string
    }

    class DdlNode {
        +operation string
        +definition object
    }

    PlanNode <|-- TableScanNode
    PlanNode <|-- IndexScanNode
    PlanNode <|-- FilterNode
    PlanNode <|-- ProjectionNode
    PlanNode <|-- NestedLoopJoinNode
    PlanNode <|-- AggregateNode
    PlanNode <|-- SortNode
    PlanNode <|-- LimitNode
    PlanNode <|-- InsertNode
    PlanNode <|-- UpdateNode
    PlanNode <|-- DeleteNode
    PlanNode <|-- DdlNode

    FilterNode --> PlanNode : input
    ProjectionNode --> PlanNode : input
    NestedLoopJoinNode --> PlanNode : left
    NestedLoopJoinNode --> PlanNode : right
    AggregateNode --> PlanNode : input
    SortNode --> PlanNode : input
    LimitNode --> PlanNode : input

Storage class diagram

classDiagram
    class StorageEngine {
        <<interface>>
        +open(options) Promise
        +close() Promise
        +beginTransaction(options) Promise~StorageTransaction~
        +readCatalog(transaction) Promise
        +writeCatalog(catalog, transaction) Promise
        +scan(request, transaction) AsyncIterable
        +get(request, transaction) Promise
        +insert(request, transaction) Promise
        +update(request, transaction) Promise
        +delete(request, transaction) Promise
    }

    class StorageTransaction {
        <<interface>>
        +id string
        +mode string
        +commit() Promise
        +rollback() Promise
        +store(name) StorageStore
    }

    class IndexedDbStorageEngine {
        +name string
        +database IDBDatabase
        +schemaVersion number
        +open(options) Promise
        +beginTransaction(options) Promise~IndexedDbStorageTransaction~
        -upgrade(event)
    }

    class IndexedDbStorageTransaction {
        +transaction IDBTransaction
        +commit() Promise
        +rollback() Promise
        +store(name) IndexedDbStore
    }

    class IndexedDbStore {
        +get(key) Promise
        +put(value, key) Promise
        +add(value, key) Promise
        +delete(key) Promise
        +openCursor(range, direction) AsyncIterable
        +index(name) IndexedDbIndex
    }

    class CatalogManager {
        +catalog DatabaseCatalog
        +load(transaction) Promise
        +save(transaction) Promise
        +getTable(name) TableSchema
        +getIndex(name) IndexSchema
    }

    StorageEngine <|.. IndexedDbStorageEngine
    StorageTransaction <|.. IndexedDbStorageTransaction
    IndexedDbStorageEngine --> IndexedDbStorageTransaction
    IndexedDbStorageTransaction --> IndexedDbStore
    IndexedDbStorageEngine --> CatalogManager

Core sequences

Open a database

sequenceDiagram
    participant App
    participant MaiaSQL
    participant DB as MaiaDatabase
    participant Storage as IndexedDbStorageEngine
    participant Catalog as CatalogManager

    App->>MaiaSQL: open(options)
    MaiaSQL->>DB: new MaiaDatabase(options)
    DB->>Storage: open(options)
    Storage->>Storage: indexedDB.open(name, version)

    alt database upgrade required
        Storage->>Storage: create internal stores
        Storage->>Storage: initialize metadata
    end

    Storage-->>DB: storage ready
    DB->>Catalog: load()
    Catalog->>Storage: read catalog metadata
    Storage-->>Catalog: catalog records
    Catalog-->>DB: DatabaseCatalog
    DB-->>MaiaSQL: ready database
    MaiaSQL-->>App: Promise<MaiaDatabase>

Execute a native SQL statement

sequenceDiagram
    participant App
    participant DB as MaiaDatabase
    participant Compiler as SqlCompiler
    participant Analyzer as SemanticAnalyzer
    participant Planner as QueryPlanner
    participant TX as TransactionManager
    participant Executor as SqlExecutor
    participant Storage as StorageEngine

    App->>DB: exec(sql, params)
    DB->>Compiler: compile(sql)
    Compiler->>Compiler: tokenize
    Compiler->>Compiler: parse to CST
    Compiler->>Compiler: build AST
    Compiler-->>DB: CompiledStatement

    DB->>Analyzer: analyze(AST, catalog)
    Analyzer-->>DB: AnalyzedStatement
    DB->>Planner: plan(statement)
    Planner-->>DB: ExecutionPlan

    DB->>TX: begin(mode)
    TX->>Storage: beginTransaction(mode)
    Storage-->>TX: StorageTransaction

    DB->>Executor: execute(plan, context)
    Executor->>Storage: scan/insert/update/delete
    Storage-->>Executor: records/results
    Executor-->>DB: MaiaResult

    DB->>TX: commit()
    TX->>Storage: commit()
    DB-->>App: MaiaResult

Execute a prepared statement

sequenceDiagram
    participant App
    participant DB as MaiaDatabase
    participant Statement as MaiaStatement
    participant Compiler as SqlCompiler
    participant Planner as QueryPlanner
    participant Executor as SqlExecutor

    App->>DB: prepare(sql)
    DB->>Compiler: compile(sql)
    Compiler-->>DB: CompiledStatement
    DB->>Statement: new MaiaStatement(compiled)
    DB-->>App: MaiaStatement

    App->>Statement: bind(params)
    App->>Statement: all()
    Statement->>Planner: plan(compiled, params)
    Planner-->>Statement: ExecutionPlan
    Statement->>Executor: execute(plan)
    Executor-->>Statement: MaiaResult
    Statement-->>App: MaiaResult

WebSQL transaction

sequenceDiagram
    participant App
    participant WebDB as WebSqlDatabase
    participant WebTX as WebSqlTransaction
    participant MaiaDB as MaiaDatabase
    participant MaiaTX as MaiaTransaction
    participant Adapter as WebSqlResultAdapter

    App->>WebDB: transaction(callback, error, success)
    WebDB->>WebTX: new transaction queue
    WebDB->>App: invoke callback(WebTX)

    App->>WebTX: executeSql(sql1, params1, cb1, err1)
    WebTX->>WebTX: enqueue command 1
    App->>WebTX: executeSql(sql2, params2, cb2, err2)
    WebTX->>WebTX: enqueue command 2
    App-->>WebDB: callback returns

    WebDB->>MaiaDB: transaction(async nativeTx)
    MaiaDB->>MaiaTX: begin()

    loop queued commands
        WebTX->>MaiaTX: exec(sql, params)
        MaiaTX-->>WebTX: MaiaResult
        WebTX->>Adapter: fromMaiaResult(result)
        Adapter-->>WebTX: SQLResultSet
        WebTX->>App: statement success callback(WebTX, result)
    end

    alt all statements succeed
        MaiaTX->>MaiaTX: commit()
        WebDB->>App: transaction success callback()
    else unhandled statement error
        MaiaTX->>MaiaTX: rollback()
        WebDB->>App: transaction error callback(SQLError)
    end

WebSQL statement error behavior

sequenceDiagram
    participant TX as WebSqlTransaction
    participant MaiaTX as MaiaTransaction
    participant ErrorAdapter as WebSqlErrorAdapter
    participant App

    TX->>MaiaTX: exec(sql, params)
    MaiaTX-->>TX: reject(MaiaError)
    TX->>ErrorAdapter: fromMaiaError(error)
    ErrorAdapter-->>TX: SQLError

    alt statement error callback exists
        TX->>App: errorCallback(TX, SQLError)
        App-->>TX: callback return value

        alt callback returns false
            TX->>TX: continue transaction
        else callback returns anything else
            TX->>TX: mark transaction failed
        end
    else no statement error callback
        TX->>TX: mark transaction failed
    end

Read transaction

sequenceDiagram
    participant App
    participant DB as WebSqlDatabase
    participant Adapter as WebSqlTransaction
    participant MaiaDB as MaiaDatabase
    participant Storage as StorageEngine

    App->>DB: readTransaction(callback)
    DB->>Adapter: create read-only transaction
    DB->>App: callback(Adapter)
    App->>Adapter: executeSql(SELECT ...)
    Adapter->>MaiaDB: readTransaction(...)
    MaiaDB->>Storage: beginTransaction(readonly)
    Storage-->>MaiaDB: transaction
    MaiaDB-->>Adapter: result
    Adapter-->>App: SQLResultSet

Insert execution

sequenceDiagram
    participant Executor as SqlExecutor
    participant Catalog as CatalogManager
    participant Eval as ExpressionEvaluator
    participant Storage as StorageEngine
    participant Functions as FunctionRegistry

    Executor->>Catalog: resolve target table
    Catalog-->>Executor: TableSchema
    Executor->>Executor: map input columns
    Executor->>Eval: evaluate supplied values
    Eval-->>Executor: candidate row
    Executor->>Executor: apply defaults and generated columns
    Executor->>Executor: apply type affinity
    Executor->>Executor: validate NOT NULL and CHECK
    Executor->>Storage: check UNIQUE and PRIMARY KEY
    Storage-->>Executor: constraint state

    alt valid row
        Executor->>Storage: insert(row)
        Storage-->>Executor: generated key
        Executor->>Functions: update last_insert_rowid state
        Executor-->>Executor: rowsAffected = 1
    else conflict
        Executor->>Executor: apply conflict policy
    end

SELECT execution pipeline

flowchart LR
    Scan[Table or index scan]
    Join[Join]
    Where[WHERE filter]
    Group[GROUP BY and aggregates]
    Having[HAVING filter]
    Window[Window functions]
    Project[Projection]
    Distinct[DISTINCT]
    Sort[ORDER BY]
    Limit[LIMIT and OFFSET]
    Result[MaiaResult]

    Scan --> Join
    Join --> Where
    Where --> Group
    Group --> Having
    Having --> Window
    Window --> Project
    Project --> Distinct
    Distinct --> Sort
    Sort --> Limit
    Limit --> Result

Not every query uses every stage. The query planner removes unnecessary nodes.


Transaction design

Native transaction states

stateDiagram-v2
    [*] --> Active
    Active --> Committing: commit()
    Committing --> Committed: storage commit succeeds
    Committing --> Failed: storage commit fails
    Active --> RollingBack: rollback()
    Active --> RollingBack: unhandled execution error
    RollingBack --> RolledBack
    Failed --> RollingBack
    Committed --> [*]
    RolledBack --> [*]

WebSQL transaction states

stateDiagram-v2
    [*] --> Collecting
    Collecting --> Executing: callback returns
    Executing --> Executing: statement succeeds
    Executing --> Executing: handled error returns false
    Executing --> Failed: unhandled error
    Executing --> Committing: queue completed
    Committing --> Succeeded
    Failed --> RollingBack
    RollingBack --> RolledBack
    Succeeded --> [*]
    RolledBack --> [*]

Catalog model

MaiaSQL maintains a logical catalog independently of IndexedDB's physical schema.

erDiagram
    DATABASE ||--o{ TABLE : contains
    TABLE ||--o{ COLUMN : defines
    TABLE ||--o{ INDEX : owns
    TABLE ||--o{ CONSTRAINT : enforces
    TABLE ||--o{ TRIGGER : invokes
    VIEW }o--|| DATABASE : belongs_to

    DATABASE {
        string name
        integer userVersion
        integer schemaVersion
    }

    TABLE {
        string name
        string storageName
        boolean strict
        boolean withoutRowid
        string primaryKeyStrategy
    }

    COLUMN {
        string name
        string declaredType
        string affinity
        boolean nullable
        string defaultExpression
        boolean generated
    }

    INDEX {
        string name
        boolean unique
        string predicate
    }

    CONSTRAINT {
        string name
        string type
        string conflictPolicy
    }

    TRIGGER {
        string name
        string timing
        string event
    }

    VIEW {
        string name
        string sql
    }

The catalog should be versioned and persisted in internal stores such as:

__maiasql_catalog
__maiasql_sequences
__maiasql_metadata

User table storage names should not collide with internal stores.


Error model

Native error hierarchy

classDiagram
    class MaiaError {
        +code string
        +message string
        +cause Error
        +location SourceLocation
        +details object
    }

    class MaiaSyntaxError
    class MaiaSemanticError
    class MaiaConstraintError
    class MaiaTransactionError
    class MaiaStorageError
    class MaiaVersionError
    class MaiaAbortError

    MaiaError <|-- MaiaSyntaxError
    MaiaError <|-- MaiaSemanticError
    MaiaError <|-- MaiaConstraintError
    MaiaError <|-- MaiaTransactionError
    MaiaError <|-- MaiaStorageError
    MaiaError <|-- MaiaVersionError
    MaiaError <|-- MaiaAbortError

WebSQL error mapping

| MaiaSQL error | WebSQL code | |---|---:| | Unknown error | 0 | | Database or storage error | 1 | | Version mismatch | 2 | | Result or statement too large | 3 | | Quota exceeded | 4 | | SQL syntax or semantic error | 5 | | Constraint violation | 6 | | Timeout or abort | 7 |

The WebSQL adapter is responsible for converting MaiaError into an SQLError-compatible object.


Proposed project structure

maiasql/
├── README.md
├── LICENSE
├── package.json
├── grammar/
│   ├── SQL.ebnf
│   └── generated/
│       └── parser.js
├── src/
│   ├── api/
│   │   ├── MaiaSQL.js
│   │   ├── MaiaDatabase.js
│   │   ├── MaiaTransaction.js
│   │   ├── MaiaStatement.js
│   │   └── MaiaResult.js
│   ├── websql/
│   │   ├── installWebSqlCompat.js
│   │   ├── WebSqlDatabase.js
│   │   ├── WebSqlTransaction.js
│   │   ├── WebSqlCommand.js
│   │   ├── WebSqlResultAdapter.js
│   │   ├── WebSqlErrorAdapter.js
│   │   └── WebSqlRowList.js
│   ├── frontend/
│   │   ├── SqlCompiler.js
│   │   ├── ParseTreeCollector.js
│   │   ├── SqlAstBuilder.js
│   │   ├── SemanticAnalyzer.js
│   │   └── DiagnosticCollector.js
│   ├── ast/
│   │   ├── statements/
│   │   ├── expressions/
│   │   └── visitors/
│   ├── planner/
│   │   ├── QueryPlanner.js
│   │   ├── ExecutionPlan.js
│   │   └── nodes/
│   ├── executor/
│   │   ├── SqlExecutor.js
│   │   ├── ExpressionEvaluator.js
│   │   ├── FunctionRegistry.js
│   │   ├── AggregateRegistry.js
│   │   └── WindowFunctionRegistry.js
│   ├── catalog/
│   │   ├── CatalogManager.js
│   │   ├── DatabaseCatalog.js
│   │   ├── TableSchema.js
│   │   ├── ColumnSchema.js
│   │   └── IndexSchema.js
│   ├── storage/
│   │   ├── StorageEngine.js
│   │   ├── StorageTransaction.js
│   │   ├── memory/
│   │   │   └── MemoryStorageEngine.js
│   │   └── indexeddb/
│   │       ├── IndexedDbStorageEngine.js
│   │       ├── IndexedDbStorageTransaction.js
│   │       ├── IndexedDbStore.js
│   │       └── DatabaseLockManager.js
│   ├── transactions/
│   │   └── TransactionManager.js
│   ├── errors/
│   │   ├── MaiaError.js
│   │   └── errorCodes.js
│   └── utils/
├── tests/
│   ├── lexer/
│   ├── parser/
│   ├── ast/
│   ├── semantic/
│   ├── planner/
│   ├── executor/
│   ├── storage/
│   ├── websql/
│   └── integration/
├── examples/
└── docs/

Implementation order

The first implementation should follow a vertical slice rather than building every subsystem completely.

flowchart LR
    A[1. Native API shell]
    B[2. SqlCompiler and AST]
    C[3. Memory storage engine]
    D[4. CREATE TABLE]
    E[5. INSERT]
    F[6. SELECT table scan]
    G[7. WHERE and expressions]
    H[8. Transactions]
    I[9. IndexedDB backend]
    J[10. WebSQL adapter]
    K[11. UPDATE and DELETE]
    L[12. Indexes and joins]

    A --> B --> C --> D --> E --> F --> G --> H --> I --> J --> K --> L

First usable milestone

The first end-to-end milestone should support:

CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL
);

INSERT INTO users(name) VALUES (?);

SELECT id, name
FROM users
WHERE id >= ?
ORDER BY id;

Through both APIs:

const db = await MaiaSQL.openMemory();
await db.exec(...);

and:

const db = openDatabase(...);
db.transaction(...);

A memory backend should be implemented before IndexedDB. This allows the parser, AST builder, semantic analyzer, executor, result model, and WebSQL adapter to be tested without IndexedDB transaction-lifetime constraints.


Testing strategy

flowchart TB
    GrammarTests[Grammar and parser tests]
    AstTests[AST snapshot tests]
    SemanticTests[Semantic validation tests]
    PlannerTests[Plan snapshot tests]
    ExecutorTests[Executor tests with memory storage]
    StorageTests[IndexedDB storage tests]
    WebSQLTests[WebSQL compatibility tests]
    IntegrationTests[End-to-end browser tests]

    GrammarTests --> AstTests
    AstTests --> SemanticTests
    SemanticTests --> PlannerTests
    PlannerTests --> ExecutorTests
    ExecutorTests --> StorageTests
    ExecutorTests --> WebSQLTests
    StorageTests --> IntegrationTests
    WebSQLTests --> IntegrationTests

Compatibility tests should verify not only successful queries, but also:

  • callback order;
  • transaction rollback behavior;
  • statement error callback return semantics;
  • rows.length;
  • rows.item(index);
  • rowsAffected;
  • insertId;
  • database version handling;
  • read-only transaction enforcement;
  • parameter binding;
  • NULL, NaN, Date, blobs, and 64-bit integer behavior.

Introspection API

The native API may expose internal representations without affecting compatibility APIs.

const statement = await db.prepare(
  "SELECT id, name FROM users WHERE id >= ?"
);

console.log(statement.tokens());
console.log(statement.cst());
console.log(statement.ast());
console.log(statement.plan());
console.log(statement.profile());

This enables:

  • SQL teaching tools;
  • query-plan visualizers;
  • parser diagnostics;
  • editor integration;
  • syntax highlighting;
  • explain tools;
  • execution profiling.

Maia ecosystem

MaiaSQL is part of the Maia project family:

  • MaiaCC
  • MaiaC
  • MaiaCpp
  • MaiaJS
  • MaiaWASM
  • MaiaSQL

Its parser is generated from EBNF by MaiaCC, using the same parser-generation infrastructure as the other Maia language projects.


License

Apache 2.0