maiasql
v1.3.0
Published
Pure JavaScript SQL engine with a WebSQL-compatible adapter over IndexedDB or memory storage.
Maintainers
Readme
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:
- a modern, Promise-based native API;
- 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.jspasses 28 of 28 checks- the current regression for
NOT INandNOT LIKEpredicates 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 .-> OPFSThe 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 --> IDBComponent 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 --> LockManagerPublic 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 <|.. IndexedDbStorageEngineWebSQL 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 --> WebSqlErrorThe 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 --> DiagnosticCollectorExecution 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 : inputStorage 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 --> CatalogManagerCore 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: MaiaResultExecute 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: MaiaResultWebSQL 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)
endWebSQL 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
endRead 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: SQLResultSetInsert 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
endSELECT 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 --> ResultNot 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_metadataUser 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 <|-- MaiaAbortErrorWebSQL 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 --> LFirst 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 --> IntegrationTestsCompatibility 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
