Skip to content

Commit 7a02f12

Browse files
samarpanBdhmlau
authored andcommitted
feat: add capability for insert multiple rows in single query
Signed-off-by: Samarpan Bhattacharya <this.is.samy@gmail.com>
1 parent ca95adb commit 7a02f12

3 files changed

Lines changed: 138 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313
node_modules
1414
checkstyle.xml
1515
loopback-connector-*.tgz
16+
.nyc_output
17+
coverage
18+
.vscode

lib/sql.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ SQLConnector.Transaction = Transaction;
4646
*/
4747
SQLConnector.prototype.relational = true;
4848

49+
/**
50+
* Set the multiInsertSupported property to indicate if multiple value insert SQL dialect is supported or not
51+
* This can be overridden by derived connectors to allow `insert multiple values` dialect for createAll
52+
* By default, it is set to false for backward compatibility
53+
* @type {boolean}
54+
*/
55+
SQLConnector.prototype.multiInsertSupported = false;
56+
4957
/**
5058
* Invoke a prototype method on the super class
5159
* @param {String} methodName Method name
@@ -540,6 +548,53 @@ SQLConnector.prototype.buildInsert = function(model, data, options) {
540548
return this.parameterize(insertStmt);
541549
};
542550

551+
/**
552+
* Build INSERT SQL statement for multiple values
553+
* @param {String} model The model name
554+
* @param {Object} data The array of model data object
555+
* @param {Object} options The options object
556+
* @returns {Object} The ParameterizedSQL Object with INSERT SQL statement
557+
*/
558+
SQLConnector.prototype.buildInsertAll = function(model, data, options) {
559+
if (!this.multiInsertSupported) {
560+
debug('multiple value insert SQL dialect is not supported by this connector');
561+
// return immediately if multiInsertSupported=false in connector
562+
return null;
563+
}
564+
const fieldsArray = this.buildFieldsFromArray(model, data);
565+
if (fieldsArray.length === 0) {
566+
debug('no fields found for insert query');
567+
// return immediately if no fields found
568+
return null;
569+
}
570+
const insertStmt = this.buildInsertInto(model, fieldsArray[0], options);
571+
572+
for (let i = 0; i < fieldsArray.length; i++) {
573+
const columnValues = fieldsArray[i].columnValues;
574+
const isLast = (i === (fieldsArray.length - 1));
575+
const isFirst = (i === 0);
576+
577+
if (columnValues.length) {
578+
const values = ParameterizedSQL.join(columnValues, ',');
579+
// Multi value query.
580+
// This lets multiple row insertion in single query
581+
values.sql = (isFirst ? 'VALUES ' : '') +
582+
'(' + values.sql + ')' +
583+
(isLast ? '' : ',');
584+
insertStmt.merge(values);
585+
} else {
586+
// Insert default values if no values provided
587+
insertStmt.merge(this.buildInsertDefaultValues(model, data, options));
588+
}
589+
}
590+
591+
const returning = this.buildInsertReturning(model, data, options);
592+
if (returning) {
593+
insertStmt.merge(returning);
594+
}
595+
return this.parameterize(insertStmt);
596+
};
597+
543598
/**
544599
* Execute a SQL statement with given parameters.
545600
*
@@ -630,6 +685,34 @@ SQLConnector.prototype.create = function(model, data, options, callback) {
630685
});
631686
};
632687

688+
/**
689+
* Create multiple data models in a single insert query
690+
* Works only if `multiInsertSupported` is set to true for the connector
691+
*
692+
* @param {String} model The model name
693+
* @param {Object} data The model instances data
694+
* @param {Object} options Options object
695+
* @param {Function} [callback] The callback function
696+
*/
697+
SQLConnector.prototype.createAll = function(model, data, options, callback) {
698+
const self = this;
699+
const stmt = this.buildInsertAll(model, data, options);
700+
if (!stmt) {
701+
debug('empty SQL statement returned for insert into multiple values');
702+
callback(new Error(
703+
g.f('empty SQL statement returned for insert into multiple values'),
704+
));
705+
}
706+
this.execute(stmt.sql, stmt.params, options, function(err, info) {
707+
if (err) {
708+
callback(err);
709+
} else {
710+
const insertedId = self.getInsertedId(model, info);
711+
callback(err, insertedId);
712+
}
713+
});
714+
};
715+
633716
/**
634717
* Save the model instance into the database
635718
* @param {String} model The model name
@@ -1177,6 +1260,24 @@ SQLConnector.prototype.buildFields = function(model, data, excludeIds) {
11771260
return this._buildFieldsForKeys(model, data, keys, excludeIds);
11781261
};
11791262

1263+
/**
1264+
* Build an array of fields for the database operation from data array
1265+
* @param {String} model Model name
1266+
* @param {Object} data Array of Model data object
1267+
* @param {Boolean} excludeIds Exclude id properties or not, default to false
1268+
* @returns {[{names: Array, values: Array, properties: Array}]}
1269+
*/
1270+
SQLConnector.prototype.buildFieldsFromArray = function(model, data, excludeIds) {
1271+
const fields = [];
1272+
if (data.length > 0) {
1273+
const keys = Object.keys(data[0]);
1274+
for (let i = 0; i < data.length; i++) {
1275+
fields.push(this._buildFieldsForKeys(model, data[i], keys, excludeIds));
1276+
}
1277+
}
1278+
return fields;
1279+
};
1280+
11801281
/**
11811282
* Build an array of fields for the replace database operation
11821283
* @param {String} model Model name

test/sql.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,38 @@ describe('sql connector', function() {
514514
expect(function() { runExecute(); }).to.not.throw();
515515
ds.connected = true;
516516
});
517+
518+
it('should build INSERT for multiple rows if multiInsertSupported is true', function() {
519+
connector.multiInsertSupported = true;
520+
const sql = connector.buildInsertAll('customer', [
521+
{name: 'Adam', middleName: 'abc', vip: true},
522+
{name: 'Test', middleName: null, vip: false},
523+
]);
524+
expect(sql.toJSON()).to.eql({
525+
sql:
526+
'INSERT INTO `CUSTOMER`(`NAME`,`middle_name`,`VIP`) VALUES ($1,$2,$3), ($4,$5,$6)',
527+
params: ['Adam', 'abc', true, 'Test', null, false],
528+
});
529+
});
530+
531+
it('should return null for INSERT multiple rows if multiInsertSupported is false',
532+
function() {
533+
connector.multiInsertSupported = false;
534+
const sql = connector.buildInsertAll('customer', [
535+
{name: 'Adam', middleName: 'abc', vip: true},
536+
{name: 'Test', middleName: null, vip: false},
537+
]);
538+
// eslint-disable-next-line no-unused-expressions
539+
expect(sql).to.be.null;
540+
});
541+
542+
it('should return null for INSERT multiple rows if multiInsertSupported not set',
543+
function() {
544+
const sql = connector.buildInsertAll('customer', [
545+
{name: 'Adam', middleName: 'abc', vip: true},
546+
{name: 'Test', middleName: null, vip: false},
547+
]);
548+
// eslint-disable-next-line no-unused-expressions
549+
expect(sql).to.be.null;
550+
});
517551
});

0 commit comments

Comments
 (0)