@igojs/db 6.0.0-beta.2 → 6.0.0-beta.4
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/package.json +7 -2
- package/src/Db.js +2 -4
- package/src/PaginatedOptimizedSql.js +49 -6
- package/src/Sql.js +12 -5
- package/test/JoinTest.js +31 -0
- package/test/PaginatedOptimizedQueryTest.js +143 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igojs/db",
|
|
3
|
-
"version": "6.0.0-beta.
|
|
3
|
+
"version": "6.0.0-beta.4",
|
|
4
4
|
"description": "Igo ORM - Database abstraction layer for MySQL and PostgreSQL",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
],
|
|
15
15
|
"author": "@igocreate",
|
|
16
16
|
"license": "ISC",
|
|
17
|
+
"homepage": "https://github.com/igocreate/igo",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git@github.com:igocreate/igo.git"
|
|
21
|
+
},
|
|
17
22
|
"publishConfig": {
|
|
18
23
|
"access": "public"
|
|
19
24
|
},
|
|
@@ -21,7 +26,7 @@
|
|
|
21
26
|
"lodash": "^4.17.21"
|
|
22
27
|
},
|
|
23
28
|
"optionalDependencies": {
|
|
24
|
-
"mysql2": "^3.
|
|
29
|
+
"mysql2": "^3.16.0",
|
|
25
30
|
"pg": "^8.16.3"
|
|
26
31
|
}
|
|
27
32
|
}
|
package/src/Db.js
CHANGED
|
@@ -81,11 +81,9 @@ class Db {
|
|
|
81
81
|
return dialect.getRows(result);
|
|
82
82
|
|
|
83
83
|
} catch (err) {
|
|
84
|
-
if (options.silent) {
|
|
85
|
-
|
|
84
|
+
if (!options.silent) {
|
|
85
|
+
logQuery(sql, params, err);
|
|
86
86
|
}
|
|
87
|
-
// log & rethrow error
|
|
88
|
-
logQuery(sql, params, err);
|
|
89
87
|
throw err;
|
|
90
88
|
|
|
91
89
|
} finally {
|
|
@@ -108,6 +108,12 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
108
108
|
|
|
109
109
|
// WHERE de la table principale
|
|
110
110
|
const params = [];
|
|
111
|
+
|
|
112
|
+
// Ajouter les params des jointures de tri (extraWhere)
|
|
113
|
+
if (this._sortJoinParams && this._sortJoinParams.length > 0) {
|
|
114
|
+
params.push(...this._sortJoinParams);
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
sql += this.whereSQL(params);
|
|
112
118
|
|
|
113
119
|
// WHERE NOT de la table principale
|
|
@@ -657,6 +663,7 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
657
663
|
const { query, dialect } = this;
|
|
658
664
|
const { esc } = dialect;
|
|
659
665
|
const mainTable = query.table;
|
|
666
|
+
const params = [];
|
|
660
667
|
|
|
661
668
|
let sql = '';
|
|
662
669
|
const processedTables = new Set(); // Pour éviter les doublons
|
|
@@ -672,17 +679,29 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
672
679
|
return;
|
|
673
680
|
}
|
|
674
681
|
|
|
675
|
-
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
682
|
+
const [, , AssociatedModel, src_column, ref_column, extraWhere] = association;
|
|
676
683
|
|
|
677
684
|
// Générer le LEFT JOIN (pour préserver toutes les lignes, même celles avec NULL)
|
|
678
|
-
|
|
679
|
-
|
|
685
|
+
let joinSql = `LEFT JOIN ${esc}${currentTableName}${esc} `;
|
|
686
|
+
joinSql += `ON ${esc}${currentTableName}${esc}.${esc}${ref_column}${esc} = ${esc}${prevTable}${esc}.${esc}${src_column}${esc}`;
|
|
687
|
+
|
|
688
|
+
// Ajouter les conditions extraWhere si présentes
|
|
689
|
+
if (extraWhere) {
|
|
690
|
+
_.forOwn(extraWhere, (value, key) => {
|
|
691
|
+
joinSql += ` AND ${esc}${currentTableName}${esc}.${esc}${key}${esc} = ${dialect.param(this.i++)}`;
|
|
692
|
+
params.push(value);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
sql += joinSql + ' ';
|
|
680
697
|
|
|
681
698
|
processedTables.add(currentTableName);
|
|
682
699
|
prevTable = currentTableName;
|
|
683
700
|
});
|
|
684
701
|
});
|
|
685
702
|
|
|
703
|
+
// Retourner à la fois le SQL et les params pour qu'ils soient ajoutés
|
|
704
|
+
this._sortJoinParams = params;
|
|
686
705
|
return sql;
|
|
687
706
|
}
|
|
688
707
|
|
|
@@ -775,7 +794,7 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
775
794
|
const { esc } = dialect;
|
|
776
795
|
|
|
777
796
|
const { association, conditions, operator, children } = node;
|
|
778
|
-
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
797
|
+
const [, , AssociatedModel, src_column, ref_column, extraWhere] = association;
|
|
779
798
|
const joinTable = AssociatedModel.schema.table;
|
|
780
799
|
|
|
781
800
|
// Ouvrir l'EXISTS
|
|
@@ -784,6 +803,14 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
784
803
|
// Condition de jointure
|
|
785
804
|
sql += `WHERE ${esc}${joinTable}${esc}.${esc}${ref_column}${esc} = ${esc}${parentTable}${esc}.${esc}${src_column}${esc} `;
|
|
786
805
|
|
|
806
|
+
// Ajouter les conditions extraWhere si présentes
|
|
807
|
+
if (extraWhere) {
|
|
808
|
+
_.forOwn(extraWhere, (value, key) => {
|
|
809
|
+
sql += `AND ${esc}${joinTable}${esc}.${esc}${key}${esc} = ${dialect.param(this.i++)} `;
|
|
810
|
+
params.push(value);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
787
814
|
// Ajouter les conditions de ce niveau (si présentes)
|
|
788
815
|
if (conditions && !_.isEmpty(conditions)) {
|
|
789
816
|
sql += this.buildConditions(conditions, joinTable, params, operator);
|
|
@@ -817,12 +844,20 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
817
844
|
const { esc } = dialect;
|
|
818
845
|
|
|
819
846
|
const { association, conditions, operator } = filterJoin;
|
|
820
|
-
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
847
|
+
const [, , AssociatedModel, src_column, ref_column, extraWhere] = association;
|
|
821
848
|
const joinTable = AssociatedModel.schema.table;
|
|
822
849
|
|
|
823
850
|
let sql = `EXISTS (SELECT 1 FROM ${esc}${joinTable}${esc} `;
|
|
824
851
|
sql += `WHERE ${esc}${joinTable}${esc}.${esc}${ref_column}${esc} = ${esc}${parentTable}${esc}.${esc}${src_column}${esc} `;
|
|
825
852
|
|
|
853
|
+
// Ajouter les conditions extraWhere si présentes
|
|
854
|
+
if (extraWhere) {
|
|
855
|
+
_.forOwn(extraWhere, (value, key) => {
|
|
856
|
+
sql += `AND ${esc}${joinTable}${esc}.${esc}${key}${esc} = ${dialect.param(this.i++)} `;
|
|
857
|
+
params.push(value);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
826
861
|
if (conditions && !_.isEmpty(conditions)) {
|
|
827
862
|
sql += this.buildConditions(conditions, joinTable, params, operator);
|
|
828
863
|
}
|
|
@@ -869,13 +904,21 @@ module.exports = class PaginatedOptimizedSql extends Sql {
|
|
|
869
904
|
const association = this._findAssociationByPath(path, query.schema);
|
|
870
905
|
|
|
871
906
|
if (association) {
|
|
872
|
-
const [, , AssociatedModel, src_column, ref_column] = association;
|
|
907
|
+
const [, , AssociatedModel, src_column, ref_column, extraWhere] = association;
|
|
873
908
|
const joinTable = AssociatedModel.schema.table;
|
|
874
909
|
|
|
875
910
|
// Construire l'EXISTS pour cette condition
|
|
876
911
|
let existsSQL = `EXISTS (SELECT 1 FROM ${esc}${joinTable}${esc} `;
|
|
877
912
|
existsSQL += `WHERE ${esc}${joinTable}${esc}.${esc}${ref_column}${esc} = ${esc}${parentTable}${esc}.${esc}${src_column}${esc} `;
|
|
878
913
|
|
|
914
|
+
// Ajouter les conditions extraWhere si présentes
|
|
915
|
+
if (extraWhere) {
|
|
916
|
+
_.forOwn(extraWhere, (ewValue, ewKey) => {
|
|
917
|
+
existsSQL += `AND ${esc}${joinTable}${esc}.${esc}${ewKey}${esc} = ${dialect.param(this.i++)} `;
|
|
918
|
+
params.push(ewValue);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
879
922
|
// Ajouter la condition sur la colonne
|
|
880
923
|
const columnRef = `${esc}${joinTable}${esc}.${esc}${column}${esc}`;
|
|
881
924
|
|
package/src/Sql.js
CHANGED
|
@@ -58,7 +58,7 @@ module.exports = class Sql {
|
|
|
58
58
|
sql += `FROM ${esc}${query.table}${esc} `;
|
|
59
59
|
|
|
60
60
|
// joins
|
|
61
|
-
sql += this.addJoins();
|
|
61
|
+
sql += this.addJoins(params);
|
|
62
62
|
|
|
63
63
|
// where
|
|
64
64
|
sql += this.whereSQL(params);
|
|
@@ -101,7 +101,7 @@ module.exports = class Sql {
|
|
|
101
101
|
sql += `FROM ${esc}${query.table}${esc} `;
|
|
102
102
|
|
|
103
103
|
// joins
|
|
104
|
-
sql += this.addJoins();
|
|
104
|
+
sql += this.addJoins(params);
|
|
105
105
|
|
|
106
106
|
// where
|
|
107
107
|
sql += this.whereSQL(params);
|
|
@@ -118,17 +118,24 @@ module.exports = class Sql {
|
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
// JOINS
|
|
121
|
-
addJoins() {
|
|
121
|
+
addJoins(params) {
|
|
122
122
|
const { query, dialect } = this;
|
|
123
123
|
const { esc } = dialect;
|
|
124
124
|
|
|
125
125
|
let sql = '';
|
|
126
126
|
_.each(query.joins, join => {
|
|
127
127
|
const { src_schema, type, association, src_alias } = join;
|
|
128
|
-
const [ assoc_type, name, Obj, src_column, column] = association;
|
|
128
|
+
const [ assoc_type, name, Obj, src_column, column, extraWhere] = association;
|
|
129
129
|
const src_table_alias = src_alias || src_schema.table;
|
|
130
130
|
const table = Obj.schema.table;
|
|
131
|
-
|
|
131
|
+
let joinSql = `${type.toUpperCase()} JOIN ${esc}${table}${esc} AS ${esc}${name}${esc} ON ${esc}${name}${esc}.${esc}${column}${esc} = ${esc}${src_table_alias}${esc}.${esc}${src_column}${esc}`;
|
|
132
|
+
if (extraWhere) {
|
|
133
|
+
_.forOwn(extraWhere, (value, key) => {
|
|
134
|
+
joinSql += ` AND ${esc}${name}${esc}.${esc}${key}${esc} = ${dialect.param(this.i++)}`;
|
|
135
|
+
params.push(value);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
sql += joinSql + ' ';
|
|
132
139
|
});
|
|
133
140
|
return sql;
|
|
134
141
|
}
|
package/test/JoinTest.js
CHANGED
|
@@ -202,6 +202,37 @@ describe('includes', () => {
|
|
|
202
202
|
assert.strictEqual(foundBook.library.collection, library.collection);
|
|
203
203
|
assert.strictEqual(foundBook.library.details.description, library.details.description);
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
it('should apply extraWhere conditions on join', async () => {
|
|
207
|
+
class BookWithExtraWhere extends Model({
|
|
208
|
+
table: 'books',
|
|
209
|
+
primary: ['id'],
|
|
210
|
+
columns: [
|
|
211
|
+
'id',
|
|
212
|
+
'code',
|
|
213
|
+
'title',
|
|
214
|
+
'library_id',
|
|
215
|
+
],
|
|
216
|
+
associations: () => ([
|
|
217
|
+
['belongs_to', 'library', Library, 'library_id', 'id', { collection: 'A' }],
|
|
218
|
+
])
|
|
219
|
+
}) {}
|
|
220
|
+
|
|
221
|
+
const libraryA = await Library.create({ title: 'Library A', collection: 'A' });
|
|
222
|
+
const libraryB = await Library.create({ title: 'Library B', collection: 'B' });
|
|
223
|
+
const bookA = await BookWithExtraWhere.create({ library_id: libraryA.id });
|
|
224
|
+
const bookB = await BookWithExtraWhere.create({ library_id: libraryB.id });
|
|
225
|
+
|
|
226
|
+
// Book A should have its library joined (collection = 'A' matches extraWhere)
|
|
227
|
+
const foundBookA = await BookWithExtraWhere.join('library').find(bookA.id);
|
|
228
|
+
assert.strictEqual(foundBookA.id, bookA.id);
|
|
229
|
+
assert.strictEqual(foundBookA.library.id, libraryA.id);
|
|
230
|
+
|
|
231
|
+
// Book B should NOT have its library joined (collection = 'B' does not match extraWhere)
|
|
232
|
+
const foundBookB = await BookWithExtraWhere.join('library').find(bookB.id);
|
|
233
|
+
assert.strictEqual(foundBookB.id, bookB.id);
|
|
234
|
+
assert.strictEqual(foundBookB.library, null);
|
|
235
|
+
});
|
|
205
236
|
});
|
|
206
237
|
|
|
207
238
|
});
|
|
@@ -1180,4 +1180,147 @@ describe('db.PaginatedOptimizedQuery', function() {
|
|
|
1180
1180
|
'FULL phase should find block association for simple column name in direct associations');
|
|
1181
1181
|
});
|
|
1182
1182
|
});
|
|
1183
|
+
|
|
1184
|
+
//
|
|
1185
|
+
describe('extraWhere support in PaginatedOptimizedQuery', function() {
|
|
1186
|
+
// Modèle avec extraWhere sur l'association
|
|
1187
|
+
class Library extends Model({
|
|
1188
|
+
table: 'libraries',
|
|
1189
|
+
primary: ['id'],
|
|
1190
|
+
columns: {
|
|
1191
|
+
id: 'integer',
|
|
1192
|
+
title: 'string',
|
|
1193
|
+
collection: 'string'
|
|
1194
|
+
}
|
|
1195
|
+
}) {}
|
|
1196
|
+
|
|
1197
|
+
class BookWithExtraWhere extends Model({
|
|
1198
|
+
table: 'books',
|
|
1199
|
+
primary: ['id'],
|
|
1200
|
+
columns: {
|
|
1201
|
+
id: 'integer',
|
|
1202
|
+
code: 'string',
|
|
1203
|
+
title: 'string',
|
|
1204
|
+
library_id: 'integer'
|
|
1205
|
+
},
|
|
1206
|
+
associations: () => ([
|
|
1207
|
+
['belongs_to', 'library', Library, 'library_id', 'id', { collection: 'A' }]
|
|
1208
|
+
])
|
|
1209
|
+
}) {}
|
|
1210
|
+
|
|
1211
|
+
it('should apply extraWhere in EXISTS clause for filterJoin', () => {
|
|
1212
|
+
const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere));
|
|
1213
|
+
query.query.verb = 'count';
|
|
1214
|
+
query.where({
|
|
1215
|
+
'library.title': 'Test%'
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
const sql = query.toSQL();
|
|
1219
|
+
|
|
1220
|
+
// Vérifier que EXISTS est présent
|
|
1221
|
+
assert.ok(sql.sql.includes('EXISTS'), 'SQL should contain EXISTS');
|
|
1222
|
+
|
|
1223
|
+
// Vérifier que extraWhere est inclus dans la clause EXISTS
|
|
1224
|
+
assert.ok(sql.sql.includes('`libraries`.`collection`'), 'SQL should include extraWhere column');
|
|
1225
|
+
assert.ok(sql.params.includes('A'), 'SQL params should include extraWhere value');
|
|
1226
|
+
|
|
1227
|
+
// Le SQL devrait ressembler à :
|
|
1228
|
+
// EXISTS (SELECT 1 FROM `libraries` WHERE `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ? AND `libraries`.`title` LIKE ?)
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('should apply extraWhere in LEFT JOIN for sorting', () => {
|
|
1232
|
+
const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere));
|
|
1233
|
+
query.query.verb = 'select_ids';
|
|
1234
|
+
query
|
|
1235
|
+
.where({ code: 'ABC' })
|
|
1236
|
+
.order('library.title ASC')
|
|
1237
|
+
.limit(50);
|
|
1238
|
+
|
|
1239
|
+
const sql = query.toSQL();
|
|
1240
|
+
|
|
1241
|
+
// Vérifier que LEFT JOIN est présent
|
|
1242
|
+
assert.ok(sql.sql.includes('LEFT JOIN `libraries`'), 'SQL should contain LEFT JOIN');
|
|
1243
|
+
|
|
1244
|
+
// Vérifier que extraWhere est inclus dans la condition du JOIN
|
|
1245
|
+
assert.ok(sql.sql.includes('`libraries`.`collection` = ?'), 'SQL should include extraWhere in JOIN condition');
|
|
1246
|
+
assert.ok(sql.params.includes('A'), 'SQL params should include extraWhere value');
|
|
1247
|
+
|
|
1248
|
+
// Le SQL devrait ressembler à :
|
|
1249
|
+
// LEFT JOIN `libraries` ON `libraries`.`id` = `books`.`library_id` AND `libraries`.`collection` = ?
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('should apply extraWhere in both EXISTS (filter) and LEFT JOIN (sort)', () => {
|
|
1253
|
+
const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere));
|
|
1254
|
+
query.query.verb = 'select_ids';
|
|
1255
|
+
query
|
|
1256
|
+
.where({
|
|
1257
|
+
code: 'ABC',
|
|
1258
|
+
'library.title': 'Test%'
|
|
1259
|
+
})
|
|
1260
|
+
.order('library.title ASC')
|
|
1261
|
+
.limit(50);
|
|
1262
|
+
|
|
1263
|
+
const sql = query.toSQL();
|
|
1264
|
+
|
|
1265
|
+
// Vérifier que EXISTS et LEFT JOIN sont présents
|
|
1266
|
+
assert.ok(sql.sql.includes('EXISTS'), 'SQL should contain EXISTS');
|
|
1267
|
+
assert.ok(sql.sql.includes('LEFT JOIN'), 'SQL should contain LEFT JOIN');
|
|
1268
|
+
|
|
1269
|
+
// Vérifier que extraWhere apparaît 2 fois (une fois dans EXISTS, une fois dans JOIN)
|
|
1270
|
+
const collectionMatches = sql.sql.match(/`libraries`\.`collection`/g) || [];
|
|
1271
|
+
assert.strictEqual(collectionMatches.length, 2, 'extraWhere should appear twice (EXISTS + JOIN)');
|
|
1272
|
+
|
|
1273
|
+
// Vérifier que 'A' est dans les params deux fois
|
|
1274
|
+
const aParams = sql.params.filter(p => p === 'A');
|
|
1275
|
+
assert.strictEqual(aParams.length, 2, 'extraWhere value should appear twice in params');
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
it('should NOT include extraWhere in COUNT (no JOIN needed)', () => {
|
|
1279
|
+
const query = mockGetDb(new PaginatedOptimizedQuery(BookWithExtraWhere));
|
|
1280
|
+
query.query.verb = 'count';
|
|
1281
|
+
query
|
|
1282
|
+
.where({ code: 'ABC' })
|
|
1283
|
+
.order('library.title ASC'); // ORDER BY ignored in COUNT
|
|
1284
|
+
|
|
1285
|
+
const sql = query.toSQL();
|
|
1286
|
+
|
|
1287
|
+
// COUNT ne devrait pas avoir de LEFT JOIN
|
|
1288
|
+
assert.ok(!sql.sql.includes('LEFT JOIN'), 'COUNT should not have LEFT JOIN');
|
|
1289
|
+
|
|
1290
|
+
// COUNT ne devrait pas avoir de ORDER BY
|
|
1291
|
+
assert.ok(!sql.sql.includes('ORDER BY'), 'COUNT should not have ORDER BY');
|
|
1292
|
+
|
|
1293
|
+
// Mais si on avait un filtre sur library, on aurait EXISTS avec extraWhere
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// Test avec plusieurs conditions extraWhere
|
|
1297
|
+
class BookWithMultipleExtraWhere extends Model({
|
|
1298
|
+
table: 'books_multi',
|
|
1299
|
+
primary: ['id'],
|
|
1300
|
+
columns: {
|
|
1301
|
+
id: 'integer',
|
|
1302
|
+
title: 'string',
|
|
1303
|
+
library_id: 'integer'
|
|
1304
|
+
},
|
|
1305
|
+
associations: () => ([
|
|
1306
|
+
['belongs_to', 'library', Library, 'library_id', 'id', { collection: 'A', title: 'Main' }]
|
|
1307
|
+
])
|
|
1308
|
+
}) {}
|
|
1309
|
+
|
|
1310
|
+
it('should apply multiple extraWhere conditions', () => {
|
|
1311
|
+
const query = mockGetDb(new PaginatedOptimizedQuery(BookWithMultipleExtraWhere));
|
|
1312
|
+
query.query.verb = 'count';
|
|
1313
|
+
query.where({
|
|
1314
|
+
'library.title': 'Test%'
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
const sql = query.toSQL();
|
|
1318
|
+
|
|
1319
|
+
// Vérifier que les deux conditions extraWhere sont présentes
|
|
1320
|
+
assert.ok(sql.sql.includes('`libraries`.`collection`'), 'SQL should include collection extraWhere');
|
|
1321
|
+
assert.ok(sql.sql.includes('`libraries`.`title`'), 'SQL should include title in filter condition');
|
|
1322
|
+
assert.ok(sql.params.includes('A'), 'SQL params should include collection value');
|
|
1323
|
+
assert.ok(sql.params.includes('Main'), 'SQL params should include title extraWhere value');
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1183
1326
|
});
|