@reldens/storage 0.95.0 → 0.96.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/.claude/driver-methods-complete-analysis.md +26 -17
- package/.claude/test-architecture.md +124 -212
- package/lib/prisma/prisma-data-server.js +26 -26
- package/lib/prisma/prisma-schema-generator.js +32 -1
- package/package.json +1 -1
- package/tests/integration/test-raw-queries.js +117 -0
- package/tests/run-tests.js +11 -0
- package/samples/objection.js +0 -37
|
@@ -1667,38 +1667,47 @@ async rawQuery(content)
|
|
|
1667
1667
|
|
|
1668
1668
|
**Return type:** Array or false
|
|
1669
1669
|
|
|
1670
|
-
#### Prisma Implementation (prisma-data-server.js:
|
|
1670
|
+
#### Prisma Implementation (prisma-data-server.js:119-138)
|
|
1671
1671
|
|
|
1672
1672
|
```javascript
|
|
1673
1673
|
async rawQuery(content)
|
|
1674
1674
|
{
|
|
1675
1675
|
try {
|
|
1676
|
-
let statements = this.splitSqlStatements(content)
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
1682
|
-
let result = await this.prisma.$executeRawUnsafe(statement);
|
|
1683
|
-
results.push(result);
|
|
1676
|
+
let statements = this.splitSqlStatements(content)
|
|
1677
|
+
.map(s => s.trim())
|
|
1678
|
+
.filter(s => '' !== s);
|
|
1679
|
+
if(0 === statements.length){
|
|
1680
|
+
return false;
|
|
1684
1681
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1682
|
+
let results = [];
|
|
1683
|
+
await this.prisma.$transaction(
|
|
1684
|
+
async (tx) => { results = await this.executeStatements(statements, tx); },
|
|
1685
|
+
{timeout: 60000}
|
|
1686
|
+
);
|
|
1687
|
+
return 1 === results.length ? results[0] : results;
|
|
1688
|
+
} catch(error) {
|
|
1689
|
+
Logger.error('Raw query failed: '+error.message);
|
|
1688
1690
|
return false;
|
|
1689
1691
|
}
|
|
1690
1692
|
}
|
|
1691
1693
|
```
|
|
1692
1694
|
|
|
1693
|
-
**Uses:**
|
|
1694
|
-
|
|
1695
|
+
**Uses:** `$transaction` wrapping `executeStatements()`, which calls `executeStatement()` per statement:
|
|
1696
|
+
- SELECT / SHOW → `$queryRawUnsafe` → returns rows array
|
|
1697
|
+
- CREATE / ALTER / DROP → `$executeRawUnsafe` → returns `{affectedRows: N}`
|
|
1698
|
+
- INSERT / UPDATE / DELETE → `$executeRawUnsafe` → returns affected row count
|
|
1695
1699
|
|
|
1696
|
-
**Return type:**
|
|
1700
|
+
**Return type:**
|
|
1701
|
+
- Empty content → `false`
|
|
1702
|
+
- Single statement → the result directly (not wrapped in array)
|
|
1703
|
+
- Multiple statements → array of results
|
|
1704
|
+
- Error → `false`
|
|
1697
1705
|
|
|
1698
1706
|
**Consistency Analysis:**
|
|
1699
|
-
- ✅ **ALL CONSISTENT** - Return array or false
|
|
1700
1707
|
- ✅ All execute raw SQL
|
|
1701
|
-
-
|
|
1708
|
+
- ✅ All return false on error or empty input
|
|
1709
|
+
- ⚠️ **RETURN TYPE DIFFERS for single statements**: ObjectionJS and MikroORM always return array or false; Prisma returns the result directly (not in an array) for a single statement, and an array only for multiple statements
|
|
1710
|
+
- ⚠️ Prisma is the only driver that explicitly splits multi-statement strings and runs them atomically in a transaction
|
|
1702
1711
|
|
|
1703
1712
|
---
|
|
1704
1713
|
|
|
@@ -365,107 +365,48 @@ async fetchEntitiesFromDatabase() {
|
|
|
365
365
|
|
|
366
366
|
### Critical: Tables Must Exist First!
|
|
367
367
|
|
|
368
|
-
**File:** `tests/utils/test-helpers.js`
|
|
368
|
+
**File:** `tests/utils/test-helpers.js`
|
|
369
369
|
|
|
370
370
|
```javascript
|
|
371
371
|
static async generateTestEntities(dataServer, driverName)
|
|
372
372
|
{
|
|
373
|
-
//
|
|
374
|
-
let models = await this.createModelsFromDatabase(dataServer, driverName);
|
|
375
|
-
if(!models || 0 === Object.keys(models).length){
|
|
376
|
-
throw new Error('Failed to create models from database for '+driverName);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Step 2: PRISMA ONLY - Generate schema + client
|
|
373
|
+
// Prisma only: generate schema + client if not already present
|
|
380
374
|
if('prisma' === driverName){
|
|
381
375
|
let config = this.getTestDbConfig();
|
|
382
376
|
config.client = 'mysql';
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// Step 3: Disconnect old client
|
|
395
|
-
await dataServer.disconnect();
|
|
396
|
-
|
|
397
|
-
// Step 4: Clear Node.js module cache
|
|
398
|
-
delete require.cache[require.resolve('@prisma/client')];
|
|
399
|
-
|
|
400
|
-
// Step 5: Reconnect with new client
|
|
401
|
-
if(!await dataServer.connect()){
|
|
402
|
-
throw new Error('Failed to reconnect Prisma client');
|
|
377
|
+
let schemaPath = FileHandler.joinPaths(process.cwd(), 'prisma', 'schema.prisma');
|
|
378
|
+
if(!FileHandler.exists(schemaPath)){
|
|
379
|
+
if(!await this.generatePrismaSchema(config)){
|
|
380
|
+
throw new Error('Failed to generate Prisma schema');
|
|
381
|
+
}
|
|
382
|
+
if(!await this.generatePrismaClient()){
|
|
383
|
+
throw new Error('Failed to generate Prisma client');
|
|
384
|
+
}
|
|
385
|
+
await dataServer.disconnect();
|
|
386
|
+
delete require.cache[require.resolve(FileHandler.joinPaths(process.cwd(), 'prisma', 'client'))];
|
|
387
|
+
await dataServer.connect();
|
|
403
388
|
}
|
|
404
389
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
390
|
+
// Run EntitiesGenerator — introspects DB, writes entity + model files to generated-entities/
|
|
391
|
+
await this.runEntitiesGenerator(dataServer, driverName);
|
|
392
|
+
this.fixGeneratedRequirePaths();
|
|
393
|
+
this.compareGeneratedWithExpected(driverName, 'entities');
|
|
394
|
+
this.compareGeneratedWithExpected(driverName, 'models/'+driverName);
|
|
395
|
+
if('objection-js' === driverName){
|
|
396
|
+
this.compareGeneratedWithExpected(driverName, 'entities-config.js');
|
|
397
|
+
this.compareGeneratedWithExpected(driverName, 'entities-translations.js');
|
|
411
398
|
}
|
|
412
|
-
|
|
399
|
+
// Load generated registered-models file and populate entity manager
|
|
400
|
+
await this.loadGeneratedEntities(dataServer, driverName);
|
|
413
401
|
return true;
|
|
414
402
|
}
|
|
415
403
|
```
|
|
416
404
|
|
|
417
|
-
###
|
|
418
|
-
|
|
419
|
-
**Lines 31-69:**
|
|
420
|
-
|
|
421
|
-
```javascript
|
|
422
|
-
static async createModelsFromDatabase(dataServer, driverName)
|
|
423
|
-
{
|
|
424
|
-
// CRITICAL: This calls fetchEntitiesFromDatabase which reads actual tables
|
|
425
|
-
let tables = await dataServer.fetchEntitiesFromDatabase();
|
|
426
|
-
if(!tables){
|
|
427
|
-
return {};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
let models = {};
|
|
431
|
-
|
|
432
|
-
if('objection-js' === driverName){
|
|
433
|
-
for(let tableName of Object.keys(tables)){
|
|
434
|
-
let entityName = sc.camelCase(tableName.replace('test_', ''));
|
|
435
|
-
// Creates dynamic Model class extending Objection Model
|
|
436
|
-
class DynamicModel extends Model {
|
|
437
|
-
static get tableName(){
|
|
438
|
-
return tableName;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
if(dataServer.knex){
|
|
442
|
-
DynamicModel.knex(dataServer.knex);
|
|
443
|
-
}
|
|
444
|
-
let modelKey = 'test'+sc.capitalizedCamelCase(entityName);
|
|
445
|
-
models[modelKey] = DynamicModel;
|
|
446
|
-
}
|
|
447
|
-
return models;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if('mikro-orm' === driverName){
|
|
451
|
-
for(let tableName of Object.keys(tables)){
|
|
452
|
-
let entityName = sc.camelCase(tableName.replace('test_', ''));
|
|
453
|
-
models['test'+sc.capitalizedCamelCase(entityName)] = {tableName: tableName};
|
|
454
|
-
}
|
|
455
|
-
return models;
|
|
456
|
-
}
|
|
405
|
+
### Entity Generation Flow
|
|
457
406
|
|
|
458
|
-
|
|
459
|
-
for(let tableName of Object.keys(tables)){
|
|
460
|
-
let entityName = sc.camelCase(tableName.replace('test_', ''));
|
|
461
|
-
models['test'+sc.capitalizedCamelCase(entityName)] = {tableName: tableName};
|
|
462
|
-
}
|
|
463
|
-
return models;
|
|
464
|
-
}
|
|
407
|
+
`runEntitiesGenerator()` creates an `EntitiesGenerator` instance pointed at the connected `dataServer`, calls `generator.generate()`, which internally calls `dataServer.fetchEntitiesFromDatabase()` to read real table metadata from `information_schema`. This is why tables must exist before calling `generateTestEntities()` — without tables, introspection returns empty and generation fails.
|
|
465
408
|
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
```
|
|
409
|
+
After generation, `loadGeneratedEntities()` loads the `generated-entities/models/[driver]/registered-models-[driver].js` file, sets `dataServer.rawEntities`, and calls `dataServer.generateEntities()` to register all driver instances in the entity manager.
|
|
469
410
|
|
|
470
411
|
**Why tables must exist first:**
|
|
471
412
|
- `fetchEntitiesFromDatabase()` queries MySQL `information_schema`
|
|
@@ -673,65 +614,49 @@ static async generatePrismaClient(){
|
|
|
673
614
|
|
|
674
615
|
### Per Driver Test Suite
|
|
675
616
|
|
|
617
|
+
**1. Initialization — runs once per driver (`DriverRegistry.initialize()`):**
|
|
618
|
+
- `setupDriver()` — connect to database
|
|
619
|
+
- `executeRawSQL()` — create tables from SQL schema
|
|
620
|
+
- `generateTestEntities()` — introspect DB, run EntitiesGenerator, load entities
|
|
621
|
+
- `dataServer.getEntity()` — obtain repository references
|
|
622
|
+
|
|
623
|
+
**2. Test execution — per group method in each test class:**
|
|
624
|
+
|
|
625
|
+
Each test class (DriversTest, NestedFiltersTest, RelationsTest, RawQueriesTest) has group methods. Each group method begins with `await TestHelpers.cleanDatabase(this.dataServer)` to DELETE all rows, then runs its tests via `runner.test()`.
|
|
626
|
+
|
|
627
|
+
Example:
|
|
628
|
+
```javascript
|
|
629
|
+
async testCreateOperations() {
|
|
630
|
+
this.runner.group('CREATE Operations');
|
|
631
|
+
await TestHelpers.cleanDatabase(this.dataServer);
|
|
632
|
+
// insert fixture data, then call runner.test() for each assertion
|
|
633
|
+
}
|
|
676
634
|
```
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
└─ getEntity (get repository references)
|
|
682
|
-
|
|
683
|
-
2. Test Suite Execution
|
|
684
|
-
│
|
|
685
|
-
├─ describe('CREATE Operations')
|
|
686
|
-
│ ├─ beforeEach() → cleanDatabase (DELETE data)
|
|
687
|
-
│ ├─ it('should create single record')
|
|
688
|
-
│ ├─ beforeEach() → cleanDatabase (DELETE data)
|
|
689
|
-
│ ├─ it('should create with JSON field')
|
|
690
|
-
│ └─ ...
|
|
691
|
-
│
|
|
692
|
-
├─ describe('UPDATE Operations')
|
|
693
|
-
│ ├─ beforeEach() → cleanDatabase (DELETE data)
|
|
694
|
-
│ ├─ it('should update by ID')
|
|
695
|
-
│ └─ ...
|
|
696
|
-
│
|
|
697
|
-
├─ describe('QUERY Operations')
|
|
698
|
-
│ ├─ beforeEach() → cleanDatabase (DELETE data)
|
|
699
|
-
│ ├─ it('should load all records')
|
|
700
|
-
│ └─ ...
|
|
701
|
-
│
|
|
702
|
-
└─ describe('DELETE Operations')
|
|
703
|
-
├─ beforeEach() → cleanDatabase (DELETE data)
|
|
704
|
-
├─ it('should delete by ID')
|
|
705
|
-
└─ ...
|
|
706
|
-
|
|
707
|
-
3. after() hook - RUNS ONCE
|
|
708
|
-
├─ dropTestTables (DROP all tables)
|
|
709
|
-
└─ teardownDriver (disconnect from database)
|
|
710
|
-
```
|
|
635
|
+
|
|
636
|
+
**3. Teardown — runs once per driver (`DriverRegistry.cleanup()`):**
|
|
637
|
+
- `dropTestTables()` — DROP all test tables
|
|
638
|
+
- `teardownDriver()` — disconnect from database
|
|
711
639
|
|
|
712
640
|
### All Drivers Execution
|
|
713
641
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
└─ after() → Drop Tables, Disconnect
|
|
729
|
-
```
|
|
642
|
+
**Driver: objection-js**
|
|
643
|
+
- Initialize: connect, create tables, generate entities
|
|
644
|
+
- Run all tests (cleanDatabase at start of each group)
|
|
645
|
+
- Teardown: drop tables, disconnect
|
|
646
|
+
|
|
647
|
+
**Driver: mikro-orm**
|
|
648
|
+
- Initialize: connect, create tables, generate entities
|
|
649
|
+
- Run all tests (cleanDatabase at start of each group)
|
|
650
|
+
- Teardown: drop tables, disconnect
|
|
651
|
+
|
|
652
|
+
**Driver: prisma**
|
|
653
|
+
- Initialize: connect, create tables, generate Prisma schema (subprocess), generate Prisma client (subprocess), reconnect, generate entities
|
|
654
|
+
- Run all tests (cleanDatabase at start of each group)
|
|
655
|
+
- Teardown: drop tables, disconnect
|
|
730
656
|
|
|
731
657
|
**Total connections:** 3 (one per driver)
|
|
732
658
|
**Total table creations:** 3 (one per driver)
|
|
733
659
|
**Total entity generations:** 3 (one per driver)
|
|
734
|
-
**Total test executions:** 441 (147 tests × 3 drivers)
|
|
735
660
|
|
|
736
661
|
---
|
|
737
662
|
|
|
@@ -775,7 +700,7 @@ Driver: prisma
|
|
|
775
700
|
|
|
776
701
|
**Symptoms:**
|
|
777
702
|
- `fetchEntitiesFromDatabase()` returns empty/null
|
|
778
|
-
- `
|
|
703
|
+
- `EntitiesGenerator.generate()` fails or produces no entities
|
|
779
704
|
- Entity generation fails with "No entities generated"
|
|
780
705
|
|
|
781
706
|
**Cause:**
|
|
@@ -828,59 +753,48 @@ Driver: prisma
|
|
|
828
753
|
|
|
829
754
|
## Summary: The Complete Flow
|
|
830
755
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
├─ DROP TABLE IF EXISTS test_products
|
|
874
|
-
├─ DROP TABLE IF EXISTS test_categories
|
|
875
|
-
└─ SET FOREIGN_KEY_CHECKS=1
|
|
876
|
-
|
|
877
|
-
2. Disconnect from database
|
|
878
|
-
├─ ObjectionJS: await knex.destroy()
|
|
879
|
-
├─ MikroORM: await orm.close()
|
|
880
|
-
└─ Prisma: await prisma.$disconnect()
|
|
881
|
-
|
|
882
|
-
SESSION END
|
|
883
|
-
```
|
|
756
|
+
**For each driver (objection-js, mikro-orm, prisma):**
|
|
757
|
+
|
|
758
|
+
**Initialization — once per driver:**
|
|
759
|
+
|
|
760
|
+
1. Connect to database
|
|
761
|
+
- ObjectionJS: create Knex instance
|
|
762
|
+
- MikroORM: initialize MikroORM
|
|
763
|
+
- Prisma: create PrismaClient (if already generated)
|
|
764
|
+
|
|
765
|
+
2. Create tables from SQL file
|
|
766
|
+
- SET FOREIGN_KEY_CHECKS=0
|
|
767
|
+
- DROP TABLE IF EXISTS (cleanup from previous run)
|
|
768
|
+
- CREATE TABLE (with all constraints)
|
|
769
|
+
- SET FOREIGN_KEY_CHECKS=1
|
|
770
|
+
|
|
771
|
+
3. Generate entities from database
|
|
772
|
+
- Run EntitiesGenerator (introspects DB, writes files to generated-entities/)
|
|
773
|
+
- Prisma only: generate schema.prisma (subprocess), generate PrismaClient (subprocess), reconnect
|
|
774
|
+
- Load registered-models file, call dataServer.generateEntities()
|
|
775
|
+
|
|
776
|
+
**Per test group — cleanDatabase() at start of each group method:**
|
|
777
|
+
- SET FOREIGN_KEY_CHECKS=0
|
|
778
|
+
- DELETE FROM test_reviews
|
|
779
|
+
- DELETE FROM test_products
|
|
780
|
+
- DELETE FROM test_categories
|
|
781
|
+
- SET FOREIGN_KEY_CHECKS=1
|
|
782
|
+
|
|
783
|
+
**Per test:** create test data, perform operations, assert results.
|
|
784
|
+
|
|
785
|
+
**Teardown — once per driver:**
|
|
786
|
+
|
|
787
|
+
1. Drop all tables
|
|
788
|
+
- SET FOREIGN_KEY_CHECKS=0
|
|
789
|
+
- DROP TABLE IF EXISTS test_reviews
|
|
790
|
+
- DROP TABLE IF EXISTS test_products
|
|
791
|
+
- DROP TABLE IF EXISTS test_categories
|
|
792
|
+
- SET FOREIGN_KEY_CHECKS=1
|
|
793
|
+
|
|
794
|
+
2. Disconnect from database
|
|
795
|
+
- ObjectionJS: `await knex.destroy()`
|
|
796
|
+
- MikroORM: `await orm.close()`
|
|
797
|
+
- Prisma: `await prisma.$disconnect()`
|
|
884
798
|
|
|
885
799
|
---
|
|
886
800
|
|
|
@@ -888,7 +802,7 @@ SESSION END
|
|
|
888
802
|
|
|
889
803
|
### Integration Test Files (All Fixed)
|
|
890
804
|
|
|
891
|
-
**test-
|
|
805
|
+
**test-drivers.js:**
|
|
892
806
|
- ✅ Changed `beforeEach` → `before` for setup
|
|
893
807
|
- ✅ Added `beforeEach` for data cleanup only
|
|
894
808
|
- ✅ Changed `afterEach` → `after` for teardown
|
|
@@ -906,6 +820,9 @@ SESSION END
|
|
|
906
820
|
- ✅ Changed `afterEach` → `after` for teardown
|
|
907
821
|
- ✅ Added error checking for entity generation
|
|
908
822
|
|
|
823
|
+
**test-raw-queries.js:**
|
|
824
|
+
- ✅ Added — covers rawQuery with single and multiple SQL statements
|
|
825
|
+
|
|
909
826
|
### Test Helpers (All Fixed)
|
|
910
827
|
|
|
911
828
|
**test-helpers.js:**
|
|
@@ -913,7 +830,7 @@ SESSION END
|
|
|
913
830
|
- ✅ Changed `cleanDatabase()` to use DELETE instead of DROP
|
|
914
831
|
- ✅ Created `dropTestTables()` for final cleanup
|
|
915
832
|
- ✅ Changed `generateTestEntities()` to throw errors instead of returning false
|
|
916
|
-
- ✅
|
|
833
|
+
- ✅ Replaced `createModelsFromDatabase()` with `runEntitiesGenerator()` + `loadGeneratedEntities()`
|
|
917
834
|
- ✅ Removed logging from `setupDriver()`
|
|
918
835
|
- ✅ Removed logging from `executeRawSQL()`
|
|
919
836
|
|
|
@@ -930,30 +847,25 @@ SESSION END
|
|
|
930
847
|
|
|
931
848
|
### Before Fixes (Wrong Pattern)
|
|
932
849
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
Total
|
|
940
|
-
|
|
850
|
+
Each test reconnected, recreated tables, and regenerated entities independently:
|
|
851
|
+
- Connect: ~500ms
|
|
852
|
+
- Create tables: ~1000ms
|
|
853
|
+
- Generate entities: ~500ms
|
|
854
|
+
- Run test: ~10ms
|
|
855
|
+
- Teardown: ~100ms
|
|
856
|
+
- **Total per test: ~2110ms**
|
|
857
|
+
- Total for 100 tests, 3 drivers: ~10+ minutes
|
|
941
858
|
|
|
942
859
|
### After Fixes (Correct Pattern)
|
|
943
860
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
└─ after(): Drop Tables (100ms) + Teardown (100ms) = 200ms
|
|
951
|
-
|
|
952
|
-
Total for 1 driver: ~2.4 seconds
|
|
953
|
-
Total for 3 drivers: ~7-10 seconds (includes Prisma subprocess)
|
|
954
|
-
```
|
|
861
|
+
Setup and teardown happen once per driver. Only data cleanup runs between tests:
|
|
862
|
+
- Setup once: connect + create tables + generate entities = ~2 seconds
|
|
863
|
+
- Each test: DELETE data (~5ms) + run test (~10ms) = ~15ms
|
|
864
|
+
- Teardown once: drop tables + disconnect = ~200ms
|
|
865
|
+
- **Total for 100 tests, 1 driver: ~3.7 seconds**
|
|
866
|
+
- Total for 3 drivers: ~10-15 seconds (includes Prisma subprocess generation)
|
|
955
867
|
|
|
956
|
-
**Speed improvement: 90x faster**
|
|
868
|
+
**Speed improvement: ~60-90x faster**
|
|
957
869
|
|
|
958
870
|
---
|
|
959
871
|
|
|
@@ -119,43 +119,43 @@ class PrismaDataServer extends BaseDataServer
|
|
|
119
119
|
async rawQuery(content)
|
|
120
120
|
{
|
|
121
121
|
try {
|
|
122
|
-
let statements = this.splitSqlStatements(content)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if('' === cleanStatement){
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
let result = await this.executeStatement(cleanStatement);
|
|
131
|
-
results.push(result);
|
|
132
|
-
//Logger.debug('Statement executed, result: ' + JSON.stringify(result));
|
|
133
|
-
} catch(stmtError) {
|
|
134
|
-
Logger.error('Statement "'+cleanStatement+'" execution failed: '+stmtError.message);
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if(0 === results.length){
|
|
139
|
-
//Logger.error('Statement results length is zero.', results);
|
|
122
|
+
let statements = this.splitSqlStatements(content)
|
|
123
|
+
.map(s => s.trim())
|
|
124
|
+
.filter(s => '' !== s);
|
|
125
|
+
if(0 === statements.length){
|
|
140
126
|
return false;
|
|
141
127
|
}
|
|
142
|
-
|
|
128
|
+
let results = [];
|
|
129
|
+
await this.prisma.$transaction(
|
|
130
|
+
async (tx) => { results = await this.executeStatements(statements, tx); },
|
|
131
|
+
{timeout: 60000}
|
|
132
|
+
);
|
|
133
|
+
return 1 === results.length ? results[0] : results;
|
|
143
134
|
} catch(error) {
|
|
144
135
|
Logger.error('Raw query failed: '+error.message);
|
|
145
136
|
return false;
|
|
146
137
|
}
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
async
|
|
140
|
+
async executeStatements(statements, tx)
|
|
150
141
|
{
|
|
151
|
-
let
|
|
152
|
-
|
|
153
|
-
|
|
142
|
+
let results = [];
|
|
143
|
+
for(let statement of statements){
|
|
144
|
+
let result = await this.executeStatement(statement, tx);
|
|
145
|
+
//Logger.debug('Statement executed, result: ' + JSON.stringify(result));
|
|
146
|
+
results.push(result);
|
|
154
147
|
}
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async executeStatement(statement, prismaClient)
|
|
152
|
+
{
|
|
153
|
+
let client = prismaClient || this.prisma;
|
|
154
|
+
let trimmedStatement = statement.trim().toUpperCase();
|
|
155
|
+
if(trimmedStatement.startsWith('SELECT') || trimmedStatement.startsWith('SHOW')){
|
|
156
|
+
return await client.$queryRawUnsafe(statement);
|
|
157
157
|
}
|
|
158
|
-
let result = await
|
|
158
|
+
let result = await client.$executeRawUnsafe(statement);
|
|
159
159
|
//Logger.debug('Raw result from Prisma: ' + result);
|
|
160
160
|
if(
|
|
161
161
|
trimmedStatement.startsWith('CREATE')
|
|
@@ -26,6 +26,33 @@ class PrismaSchemaGenerator
|
|
|
26
26
|
this.dbParams = sc.get(props, 'dbParams', process.env.RELDENS_DB_PARAMS || '');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
static ensureStubFiles(projectRoot)
|
|
30
|
+
{
|
|
31
|
+
let stubPath = FileHandler.joinPaths(projectRoot, 'node_modules', '.prisma', 'client', 'default.js');
|
|
32
|
+
if(FileHandler.exists(stubPath)){
|
|
33
|
+
Logger.info('Prisma stub files already exist.');
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
Logger.info('Creating Prisma stub files...');
|
|
37
|
+
try {
|
|
38
|
+
let defaultIndexPath = FileHandler.joinPaths(
|
|
39
|
+
projectRoot, 'node_modules', '@prisma', 'client', 'scripts', 'default-index.js'
|
|
40
|
+
);
|
|
41
|
+
if(!FileHandler.exists(defaultIndexPath)){
|
|
42
|
+
Logger.warning('Prisma default-index script not found at: '+defaultIndexPath);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
let stubDir = FileHandler.joinPaths(projectRoot, 'node_modules', '.prisma', 'client');
|
|
46
|
+
FileHandler.createFolder(stubDir);
|
|
47
|
+
FileHandler.copyFile(defaultIndexPath, stubPath);
|
|
48
|
+
Logger.info('Prisma stub files created successfully.');
|
|
49
|
+
return true;
|
|
50
|
+
} catch(error) {
|
|
51
|
+
Logger.error('Failed to create stub files: '+error.message);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
async generate()
|
|
30
57
|
{
|
|
31
58
|
FileHandler.createFolder(this.prismaSchemaPath);
|
|
@@ -39,6 +66,11 @@ class PrismaSchemaGenerator
|
|
|
39
66
|
Logger.critical('Failed to pull database schema: '+error.message);
|
|
40
67
|
return false;
|
|
41
68
|
}
|
|
69
|
+
return this.generateClient();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
generateClient()
|
|
73
|
+
{
|
|
42
74
|
let generateCommand = 'npx prisma generate';
|
|
43
75
|
if(this.dataProxy){
|
|
44
76
|
generateCommand += ' --data-proxy';
|
|
@@ -58,7 +90,6 @@ class PrismaSchemaGenerator
|
|
|
58
90
|
Logger.critical('Generate command failed: '+error.message);
|
|
59
91
|
return false;
|
|
60
92
|
}
|
|
61
|
-
await this.waitForSchemaGeneration();
|
|
62
93
|
return true;
|
|
63
94
|
}
|
|
64
95
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - Raw Queries Integration Test
|
|
4
|
+
* Tests rawQuery with single and multiple SQL statements across all drivers
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { TestRunner, assert } = require('../utils/test-runner');
|
|
9
|
+
const { TestHelpers } = require('../utils/test-helpers');
|
|
10
|
+
|
|
11
|
+
class RawQueriesTest
|
|
12
|
+
{
|
|
13
|
+
|
|
14
|
+
constructor(dataServer, repos, driverName)
|
|
15
|
+
{
|
|
16
|
+
this.dataServer = dataServer;
|
|
17
|
+
this.driverName = driverName;
|
|
18
|
+
this.runner = new TestRunner();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async run()
|
|
22
|
+
{
|
|
23
|
+
this.runner.suite('Raw Queries - Driver: '+this.driverName);
|
|
24
|
+
await this.testSingleStatements();
|
|
25
|
+
await this.testMultipleStatements();
|
|
26
|
+
return this.runner.getResults();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async testSingleStatements()
|
|
30
|
+
{
|
|
31
|
+
this.runner.group('Single Statement');
|
|
32
|
+
await TestHelpers.cleanDatabase(this.dataServer);
|
|
33
|
+
await this.dataServer.rawQuery(
|
|
34
|
+
'INSERT INTO test_categories (id, name, slug, is_active, display_order)'
|
|
35
|
+
+' VALUES (9001, \'Raw Single Test\', \'raw-single-test\', 1, 99)'
|
|
36
|
+
);
|
|
37
|
+
await this.runner.test('should return rows from SELECT single statement', async () => {
|
|
38
|
+
let result = await this.dataServer.rawQuery(
|
|
39
|
+
'SELECT id, name, slug FROM test_categories WHERE id = 9001'
|
|
40
|
+
);
|
|
41
|
+
assert.ok(result);
|
|
42
|
+
assert.ok(Array.isArray(result));
|
|
43
|
+
assert.strictEqual(result.length, 1);
|
|
44
|
+
assert.strictEqual(result[0].name, 'Raw Single Test');
|
|
45
|
+
assert.strictEqual(result[0].slug, 'raw-single-test');
|
|
46
|
+
});
|
|
47
|
+
await this.runner.test('should execute UPDATE single statement and reflect changes', async () => {
|
|
48
|
+
await this.dataServer.rawQuery(
|
|
49
|
+
'UPDATE test_categories SET name = \'Updated Raw Single\' WHERE id = 9001'
|
|
50
|
+
);
|
|
51
|
+
let result = await this.dataServer.rawQuery(
|
|
52
|
+
'SELECT id, name FROM test_categories WHERE id = 9001'
|
|
53
|
+
);
|
|
54
|
+
assert.ok(result);
|
|
55
|
+
assert.ok(Array.isArray(result));
|
|
56
|
+
assert.strictEqual(result.length, 1);
|
|
57
|
+
assert.strictEqual(result[0].name, 'Updated Raw Single');
|
|
58
|
+
});
|
|
59
|
+
await this.runner.test('should execute DELETE single statement and remove record', async () => {
|
|
60
|
+
await this.dataServer.rawQuery('DELETE FROM test_categories WHERE id = 9001');
|
|
61
|
+
let result = await this.dataServer.rawQuery(
|
|
62
|
+
'SELECT id FROM test_categories WHERE id = 9001'
|
|
63
|
+
);
|
|
64
|
+
let isEmpty = !result || (Array.isArray(result) && 0 === result.length);
|
|
65
|
+
assert.ok(isEmpty, 'Record should not exist after DELETE');
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async testMultipleStatements()
|
|
70
|
+
{
|
|
71
|
+
this.runner.group('Multiple Statements');
|
|
72
|
+
await TestHelpers.cleanDatabase(this.dataServer);
|
|
73
|
+
if('prisma' === this.driverName){
|
|
74
|
+
await this.runner.test('should execute multiple statements in one rawQuery call', async () => {
|
|
75
|
+
let results = await this.dataServer.rawQuery(
|
|
76
|
+
'INSERT INTO test_categories (id, name, slug, is_active, display_order)'
|
|
77
|
+
+' VALUES (9002, \'Raw Multi 1\', \'raw-multi-1\', 1, 1);'
|
|
78
|
+
+'INSERT INTO test_categories (id, name, slug, is_active, display_order)'
|
|
79
|
+
+' VALUES (9003, \'Raw Multi 2\', \'raw-multi-2\', 1, 2);'
|
|
80
|
+
);
|
|
81
|
+
assert.ok(results);
|
|
82
|
+
assert.ok(Array.isArray(results));
|
|
83
|
+
assert.strictEqual(results.length, 2);
|
|
84
|
+
let rows = await this.dataServer.rawQuery(
|
|
85
|
+
'SELECT id, name FROM test_categories ORDER BY id'
|
|
86
|
+
);
|
|
87
|
+
assert.ok(rows);
|
|
88
|
+
assert.ok(Array.isArray(rows));
|
|
89
|
+
assert.strictEqual(rows.length, 2);
|
|
90
|
+
assert.strictEqual(rows[0].name, 'Raw Multi 1');
|
|
91
|
+
assert.strictEqual(rows[1].name, 'Raw Multi 2');
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
await this.runner.test('should execute multiple sequential rawQuery calls', async () => {
|
|
95
|
+
await this.dataServer.rawQuery(
|
|
96
|
+
'INSERT INTO test_categories (id, name, slug, is_active, display_order)'
|
|
97
|
+
+' VALUES (9002, \'Raw Multi 1\', \'raw-multi-1\', 1, 1)'
|
|
98
|
+
);
|
|
99
|
+
await this.dataServer.rawQuery(
|
|
100
|
+
'INSERT INTO test_categories (id, name, slug, is_active, display_order)'
|
|
101
|
+
+' VALUES (9003, \'Raw Multi 2\', \'raw-multi-2\', 1, 2)'
|
|
102
|
+
);
|
|
103
|
+
let result = await this.dataServer.rawQuery(
|
|
104
|
+
'SELECT id, name FROM test_categories ORDER BY id'
|
|
105
|
+
);
|
|
106
|
+
assert.ok(result);
|
|
107
|
+
assert.ok(Array.isArray(result));
|
|
108
|
+
assert.strictEqual(result.length, 2);
|
|
109
|
+
assert.strictEqual(result[0].name, 'Raw Multi 1');
|
|
110
|
+
assert.strictEqual(result[1].name, 'Raw Multi 2');
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = RawQueriesTest;
|
package/tests/run-tests.js
CHANGED
|
@@ -11,6 +11,7 @@ const { DriverRegistry } = require('./utils/driver-registry');
|
|
|
11
11
|
const DriversTest = require('./integration/test-drivers');
|
|
12
12
|
const NestedFiltersTest = require('./integration/test-nested-filters');
|
|
13
13
|
const RelationsTest = require('./integration/test-relations');
|
|
14
|
+
const RawQueriesTest = require('./integration/test-raw-queries');
|
|
14
15
|
const EntityManagerTest = require('./unit/test-entity-manager');
|
|
15
16
|
const TypeMapperTest = require('./unit/test-type-mapper');
|
|
16
17
|
const DriversUnitTest = require('./unit/test-drivers');
|
|
@@ -156,6 +157,16 @@ class RunTests
|
|
|
156
157
|
Logger.critical('Driver '+driverName+' relations tests crashed: '+error.message);
|
|
157
158
|
Logger.critical(error.stack);
|
|
158
159
|
}
|
|
160
|
+
try {
|
|
161
|
+
let rawQueriesTest = new RawQueriesTest(dataServer, repos, driverName);
|
|
162
|
+
let rawQueriesResult = await rawQueriesTest.run();
|
|
163
|
+
this.allCounts.total += rawQueriesResult.total;
|
|
164
|
+
this.allCounts.passed += rawQueriesResult.passed;
|
|
165
|
+
this.allCounts.failed += rawQueriesResult.failed;
|
|
166
|
+
} catch(error) {
|
|
167
|
+
Logger.critical('Driver '+driverName+' raw queries tests crashed: '+error.message);
|
|
168
|
+
Logger.critical(error.stack);
|
|
169
|
+
}
|
|
159
170
|
}
|
|
160
171
|
}
|
|
161
172
|
|
package/samples/objection.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
const { ObjectionJsDataServer } = require('../lib/objection-js/objection-js-data-server');
|
|
2
|
-
|
|
3
|
-
const { Model } = require('objection');
|
|
4
|
-
|
|
5
|
-
class UsersModel extends Model
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
static get tableName()
|
|
9
|
-
{
|
|
10
|
-
return 'your_table_name';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function test(){
|
|
16
|
-
const entityKey = 'users';
|
|
17
|
-
|
|
18
|
-
let server = new ObjectionJsDataServer({
|
|
19
|
-
client: 'mysql',
|
|
20
|
-
config: {
|
|
21
|
-
user: 'user',
|
|
22
|
-
password: 'password',
|
|
23
|
-
database: 'database_name',
|
|
24
|
-
port : 3306,
|
|
25
|
-
},
|
|
26
|
-
rawEntities: {[entityKey]: UsersModel}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
await server.connect();
|
|
30
|
-
await server.generateEntities();
|
|
31
|
-
|
|
32
|
-
console.log(await server.getEntity(entityKey).loadAll());
|
|
33
|
-
|
|
34
|
-
process.exit();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
test();
|