@objectql/driver-memory 4.0.1 → 4.0.3

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/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { Data, System } from '@objectstack/spec';
1
+ import { Data, System as SystemSpec } from '@objectstack/spec';
2
2
  type QueryAST = Data.QueryAST;
3
- type FilterNode = Data.FilterNode;
4
3
  type SortNode = Data.SortNode;
5
- type DriverInterface = System.DriverInterface;
4
+ type DriverInterface = Data.DriverInterface;
6
5
  /**
7
6
  * ObjectQL
8
7
  * Copyright (c) 2026-present ObjectStack Inc.
@@ -39,7 +38,7 @@ type DriverInterface = System.DriverInterface;
39
38
  */
40
39
 
41
40
  import { Driver, ObjectQLError } from '@objectql/types';
42
- import { Query } from 'mingo';
41
+ import { Query, Aggregator } from 'mingo';
43
42
 
44
43
  /**
45
44
  * Command interface for executeCommand method
@@ -73,6 +72,24 @@ export interface MemoryDriverConfig {
73
72
  initialData?: Record<string, any[]>;
74
73
  /** Optional: Enable strict mode (throw on missing objects) */
75
74
  strictMode?: boolean;
75
+ /** Optional: Enable persistence to file system */
76
+ persistence?: {
77
+ /** File path to persist data */
78
+ filePath: string;
79
+ /** Auto-save interval in milliseconds (default: 5000) */
80
+ autoSaveInterval?: number;
81
+ };
82
+ /** Optional: Fields to index for faster queries */
83
+ indexes?: Record<string, string[]>; // objectName -> field names
84
+ }
85
+
86
+ /**
87
+ * In-memory transaction
88
+ */
89
+ interface MemoryTransaction {
90
+ id: string;
91
+ snapshot: Map<string, any>;
92
+ operations: Array<{ type: 'set' | 'delete'; key: string; value?: any }>;
76
93
  }
77
94
 
78
95
  /**
@@ -88,32 +105,47 @@ export class MemoryDriver implements Driver {
88
105
  public readonly name = 'MemoryDriver';
89
106
  public readonly version = '4.0.0';
90
107
  public readonly supports = {
91
- transactions: false,
108
+ transactions: true,
92
109
  joins: false,
93
110
  fullTextSearch: false,
94
111
  jsonFields: true,
95
112
  arrayFields: true,
96
113
  queryFilters: true,
97
- queryAggregations: false,
114
+ queryAggregations: true,
98
115
  querySorting: true,
99
116
  queryPagination: true,
100
117
  queryWindowFunctions: false,
101
118
  querySubqueries: false
102
119
  };
103
120
 
104
- private store: Map<string, any>;
105
- private config: MemoryDriverConfig;
106
- private idCounters: Map<string, number>;
121
+ protected store: Map<string, any>;
122
+ protected config: MemoryDriverConfig;
123
+ protected idCounters: Map<string, number>;
124
+ protected transactions: Map<string, MemoryTransaction>;
125
+ protected indexes: Map<string, Map<string, Set<string>>>; // objectName -> field -> Set<recordIds>
126
+ protected persistenceTimer?: NodeJS.Timeout;
107
127
 
108
128
  constructor(config: MemoryDriverConfig = {}) {
109
129
  this.config = config;
110
130
  this.store = new Map<string, any>();
111
131
  this.idCounters = new Map<string, number>();
132
+ this.transactions = new Map();
133
+ this.indexes = new Map();
112
134
 
113
135
  // Load initial data if provided
114
136
  if (config.initialData) {
115
137
  this.loadInitialData(config.initialData);
116
138
  }
139
+
140
+ // Set up persistence if configured
141
+ if (config.persistence) {
142
+ this.setupPersistence();
143
+ }
144
+
145
+ // Build indexes if configured
146
+ if (config.indexes) {
147
+ this.buildIndexes(config.indexes);
148
+ }
117
149
  }
118
150
 
119
151
  /**
@@ -135,7 +167,7 @@ export class MemoryDriver implements Driver {
135
167
  /**
136
168
  * Load initial data into the store.
137
169
  */
