@optimystic/quereus-plugin-optimystic 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +358 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +2973 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-B1lVAVv0.d.ts +596 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.js +2873 -0
- package/dist/plugin.js.map +1 -0
- package/examples/README.md +232 -0
- package/examples/quoomb.config.absolute.json +22 -0
- package/examples/quoomb.config.dev.json +16 -0
- package/examples/quoomb.config.linked.json +25 -0
- package/examples/quoomb.config.local.json +22 -0
- package/examples/quoomb.config.node1.json +23 -0
- package/examples/quoomb.config.node2.env.json +28 -0
- package/examples/quoomb.config.node2.json +29 -0
- package/examples/quoomb.config.single-node.json +25 -0
- package/examples/quoomb.config.web.json +30 -0
- package/examples/start-mesh.ps1 +48 -0
- package/examples/start-mesh.sh +44 -0
- package/examples/test-comprehensive.sql +25 -0
- package/examples/test-manual-plugin.txt +23 -0
- package/examples/test-multi-insert.sql +11 -0
- package/examples/test-network.sql +20 -0
- package/examples/test-plugin.sql +20 -0
- package/examples/test-single-insert.sql +20 -0
- package/examples/test-single-node.sql +20 -0
- package/package.json +131 -0
- package/src/functions/transaction-id.ts +29 -0
- package/src/index.ts +42 -0
- package/src/optimystic-adapter/collection-factory.ts +337 -0
- package/src/optimystic-adapter/key-network.ts +106 -0
- package/src/optimystic-adapter/txn-bridge.ts +272 -0
- package/src/optimystic-adapter/vtab-connection.ts +76 -0
- package/src/optimystic-module.ts +1064 -0
- package/src/plugin.ts +64 -0
- package/src/schema/index-manager.ts +266 -0
- package/src/schema/row-codec.ts +312 -0
- package/src/schema/schema-manager.ts +217 -0
- package/src/schema/statistics-collector.ts +173 -0
- package/src/transaction/index.ts +16 -0
- package/src/transaction/quereus-engine.ts +174 -0
- package/src/types.ts +91 -0
- package/src/util/generate-transaction-id.ts +41 -0
- package/test/README.md +313 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SchemaManager - Manages table schemas in Optimystic trees
|
|
3
|
+
*
|
|
4
|
+
* Stores and retrieves table schema definitions from distributed Optimystic trees.
|
|
5
|
+
* Schema is stored in a dedicated tree at `tree://schema/{tableName}`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Tree } from '@optimystic/db-core';
|
|
9
|
+
import type { TableSchema, ColumnSchema } from '@quereus/quereus';
|
|
10
|
+
import type { ITransactor } from '@optimystic/db-core';
|
|
11
|
+
|
|
12
|
+
// IndexSchema type from TableSchema.indexes
|
|
13
|
+
type IndexSchema = NonNullable<TableSchema['indexes']>[number];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Serializable schema storage format
|
|
17
|
+
*/
|
|
18
|
+
export interface StoredTableSchema {
|
|
19
|
+
name: string;
|
|
20
|
+
schemaName: string;
|
|
21
|
+
columns: StoredColumnSchema[];
|
|
22
|
+
primaryKeyDefinition: StoredPrimaryKeyColumn[];
|
|
23
|
+
indexes: StoredIndexSchema[];
|
|
24
|
+
vtabModuleName: string;
|
|
25
|
+
vtabArgs?: Record<string, any>;
|
|
26
|
+
isTemporary?: boolean;
|
|
27
|
+
estimatedRows?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StoredColumnSchema {
|
|
31
|
+
name: string;
|
|
32
|
+
affinity: string;
|
|
33
|
+
notNull: boolean;
|
|
34
|
+
primaryKey: boolean;
|
|
35
|
+
pkOrder: number;
|
|
36
|
+
defaultValue?: any;
|
|
37
|
+
collation: string;
|
|
38
|
+
generated: boolean;
|
|
39
|
+
pkDirection?: 'asc' | 'desc';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StoredPrimaryKeyColumn {
|
|
43
|
+
index: number;
|
|
44
|
+
desc?: boolean;
|
|
45
|
+
autoIncrement?: boolean;
|
|
46
|
+
collation?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StoredIndexSchema {
|
|
50
|
+
name: string;
|
|
51
|
+
columns: StoredIndexColumn[];
|
|
52
|
+
unique?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StoredIndexColumn {
|
|
56
|
+
index: number;
|
|
57
|
+
desc?: boolean;
|
|
58
|
+
collation?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Manages schema storage and retrieval in Optimystic trees
|
|
63
|
+
*/
|
|
64
|
+
export class SchemaManager {
|
|
65
|
+
private schemaCache = new Map<string, StoredTableSchema>();
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly getSchemaTree: (transactor?: ITransactor) => Promise<Tree<string, any>>
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Store a table schema
|
|
73
|
+
*/
|
|
74
|
+
async storeSchema(schema: TableSchema, transactor?: ITransactor): Promise<void> {
|
|
75
|
+
const stored = this.tableSchemaToStored(schema);
|
|
76
|
+
this.schemaCache.set(schema.name, stored);
|
|
77
|
+
|
|
78
|
+
const tree = await this.getSchemaTree(transactor);
|
|
79
|
+
await tree.replace([[schema.name, stored]]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retrieve a table schema
|
|
84
|
+
*/
|
|
85
|
+
async getSchema(tableName: string, transactor?: ITransactor): Promise<StoredTableSchema | undefined> {
|
|
86
|
+
// Check cache first
|
|
87
|
+
const cached = this.schemaCache.get(tableName);
|
|
88
|
+
if (cached) {
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Load from tree
|
|
93
|
+
const tree = await this.getSchemaTree(transactor);
|
|
94
|
+
const path = await tree.find(tableName);
|
|
95
|
+
if (!tree.isValid(path)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const entry = tree.at(path) as [string, StoredTableSchema];
|
|
100
|
+
if (entry && entry.length >= 2) {
|
|
101
|
+
const stored = entry[1];
|
|
102
|
+
this.schemaCache.set(tableName, stored);
|
|
103
|
+
return stored;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Delete a table schema
|
|
111
|
+
*/
|
|
112
|
+
async deleteSchema(tableName: string, transactor?: ITransactor): Promise<void> {
|
|
113
|
+
this.schemaCache.delete(tableName);
|
|
114
|
+
|
|
115
|
+
const tree = await this.getSchemaTree(transactor);
|
|
116
|
+
await tree.replace([[tableName, undefined]]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List all table names
|
|
121
|
+
*/
|
|
122
|
+
async listTables(transactor?: ITransactor): Promise<string[]> {
|
|
123
|
+
const tree = await this.getSchemaTree(transactor);
|
|
124
|
+
const tables: string[] = [];
|
|
125
|
+
|
|
126
|
+
for await (const path of tree.range({ isAscending: true } as any)) {
|
|
127
|
+
if (!tree.isValid(path)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const entry = tree.at(path) as [string, any];
|
|
132
|
+
if (entry && entry.length >= 1) {
|
|
133
|
+
tables.push(entry[0]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return tables;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear the schema cache
|
|
142
|
+
*/
|
|
143
|
+
clearCache(): void {
|
|
144
|
+
this.schemaCache.clear();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert TableSchema to storable format
|
|
149
|
+
*/
|
|
150
|
+
private tableSchemaToStored(schema: TableSchema): StoredTableSchema {
|
|
151
|
+
return {
|
|
152
|
+
name: schema.name,
|
|
153
|
+
schemaName: schema.schemaName,
|
|
154
|
+
columns: schema.columns.map(col => this.columnSchemaToStored(col)),
|
|
155
|
+
primaryKeyDefinition: schema.primaryKeyDefinition.map(pk => ({
|
|
156
|
+
index: pk.index,
|
|
157
|
+
desc: pk.desc,
|
|
158
|
+
autoIncrement: pk.autoIncrement,
|
|
159
|
+
collation: pk.collation,
|
|
160
|
+
})),
|
|
161
|
+
indexes: (schema.indexes || []).map(idx => this.indexSchemaToStored(idx)),
|
|
162
|
+
vtabModuleName: schema.vtabModuleName,
|
|
163
|
+
vtabArgs: schema.vtabArgs as Record<string, any>,
|
|
164
|
+
isTemporary: schema.isTemporary,
|
|
165
|
+
estimatedRows: schema.estimatedRows,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Convert ColumnSchema to storable format
|
|
171
|
+
*/
|
|
172
|
+
private columnSchemaToStored(col: ColumnSchema): StoredColumnSchema {
|
|
173
|
+
return {
|
|
174
|
+
name: col.name,
|
|
175
|
+
affinity: col.logicalType.name, // Use logicalType.name for storage
|
|
176
|
+
notNull: col.notNull,
|
|
177
|
+
primaryKey: col.primaryKey,
|
|
178
|
+
pkOrder: col.pkOrder,
|
|
179
|
+
defaultValue: col.defaultValue ? this.serializeExpression(col.defaultValue) : undefined,
|
|
180
|
+
collation: col.collation,
|
|
181
|
+
generated: col.generated,
|
|
182
|
+
pkDirection: col.pkDirection,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Convert IndexSchema to storable format
|
|
188
|
+
*/
|
|
189
|
+
private indexSchemaToStored(idx: IndexSchema): StoredIndexSchema {
|
|
190
|
+
return {
|
|
191
|
+
name: idx.name,
|
|
192
|
+
columns: idx.columns.map((col: { index: number; desc?: boolean; collation?: string }) => ({
|
|
193
|
+
index: col.index,
|
|
194
|
+
desc: col.desc,
|
|
195
|
+
collation: col.collation,
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Serialize an expression for storage
|
|
202
|
+
* For now, we'll store a simplified representation
|
|
203
|
+
*/
|
|
204
|
+
private serializeExpression(expr: any): any {
|
|
205
|
+
// TODO: Implement proper expression serialization
|
|
206
|
+
// For now, just store the expression as-is if it's a simple value
|
|
207
|
+
if (typeof expr === 'object' && expr !== null) {
|
|
208
|
+
if ('type' in expr && expr.type === 'literal') {
|
|
209
|
+
return { type: 'literal', value: expr.value };
|
|
210
|
+
}
|
|
211
|
+
// For complex expressions, we'll need to implement full serialization
|
|
212
|
+
return { type: 'complex', raw: JSON.stringify(expr) };
|
|
213
|
+
}
|
|
214
|
+
return expr;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatisticsCollector - Collects and maintains table statistics for query optimization
|
|
3
|
+
*
|
|
4
|
+
* Tracks row counts, distinct values, and provides cost estimates for query planning.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StoredTableSchema } from './schema-manager.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Statistics for a single column
|
|
11
|
+
*/
|
|
12
|
+
export interface ColumnStatistics {
|
|
13
|
+
/** Approximate number of distinct values */
|
|
14
|
+
distinctCount: number;
|
|
15
|
+
/** Approximate number of NULL values */
|
|
16
|
+
nullCount: number;
|
|
17
|
+
/** Sample of values for histogram (optional) */
|
|
18
|
+
sampleValues?: unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Statistics for a table
|
|
23
|
+
*/
|
|
24
|
+
export interface TableStatistics {
|
|
25
|
+
/** Total number of rows (approximate) */
|
|
26
|
+
rowCount: number;
|
|
27
|
+
/** Statistics per column */
|
|
28
|
+
columnStats: Map<number, ColumnStatistics>;
|
|
29
|
+
/** Last update timestamp */
|
|
30
|
+
lastUpdated: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Collects and maintains statistics for query optimization
|
|
35
|
+
*/
|
|
36
|
+
export class StatisticsCollector {
|
|
37
|
+
private stats: TableStatistics;
|
|
38
|
+
|
|
39
|
+
constructor(private schema: StoredTableSchema) {
|
|
40
|
+
this.stats = {
|
|
41
|
+
rowCount: 0,
|
|
42
|
+
columnStats: new Map(),
|
|
43
|
+
lastUpdated: Date.now(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Initialize column stats
|
|
47
|
+
for (let i = 0; i < schema.columns.length; i++) {
|
|
48
|
+
this.stats.columnStats.set(i, {
|
|
49
|
+
distinctCount: 0,
|
|
50
|
+
nullCount: 0,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get current table statistics
|
|
57
|
+
*/
|
|
58
|
+
getStatistics(): TableStatistics {
|
|
59
|
+
return this.stats;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get estimated row count
|
|
64
|
+
*/
|
|
65
|
+
getRowCount(): number {
|
|
66
|
+
return this.stats.rowCount;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get estimated distinct count for a column
|
|
71
|
+
*/
|
|
72
|
+
getDistinctCount(columnIndex: number): number {
|
|
73
|
+
const colStats = this.stats.columnStats.get(columnIndex);
|
|
74
|
+
return colStats?.distinctCount || 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Increment row count (called on INSERT)
|
|
79
|
+
*/
|
|
80
|
+
incrementRowCount(): void {
|
|
81
|
+
this.stats.rowCount++;
|
|
82
|
+
this.stats.lastUpdated = Date.now();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Decrement row count (called on DELETE)
|
|
87
|
+
*/
|
|
88
|
+
decrementRowCount(): void {
|
|
89
|
+
this.stats.rowCount = Math.max(0, this.stats.rowCount - 1);
|
|
90
|
+
this.stats.lastUpdated = Date.now();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Estimate selectivity of an equality constraint
|
|
95
|
+
* Returns a value between 0 and 1 representing the fraction of rows that match
|
|
96
|
+
*/
|
|
97
|
+
estimateEqualitySelectivity(columnIndex: number): number {
|
|
98
|
+
const distinctCount = this.getDistinctCount(columnIndex);
|
|
99
|
+
if (distinctCount === 0) {
|
|
100
|
+
return 0.1; // Default estimate
|
|
101
|
+
}
|
|
102
|
+
return 1.0 / distinctCount;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Estimate selectivity of a range constraint
|
|
107
|
+
* Returns a value between 0 and 1 representing the fraction of rows that match
|
|
108
|
+
*/
|
|
109
|
+
estimateRangeSelectivity(_columnIndex: number): number {
|
|
110
|
+
// Simple heuristic: assume range matches 25% of rows
|
|
111
|
+
return 0.25;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Estimate cost of a full table scan
|
|
116
|
+
*/
|
|
117
|
+
estimateTableScanCost(): number {
|
|
118
|
+
// Cost is proportional to row count
|
|
119
|
+
// Base cost of 1.0 per row
|
|
120
|
+
return Math.max(1000, this.stats.rowCount);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Estimate cost of an index scan
|
|
125
|
+
*/
|
|
126
|
+
estimateIndexScanCost(selectivity: number): number {
|
|
127
|
+
// Cost includes:
|
|
128
|
+
// 1. Index lookup cost (logarithmic)
|
|
129
|
+
// 2. Row fetch cost (proportional to selected rows)
|
|
130
|
+
const indexLookupCost = Math.log2(Math.max(1, this.stats.rowCount)) * 2;
|
|
131
|
+
const rowFetchCost = this.stats.rowCount * selectivity;
|
|
132
|
+
return indexLookupCost + rowFetchCost;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Estimate number of rows returned by a constraint
|
|
137
|
+
*/
|
|
138
|
+
estimateRowsForConstraint(columnIndex: number, isEquality: boolean): number {
|
|
139
|
+
if (isEquality) {
|
|
140
|
+
const selectivity = this.estimateEqualitySelectivity(columnIndex);
|
|
141
|
+
return Math.max(1, Math.floor(this.stats.rowCount * selectivity));
|
|
142
|
+
} else {
|
|
143
|
+
const selectivity = this.estimateRangeSelectivity(columnIndex);
|
|
144
|
+
return Math.max(1, Math.floor(this.stats.rowCount * selectivity));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update statistics based on actual data (called periodically)
|
|
150
|
+
* This is a simplified version - a real implementation would sample the data
|
|
151
|
+
*/
|
|
152
|
+
async updateStatistics(sampleSize = 1000): Promise<void> {
|
|
153
|
+
// TODO: Implement actual statistics collection by sampling the table
|
|
154
|
+
// For now, we just update the timestamp
|
|
155
|
+
this.stats.lastUpdated = Date.now();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reset statistics
|
|
160
|
+
*/
|
|
161
|
+
reset(): void {
|
|
162
|
+
this.stats.rowCount = 0;
|
|
163
|
+
this.stats.columnStats.clear();
|
|
164
|
+
for (let i = 0; i < this.schema.columns.length; i++) {
|
|
165
|
+
this.stats.columnStats.set(i, {
|
|
166
|
+
distinctCount: 0,
|
|
167
|
+
nullCount: 0,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
this.stats.lastUpdated = Date.now();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction support for Quereus-Optimystic integration.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the QuereusEngine for executing SQL transactions
|
|
5
|
+
* through the Optimystic distributed transaction system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
QuereusEngine,
|
|
10
|
+
QUEREUS_ENGINE_ID,
|
|
11
|
+
createQuereusStatement,
|
|
12
|
+
createQuereusStatements
|
|
13
|
+
} from './quereus-engine.js';
|
|
14
|
+
|
|
15
|
+
export type { QuereusStatement } from './quereus-engine.js';
|
|
16
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Database, SqlParameters } from '@quereus/quereus';
|
|
2
|
+
import type {
|
|
3
|
+
ITransactionEngine,
|
|
4
|
+
Transaction,
|
|
5
|
+
ExecutionResult,
|
|
6
|
+
CollectionActions,
|
|
7
|
+
TransactionCoordinator
|
|
8
|
+
} from '@optimystic/db-core';
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Engine ID for Quereus SQL transactions.
|
|
13
|
+
* Format: "quereus@{version}" where version matches the quereus package version.
|
|
14
|
+
*/
|
|
15
|
+
export const QUEREUS_ENGINE_ID = 'quereus@0.5.3';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Statement format for Quereus transactions.
|
|
19
|
+
* Each statement is a SQL string with optional parameters.
|
|
20
|
+
*/
|
|
21
|
+
export type QuereusStatement = {
|
|
22
|
+
/** The SQL statement to execute */
|
|
23
|
+
sql: string;
|
|
24
|
+
/** Optional parameters for the statement */
|
|
25
|
+
params?: SqlParameters;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Quereus-specific transaction engine for SQL execution.
|
|
30
|
+
*
|
|
31
|
+
* This engine:
|
|
32
|
+
* 1. Executes SQL statements through a Quereus database
|
|
33
|
+
* 2. Collects resulting actions from the virtual table module
|
|
34
|
+
* 3. Computes schema hash for validation
|
|
35
|
+
*
|
|
36
|
+
* Used for both initial execution (client creating transaction) and
|
|
37
|
+
* re-execution (validators verifying transaction).
|
|
38
|
+
*/
|
|
39
|
+
export class QuereusEngine implements ITransactionEngine {
|
|
40
|
+
private schemaHashCache: string | undefined;
|
|
41
|
+
private schemaVersion: number = 0;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
private readonly db: Database,
|
|
45
|
+
private readonly coordinator: TransactionCoordinator
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Execute a transaction's statements and produce actions.
|
|
50
|
+
*
|
|
51
|
+
* For initial execution: Executes SQL through Quereus, which triggers
|
|
52
|
+
* the Optimystic virtual table module to apply actions.
|
|
53
|
+
*
|
|
54
|
+
* For validation: Re-executes the same SQL statements to verify
|
|
55
|
+
* they produce the same operations.
|
|
56
|
+
*/
|
|
57
|
+
async execute(transaction: Transaction): Promise<ExecutionResult> {
|
|
58
|
+
try {
|
|
59
|
+
const allActions: CollectionActions[] = [];
|
|
60
|
+
|
|
61
|
+
for (const statementJson of transaction.statements) {
|
|
62
|
+
const statement = JSON.parse(statementJson) as QuereusStatement;
|
|
63
|
+
|
|
64
|
+
// Execute SQL through Quereus
|
|
65
|
+
// The Optimystic virtual table module will:
|
|
66
|
+
// 1. Translate SQL mutations to actions
|
|
67
|
+
// 2. Call coordinator.applyActions() with the stampId
|
|
68
|
+
await this.db.exec(statement.sql, statement.params);
|
|
69
|
+
|
|
70
|
+
// Note: Actions are collected by the coordinator's trackers,
|
|
71
|
+
// not returned directly from exec(). The coordinator tracks
|
|
72
|
+
// all actions applied during this transaction.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
actions: allActions
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: `Failed to execute SQL transaction: ${error instanceof Error ? error.message : String(error)}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the schema hash for this engine.
|
|
89
|
+
*
|
|
90
|
+
* The schema hash is used for validation - all participants must have
|
|
91
|
+
* matching schema hashes for a transaction to be valid.
|
|
92
|
+
*
|
|
93
|
+
* Uses caching to avoid recomputing if schema hasn't changed.
|
|
94
|
+
*/
|
|
95
|
+
async getSchemaHash(): Promise<string> {
|
|
96
|
+
// Check if we have a cached hash
|
|
97
|
+
if (this.schemaHashCache !== undefined) {
|
|
98
|
+
return this.schemaHashCache;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Compute and cache the schema hash
|
|
102
|
+
this.schemaHashCache = await this.computeSchemaHash();
|
|
103
|
+
return this.schemaHashCache;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Invalidate the schema hash cache.
|
|
108
|
+
* Call this when the schema changes (e.g., after DDL statements).
|
|
109
|
+
*/
|
|
110
|
+
invalidateSchemaCache(): void {
|
|
111
|
+
this.schemaHashCache = undefined;
|
|
112
|
+
this.schemaVersion++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the current schema version number.
|
|
117
|
+
* Increments each time the schema cache is invalidated.
|
|
118
|
+
*/
|
|
119
|
+
getSchemaVersion(): number {
|
|
120
|
+
return this.schemaVersion;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Compute the schema hash from the database catalog.
|
|
125
|
+
*
|
|
126
|
+
* Uses the schema() table-valued function to get schema information,
|
|
127
|
+
* then hashes the canonical representation using SHA-256.
|
|
128
|
+
*/
|
|
129
|
+
private async computeSchemaHash(): Promise<string> {
|
|
130
|
+
// Query schema information from Quereus
|
|
131
|
+
const schemaInfo: Array<{ type: string; name: string; sql: string | null }> = [];
|
|
132
|
+
|
|
133
|
+
for await (const row of this.db.eval("select type, name, sql from schema() order by type, name")) {
|
|
134
|
+
schemaInfo.push({
|
|
135
|
+
type: row.type as string,
|
|
136
|
+
name: row.name as string,
|
|
137
|
+
sql: row.sql as string | null
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Serialize to canonical JSON
|
|
142
|
+
const catalogJson = JSON.stringify(schemaInfo);
|
|
143
|
+
|
|
144
|
+
// Compute SHA-256 hash using @noble/hashes
|
|
145
|
+
const hashBytes = sha256(new TextEncoder().encode(catalogJson));
|
|
146
|
+
// Use first 16 bytes encoded as base64url for compact representation
|
|
147
|
+
const hashBase64 = bytesToBase64url(hashBytes.slice(0, 16));
|
|
148
|
+
return `schema:${hashBase64}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert bytes to base64url encoding (URL-safe, no padding).
|
|
154
|
+
*/
|
|
155
|
+
function bytesToBase64url(bytes: Uint8Array): string {
|
|
156
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
157
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Helper to create Quereus statement JSON for a transaction.
|
|
162
|
+
*/
|
|
163
|
+
export function createQuereusStatement(sql: string, params?: SqlParameters): string {
|
|
164
|
+
const statement: QuereusStatement = { sql, params };
|
|
165
|
+
return JSON.stringify(statement);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Helper to create an array of Quereus statements for a transaction.
|
|
170
|
+
*/
|
|
171
|
+
export function createQuereusStatements(statements: Array<{ sql: string; params?: SqlParameters }>): string[] {
|
|
172
|
+
return statements.map(s => createQuereusStatement(s.sql, s.params));
|
|
173
|
+
}
|
|
174
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
2
|
+
import type { IKeyNetwork, ITransactor } from '@optimystic/db-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for the optimystic virtual table
|
|
6
|
+
*/
|
|
7
|
+
export interface OptimysticOptions {
|
|
8
|
+
/** URI for the collection (e.g., 'tree://mydb/users') */
|
|
9
|
+
collectionUri: string;
|
|
10
|
+
|
|
11
|
+
/** Transactor type - 'network', 'test', or custom class name */
|
|
12
|
+
transactor?: 'network' | 'test' | string;
|
|
13
|
+
|
|
14
|
+
/** Key network type - 'libp2p', 'test', or custom class name */
|
|
15
|
+
keyNetwork?: 'libp2p' | 'test' | string;
|
|
16
|
+
|
|
17
|
+
/** Existing libp2p instance to use (optional) */
|
|
18
|
+
libp2p?: Libp2p;
|
|
19
|
+
|
|
20
|
+
/** Options for creating a new libp2p node */
|
|
21
|
+
libp2pOptions?: LibP2PNodeOptions;
|
|
22
|
+
|
|
23
|
+
/** Enable local snapshot cache */
|
|
24
|
+
cache?: boolean;
|
|
25
|
+
|
|
26
|
+
/** Row encoding format */
|
|
27
|
+
encoding?: 'json' | 'msgpack';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for creating a libp2p node
|
|
32
|
+
*/
|
|
33
|
+
export interface LibP2PNodeOptions {
|
|
34
|
+
/** Network port to listen on */
|
|
35
|
+
port?: number;
|
|
36
|
+
|
|
37
|
+
/** Network name for protocol prefixes */
|
|
38
|
+
networkName?: string;
|
|
39
|
+
|
|
40
|
+
/** Bootstrap nodes for peer discovery */
|
|
41
|
+
bootstrapNodes?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Internal configuration after parsing and validation
|
|
46
|
+
*/
|
|
47
|
+
export interface ParsedOptimysticOptions {
|
|
48
|
+
collectionUri: string;
|
|
49
|
+
transactor: 'network' | 'test' | string;
|
|
50
|
+
keyNetwork: 'libp2p' | 'test' | string;
|
|
51
|
+
libp2p?: Libp2p;
|
|
52
|
+
libp2pOptions: LibP2PNodeOptions;
|
|
53
|
+
cache: boolean;
|
|
54
|
+
encoding: 'json' | 'msgpack';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Registry for custom transactor and key network implementations
|
|
59
|
+
*/
|
|
60
|
+
export interface CustomImplementationRegistry {
|
|
61
|
+
transactors: Map<string, new (...args: any[]) => ITransactor>;
|
|
62
|
+
keyNetworks: Map<string, new (...args: any[]) => IKeyNetwork>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Column definition for the virtual table
|
|
67
|
+
*/
|
|
68
|
+
export interface ColumnDefinition {
|
|
69
|
+
name: string;
|
|
70
|
+
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'NULL';
|
|
71
|
+
isPrimaryKey: boolean;
|
|
72
|
+
isNotNull: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Row data as stored in the tree
|
|
77
|
+
* Format: [primaryKey, encodedRow]
|
|
78
|
+
* - primaryKey: string representation of the primary key (composite keys are joined with \x00)
|
|
79
|
+
* - encodedRow: JSON-encoded row data
|
|
80
|
+
*/
|
|
81
|
+
export type RowData = [string, string];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Transaction state for managing Optimystic transactions
|
|
85
|
+
*/
|
|
86
|
+
export interface TransactionState {
|
|
87
|
+
transactor: ITransactor;
|
|
88
|
+
isActive: boolean;
|
|
89
|
+
collections: Map<string, any>; // Tree collections used in this transaction
|
|
90
|
+
transactionId: string; // Unique identifier for this transaction (stable within transaction, cycles between)
|
|
91
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomBytes } from '@libp2p/crypto';
|
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates a unique transaction ID that includes a hash of the peer ID
|
|
7
|
+
* to reduce collision probability across distributed nodes.
|
|
8
|
+
*
|
|
9
|
+
* Format: {peerIdHash}-{randomBytes}
|
|
10
|
+
* - peerIdHash: First 16 bytes of SHA-256 hash of peer ID (base64url)
|
|
11
|
+
* - randomBytes: 16 random bytes (base64url)
|
|
12
|
+
* - Total: 32 bytes, base64url encoded
|
|
13
|
+
*
|
|
14
|
+
* @param peerId Optional peer ID to include in the hash. If not provided, only random bytes are used.
|
|
15
|
+
* @returns A unique transaction ID string
|
|
16
|
+
*/
|
|
17
|
+
export function generateTransactionId(peerId?: string): string {
|
|
18
|
+
const randomPart = randomBytes(16);
|
|
19
|
+
|
|
20
|
+
if (peerId) {
|
|
21
|
+
// Hash the peer ID and take first 16 bytes
|
|
22
|
+
const peerIdBytes = new TextEncoder().encode(peerId);
|
|
23
|
+
const peerIdHash = sha256(peerIdBytes);
|
|
24
|
+
const peerIdHashPart = peerIdHash.slice(0, 16);
|
|
25
|
+
|
|
26
|
+
// Combine peer ID hash and random bytes
|
|
27
|
+
const combined = new Uint8Array(32);
|
|
28
|
+
combined.set(peerIdHashPart, 0);
|
|
29
|
+
combined.set(randomPart, 16);
|
|
30
|
+
|
|
31
|
+
return uint8ArrayToString(combined, 'base64url');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Fallback: just use random bytes (padded to 32 bytes for consistency)
|
|
35
|
+
const fallback = new Uint8Array(32);
|
|
36
|
+
fallback.set(randomPart, 0);
|
|
37
|
+
fallback.set(randomBytes(16), 16);
|
|
38
|
+
|
|
39
|
+
return uint8ArrayToString(fallback, 'base64url');
|
|
40
|
+
}
|
|
41
|
+
|