138
- private loadInitialData(data: Record<string, any[]>): void {
170
+ protected loadInitialData(data: Record<string, any[]>): void {
139
171
  for (const [objectName, records] of Object.entries(data)) {
140
172
  for (const record of records) {
141
173
  const id = record.id || this.generateId(objectName);
@@ -150,9 +182,6 @@ export class MemoryDriver implements Driver {
150
182
  * Supports filtering, sorting, pagination, and field projection using Mingo.
151
183
  */
152
184
  async find(objectName: string, query: any = {}, options?: any): Promise<any[]> {
153
- // Normalize query to support both legacy and QueryAST formats
154
- const normalizedQuery = this.normalizeQuery(query);
155
-
156
185
  // Get all records for this object type
157
186
  const pattern = `${objectName}:`;
158
187
  let records: any[] = [];
@@ -164,7 +193,7 @@ export class MemoryDriver implements Driver {
164
193
  }
165
194
 
166
195
  // Convert ObjectQL filters to MongoDB query format
167
- const mongoQuery = this.convertToMongoQuery(normalizedQuery.filters);
196
+ const mongoQuery = this.convertToMongoQuery(query.where);
168
197
 
169
198
  // Apply filters using Mingo
170
199
  if (mongoQuery && Object.keys(mongoQuery).length > 0) {
@@ -173,21 +202,21 @@ export class MemoryDriver implements Driver {
173
202
  }
174
203
 
175
204
  // Apply sorting manually (Mingo's sort has issues with CJS builds)
176
- if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort) && normalizedQuery.sort.length > 0) {
177
- records = this.applyManualSort(records, normalizedQuery.sort);
205
+ if (query.orderBy && Array.isArray(query.orderBy) && query.orderBy.length > 0) {
206
+ records = this.applyManualSort(records, query.orderBy);
178
207
  }
179
208
 
180
209
  // Apply pagination
181
- if (normalizedQuery.skip) {
182
- records = records.slice(normalizedQuery.skip);
210
+ if (query.offset) {
211
+ records = records.slice(query.offset);
183
212
  }
184
- if (normalizedQuery.limit) {
185
- records = records.slice(0, normalizedQuery.limit);
213
+ if (query.limit) {
214
+ records = records.slice(0, query.limit);
186
215
  }
187
216
 
188
217
  // Apply field projection
189
- if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
190
- records = records.map(doc => this.projectFields(doc, normalizedQuery.fields));
218
+ if (query.fields && Array.isArray(query.fields)) {
219
+ records = records.map(doc => this.projectFields(doc, query.fields));
191
220
  }
192
221
 
193
222
  return records;
@@ -239,6 +268,10 @@ export class MemoryDriver implements Driver {
239
268
  };
240
269
 
241
270
  this.store.set(key, doc);
271
+
272
+ // Update indexes
273
+ this.updateIndex(objectName, id, doc);
274
+
242
275
  return { ...doc };
243
276
  }
244
277
 
@@ -269,6 +302,10 @@ export class MemoryDriver implements Driver {
269
302
  };
270
303
 
271
304
  this.store.set(key, doc);
305
+
306
+ // Update indexes
307
+ this.updateIndex(objectName, id.toString(), doc);
308
+
272
309
  return { ...doc };
273
310
  }
274
311
 
@@ -279,6 +316,11 @@ export class MemoryDriver implements Driver {
279
316
  const key = `${objectName}:${id}`;
280
317
  const deleted = this.store.delete(key);
281
318
 
319
+ // Remove from indexes
320
+ if (deleted) {
321
+ this.removeFromIndex(objectName, id.toString());
322
+ }
323
+
282
324
  if (!deleted && this.config.strictMode) {
283
325
  throw new ObjectQLError({
284
326
  code: 'RECORD_NOT_FOUND',
@@ -296,10 +338,10 @@ export class MemoryDriver implements Driver {
296
338
  async count(objectName: string, filters: any, options?: any): Promise<number> {
297
339
  const pattern = `${objectName}:`;
298
340
 
299
- // Extract actual filters from query object if needed
300
- let actualFilters = filters;
301
- if (filters && !Array.isArray(filters) && filters.filters) {
302
- actualFilters = filters.filters;
341
+ // Extract where condition from query object if needed
342
+ let whereCondition = filters;
343
+ if (filters && !Array.isArray(filters) && filters.where) {
344
+ whereCondition = filters.where;
303
345
  }
304
346
 
305
347
  // Get all records for this object type
@@ -311,12 +353,12 @@ export class MemoryDriver implements Driver {
311
353
  }
312
354
 
313
355
  // If no filters, return total count
314
- if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
356
+ if (!whereCondition || (Array.isArray(whereCondition) && whereCondition.length === 0)) {
315
357
  return records.length;
316
358
  }
317
359
 
318
360
  // Convert to MongoDB query and use Mingo to count
319
- const mongoQuery = this.convertToMongoQuery(actualFilters);
361
+ const mongoQuery = this.convertToMongoQuery(whereCondition);
320
362
  if (mongoQuery && Object.keys(mongoQuery).length > 0) {
321
363
  const mingoQuery = new Query(mongoQuery);
322
364
  const matchedRecords = mingoQuery.find(records).all();
@@ -472,62 +514,327 @@ export class MemoryDriver implements Driver {
472
514
  }
473
515
 
474
516
  /**
475
- * Disconnect (no-op for memory driver).
517
+ * Disconnect and cleanup resources.
476
518
  */
477
519
  async disconnect(): Promise<void> {
478
- // No-op: Memory driver doesn't need cleanup
520
+ // Save data to disk if persistence is enabled
521
+ if (this.config.persistence) {
522
+ this.saveToDisk();
523
+
524
+ // Clear the auto-save timer
525
+ if (this.persistenceTimer) {
526
+ clearInterval(this.persistenceTimer);
527
+ this.persistenceTimer = undefined;
528
+ }
529
+ }
479
530
  }
480
531
 
481
- // ========== Helper Methods ==========
482
-
483
532
  /**
484
- * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
485
- * This ensures backward compatibility while supporting the new @objectstack/spec interface.
533
+ * Perform aggregation operations using Mingo
534
+ * @param objectName - The object type to aggregate
535
+ * @param pipeline - MongoDB-style aggregation pipeline
536
+ * @param options - Optional query options
537
+ * @returns Aggregation results
486
538
  *
487
- * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
488
- * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
539
+ * @example
540
+ * // Group by status and count
541
+ * const results = await driver.aggregate('orders', [
542
+ * { $match: { status: 'completed' } },
543
+ * { $group: { _id: '$customer', totalAmount: { $sum: '$amount' } } }
544
+ * ]);
545
+ *
546
+ * @example
547
+ * // Calculate average with filter
548
+ * const results = await driver.aggregate('products', [
549
+ * { $match: { category: 'electronics' } },
550
+ * { $group: { _id: null, avgPrice: { $avg: '$price' } } }
551
+ * ]);
552
+ */
553
+ async aggregate(objectName: string, pipeline: any[], options?: any): Promise<any[]> {
554
+ const pattern = `${objectName}:`;
555
+
556
+ // Get all records for this object type
557
+ let records: any[] = [];
558
+ for (const [key, value] of this.store.entries()) {
559
+ if (key.startsWith(pattern)) {
560
+ records.push({ ...value });
561
+ }
562
+ }
563
+
564
+ // Use Mingo to execute the aggregation pipeline
565
+ const aggregator = new Aggregator(pipeline);
566
+ const results = aggregator.run(records);
567
+
568
+ return results;
569
+ }
570
+
571
+ /**
572
+ * Begin a new transaction
573
+ * @returns Transaction object that can be passed to other methods
489
574
  */
490
- private normalizeQuery(query: any): any {
491
- if (!query) return {};
575
+ async beginTransaction(): Promise<any> {
576
+ const txId = `tx_${Date.now()}_${Math.random().toString(36).substring(7)}`;
577
+
578
+ // Create a deep snapshot of the current store to ensure complete transaction isolation
579
+ // This prevents issues with nested objects and arrays being modified during the transaction
580
+ const snapshot = new Map<string, any>();
581
+ for (const [key, value] of this.store.entries()) {
582
+ snapshot.set(key, JSON.parse(JSON.stringify(value)));
583
+ }
584
+
585
+ const transaction: MemoryTransaction = {
586
+ id: txId,
587
+ snapshot,
588
+ operations: []
589
+ };
590
+
591
+ this.transactions.set(txId, transaction);
592
+
593
+ return { id: txId };
594
+ }
595
+
596
+ /**
597
+ * Commit a transaction
598
+ * @param transaction - Transaction object returned by beginTransaction()
599
+ */
600
+ async commitTransaction(transaction: any): Promise<void> {
601
+ const txId = transaction?.id;
602
+ if (!txId) {
603
+ throw new ObjectQLError({
604
+ code: 'INVALID_TRANSACTION',
605
+ message: 'Invalid transaction object'
606
+ });
607
+ }
608
+
609
+ const tx = this.transactions.get(txId);
610
+ if (!tx) {
611
+ throw new ObjectQLError({
612
+ code: 'TRANSACTION_NOT_FOUND',
613
+ message: `Transaction ${txId} not found`
614
+ });
615
+ }
616
+
617
+ // Operations are already applied to the store during the transaction
618
+ // Just clean up the transaction record
619
+ this.transactions.delete(txId);
620
+ }
621
+
622
+ /**
623
+ * Rollback a transaction
624
+ * @param transaction - Transaction object returned by beginTransaction()
625
+ */
626
+ async rollbackTransaction(transaction: any): Promise<void> {
627
+ const txId = transaction?.id;
628
+ if (!txId) {
629
+ throw new ObjectQLError({
630
+ code: 'INVALID_TRANSACTION',
631
+ message: 'Invalid transaction object'
632
+ });
633
+ }
634
+
635
+ const tx = this.transactions.get(txId);
636
+ if (!tx) {
637
+ throw new ObjectQLError({
638
+ code: 'TRANSACTION_NOT_FOUND',
639
+ message: `Transaction ${txId} not found`
640
+ });
641
+ }
642
+
643
+ // Restore the snapshot
644
+ this.store = new Map(tx.snapshot);
645
+
646
+ // Clean up the transaction
647
+ this.transactions.delete(txId);
648
+ }
649
+
650
+ /**
651
+ * Set up persistence to file system
652
+ * @private
653
+ */
654
+ protected setupPersistence(): void {
655
+ if (!this.config.persistence) return;
656
+
657
+ const { filePath, autoSaveInterval = 5000 } = this.config.persistence;
658
+
659
+ // Try to load existing data from file
660
+ try {
661
+ const fs = require('fs');
662
+ if (fs.existsSync(filePath)) {
663
+ const fileData = fs.readFileSync(filePath, 'utf8');
664
+ const data = JSON.parse(fileData);
665
+
666
+ // Load data into store
667
+ for (const [key, value] of Object.entries(data.store || {})) {
668
+ this.store.set(key, value);
669
+ }
670
+
671
+ // Load ID counters
672
+ if (data.idCounters) {
673
+ for (const [key, value] of Object.entries(data.idCounters)) {
674
+ this.idCounters.set(key, value as number);
675
+ }
676
+ }
677
+ }
678
+ } catch (error) {
679
+ console.warn('[MemoryDriver] Failed to load persisted data:', error);
680
+ }
492
681
 
493
- const normalized: any = { ...query };
682
+ // Set up auto-save timer
683
+ // Use unref() to allow Node.js process to exit gracefully when idle
684
+ this.persistenceTimer = setInterval(() => {
685
+ this.saveToDisk();
686
+ }, autoSaveInterval);
687
+ this.persistenceTimer.unref();
688
+ }
689
+
690
+ /**
691
+ * Save current state to disk
692
+ * @private
693
+ */
694
+ protected saveToDisk(): void {
695
+ if (!this.config.persistence) return;
494
696
 
495
- // Normalize limit/top
496
- if (normalized.top !== undefined && normalized.limit === undefined) {
497
- normalized.limit = normalized.top;
697
+ try {
698
+ const fs = require('fs');
699
+ const data = {
700
+ store: Object.fromEntries(this.store),
701
+ idCounters: Object.fromEntries(this.idCounters),
702
+ timestamp: new Date().toISOString()
703
+ };
704
+
705
+ fs.writeFileSync(this.config.persistence.filePath, JSON.stringify(data, null, 2), 'utf8');
706
+ } catch (error) {
707
+ console.error('[MemoryDriver] Failed to save data to disk:', error);
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Build indexes for faster queries
713
+ * @private
714
+ */
715
+ protected buildIndexes(indexConfig: Record<string, string[]>): void {
716
+ for (const [objectName, fields] of Object.entries(indexConfig)) {
717
+ const objectIndexes = new Map<string, Set<string>>();
718
+
719
+ for (const field of fields) {
720
+ const fieldIndex = new Set<string>();
721
+
722
+ // Scan all records of this object type
723
+ const pattern = `${objectName}:`;
724
+ for (const [key, record] of this.store.entries()) {
725
+ if (key.startsWith(pattern)) {
726
+ const fieldValue = record[field];
727
+ if (fieldValue !== undefined && fieldValue !== null) {
728
+ // Store the record ID in the index
729
+ fieldIndex.add(record.id);
730
+ }
731
+ }
732
+ }
733
+
734
+ objectIndexes.set(field, fieldIndex);
735
+ }
736
+
737
+ this.indexes.set(objectName, objectIndexes);
498
738
  }
739
+ }
740
+
741
+ /**
742
+ * Update index when a record is created or updated
743
+ * @private
744
+ */
745
+ protected updateIndex(objectName: string, recordId: string, record: any): void {
746
+ const objectIndexes = this.indexes.get(objectName);
747
+ if (!objectIndexes) return;
499
748
 
500
- // Normalize sort format
501
- if (normalized.sort && Array.isArray(normalized.sort)) {
502
- // Check if it's already in the array format [field, order]
503
- const firstSort = normalized.sort[0];
504
- if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
505
- // Convert from QueryAST format {field, order} to internal format [field, order]
506
- normalized.sort = normalized.sort.map((item: any) => [
507
- item.field,
508
- item.order || item.direction || item.dir || 'asc'
509
- ]);
749
+ for (const [field, indexSet] of objectIndexes.entries()) {
750
+ const fieldValue = record[field];
751
+ if (fieldValue !== undefined && fieldValue !== null) {
752
+ indexSet.add(recordId);
753
+ } else {
754
+ indexSet.delete(recordId);
510
755
  }
511
756
  }
757
+ }
758
+
759
+ /**
760
+ * Remove record from indexes
761
+ * @private
762
+ */
763
+ protected removeFromIndex(objectName: string, recordId: string): void {
764
+ const objectIndexes = this.indexes.get(objectName);
765
+ if (!objectIndexes) return;
512
766
 
513
- return normalized;
767
+ for (const indexSet of objectIndexes.values()) {
768
+ indexSet.delete(recordId);
769
+ }
514
770
  }
515
771
 
772
+ // ========== Helper Methods ==========
773
+
774
+ /**
775
+ * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
776
+ * This ensures backward compatibility while supporting the new @objectstack/spec interface.
777
+ *
778
+ * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
779
+ * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
780
+ */
516
781
  /**
517
782
  * Convert ObjectQL filters to MongoDB query format for Mingo.
518
783
  *
519
- * Supports ObjectQL filter format:
520
- * [
521
- * ['field', 'operator', value],
522
- * 'or',
523
- * ['field2', 'operator', value2]
524
- * ]
784
+ * Supports both:
785
+ * 1. Legacy ObjectQL filter format (array):
786
+ * [['field', 'operator', value], 'or', ['field2', 'operator', value2']]
787
+ * 2. New FilterCondition format (object - already MongoDB-like):
788
+ * { $and: [{ field: { $eq: value }}, { field2: { $gt: value2 }}] }
525
789
  *
526
790
  * Converts to MongoDB query format:
527
791
  * { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] }
528
792
  */
529
- private convertToMongoQuery(filters?: any[]): Record<string, any> {
530
- if (!filters || filters.length === 0) {
793
+ /**
794
+ * Convert ObjectQL filter format to MongoDB query format.
795
+ * Supports three input formats:
796
+ *
797
+ * 1. AST Comparison Node: { type: 'comparison', field: string, operator: string, value: any }
798
+ * 2. AST Logical Node: { type: 'logical', operator: 'and' | 'or', conditions: Node[] }
799
+ * 3. Legacy Array Format: [field, operator, value, 'and', field2, operator2, value2]
800
+ * 4. MongoDB Format: { field: value } or { field: { $eq: value } } (passthrough)
801
+ *
802
+ * @param filters - Filter in any supported format
803
+ * @returns MongoDB query object
804
+ */
805
+ protected convertToMongoQuery(filters?: any[] | Record<string, any>): Record<string, any> {
806
+ if (!filters) {
807
+ return {};
808
+ }
809
+
810
+ // Handle AST node format (ObjectQL QueryAST)
811
+ if (!Array.isArray(filters) && typeof filters === 'object') {
812
+ // AST Comparison Node: { type: 'comparison', field, operator, value }
813
+ if (filters.type === 'comparison') {
814
+ const mongoCondition = this.convertConditionToMongo(filters.field, filters.operator, filters.value);
815
+ return mongoCondition || {};
816
+ }
817
+
818
+ // AST Logical Node: { type: 'logical', operator: 'and' | 'or', conditions: [...] }
819
+ else if (filters.type === 'logical') {
820
+ const conditions = filters.conditions?.map((cond: any) => this.convertToMongoQuery(cond)) || [];
821
+ if (conditions.length === 0) {
822
+ return {};
823
+ }
824
+ if (conditions.length === 1) {
825
+ return conditions[0];
826
+ }
827
+ const operator = filters.operator === 'or' ? '$or' : '$and';
828
+ return { [operator]: conditions };
829
+ }
830
+
831
+ // MongoDB format (passthrough): { field: value } or { field: { $eq: value } }
832
+ // This handles cases where filters is already a proper MongoDB query
833
+ return filters;
834
+ }
835
+
836
+ // Handle legacy array format
837
+ if (filters.length === 0) {
531
838
  return {};
532
839
  }
533
840
 
@@ -606,7 +913,7 @@ export class MemoryDriver implements Driver {
606
913
  /**
607
914
  * Convert a single ObjectQL condition to MongoDB operator format.
608
915
  */
609
- private convertConditionToMongo(field: string, operator: string, value: any): Record<string, any> | null {
916
+ protected convertConditionToMongo(field: string, operator: string, value: any): Record<string, any> | null {
610
917
  switch (operator) {
611
918
  case '=':
612
919
  case '==':
@@ -667,7 +974,7 @@ export class MemoryDriver implements Driver {
667
974
  * Escape special regex characters to prevent ReDoS and ensure literal matching.
668
975
  * This is crucial for security when using user input in regex patterns.
669
976
  */
670
- private escapeRegex(str: string): string {
977
+ protected escapeRegex(str: string): string {
671
978
  return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
672
979
  }
673
980
 
@@ -675,7 +982,7 @@ export class MemoryDriver implements Driver {
675
982
  * Apply manual sorting to an array of records.
676
983
  * This is used instead of Mingo's sort to avoid CJS build issues.
677
984
  */
678
- private applyManualSort(records: any[], sort: any[]): any[] {
985
+ protected applyManualSort(records: any[], sort: any[]): any[] {
679
986
  const sorted = [...records];
680
987
 
681
988
  // Apply sorts in reverse order for correct multi-field precedence
@@ -716,7 +1023,7 @@ export class MemoryDriver implements Driver {
716
1023
  /**
717
1024
  * Project specific fields from a document.
718
1025
  */
719
- private projectFields(doc: any, fields: string[]): any {
1026
+ protected projectFields(doc: any, fields: string[]): any {
720
1027
  const result: any = {};
721
1028
  for (const field of fields) {
722
1029
  if (doc[field] !== undefined) {
@@ -729,7 +1036,7 @@ export class MemoryDriver implements Driver {
729
1036
  /**
730
1037
  * Generate a unique ID for a record.
731
1038
  */
732
- private generateId(objectName: string): string {
1039
+ protected generateId(objectName: string): string {
733
1040
  const counter = (this.idCounters.get(objectName) || 0) + 1;
734
1041
  this.idCounters.set(objectName, counter);
735
1042
 
@@ -751,17 +1058,8 @@ export class MemoryDriver implements Driver {
751
1058
  async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
752
1059
  const objectName = ast.object || '';
753
1060
 
754
- // Convert QueryAST to legacy query format
755
- const legacyQuery: any = {
756
- fields: ast.fields,
757
- filters: this.convertFilterNodeToLegacy(ast.filters),
758
- sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
759
- limit: ast.top,
760
- offset: ast.skip,
761
- };
762
-
763
- // Use existing find method
764
- const results = await this.find(objectName, legacyQuery, options);
1061
+ // Use existing find method with QueryAST directly
1062
+ const results = await this.find(objectName, ast, options);
765
1063
 
766
1064
  return {
767
1065
  value: results,
@@ -873,63 +1171,6 @@ export class MemoryDriver implements Driver {
873
1171
  }
874
1172
  }
875
1173
 
876
- /**
877
- * Convert FilterNode (QueryAST format) to legacy filter array format
878
- * This allows reuse of existing filter logic while supporting new QueryAST
879
- *
880
- * @private
881
- */
882
- private convertFilterNodeToLegacy(node?: FilterNode): any {
883
- if (!node) return undefined;
884
-
885
- switch (node.type) {
886
- case 'comparison':
887
- // Convert comparison node to [field, operator, value] format
888
- const operator = node.operator || '=';
889
- return [[node.field, operator, node.value]];
890
-
891
- case 'and':
892
- // Convert AND node to array with 'and' separator
893
- if (!node.children || node.children.length === 0) return undefined;
894
- const andResults: any[] = [];
895
- for (const child of node.children) {
896
- const converted = this.convertFilterNodeToLegacy(child);
897
- if (converted) {
898
- if (andResults.length > 0) {
899
- andResults.push('and');
900
- }
901
- andResults.push(...(Array.isArray(converted) ? converted : [converted]));
902
- }
903
- }
904
- return andResults.length > 0 ? andResults : undefined;
905
-
906
- case 'or':
907
- // Convert OR node to array with 'or' separator
908
- if (!node.children || node.children.length === 0) return undefined;
909
- const orResults: any[] = [];
910
- for (const child of node.children) {
911
- const converted = this.convertFilterNodeToLegacy(child);
912
- if (converted) {
913
- if (orResults.length > 0) {
914
- orResults.push('or');
915
- }
916
- orResults.push(...(Array.isArray(converted) ? converted : [converted]));
917
- }
918
- }
919
- return orResults.length > 0 ? orResults : undefined;
920
-
921
- case 'not':
922
- // NOT is complex - we'll just process the first child for now
923
- if (node.children && node.children.length > 0) {
924
- return this.convertFilterNodeToLegacy(node.children[0]);
925
- }
926
- return undefined;
927
-
928
- default:
929
- return undefined;
930
- }
931
- }
932
-
933
1174
  /**
934
1175
  * Execute command (alternative signature for compatibility)
935
1176
  *