Skip to content

Commit c33d553

Browse files
committed
WL#12019: better handling of X DevAPI expressions
A single generic parser was being used to parse all the X DevAPI expressions in the scope of a given statement right before it was executed. Although this centralized approach looks good on paper, it also brings with it a lot of unwanted limitations and performance issues. With a dynamicaly generated parser, this means that to overcome specific diferences between parser types, the parser has to be re-created for each different type that requires so. Also, not spreading the parsing tasks between stages in the statement life-cycle means we are creating a possible bottleneck in a single stage. Additionally, the fact that the "mysqlx.expr()" method is not only parsing but also creating the corresponding protobuf message of a given expression means that we need to take special care to handle expressions that are already in the protobuf format when we have to parse any other expression in the scope of the statement execution. The fact that expressions are also being parsed at the protocol "layer" in a conceptual connector stack is also not good from a design standpoint. This also leads to a situation where expressions are parsed more than once when using prepared statements. X DevAPI expressions are a high level concept that should be handled at the API level. The expression parser itself is also stateful because it needs to keep track of the placeholders for each expression. This should be a responsability for the routines that create the protobuf messages. This patch addresses these issues by narrowing the scope and responsability of the "mysqlx.expr()" method, which now only works as an abstraction for a given value in the form of an expression string and by moving the parsing logic to the API level. It introduces some new DevAPI-level abstractions that encapsulate their own parser setup, which is created only once and used every time the application calls an API method that relies on structured expressions. Internally, the parser implementation is now stateless and the placeholder assignment logic has been simplified and moved to the proper layer in the stack. To ensure this changes are, in fact, meaningful, the patch also introduces a few benchmarks for the relevant CRUD-like statements. Change-Id: I37c63840cc00b3c006b0839a47d31ada250c0cab
1 parent bc1f9c4 commit c33d553

File tree

218 files changed

+10087
-8517
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

218 files changed

+10087
-8517
lines changed

benchmarks/collection-add.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates.
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License, version 2.0, as
6+
* published by the Free Software Foundation.
7+
*
8+
* This program is also distributed with certain software (including
9+
* but not limited to OpenSSL) that is licensed under separate terms,
10+
* as designated in a particular file or component or in included license
11+
* documentation. The authors of MySQL hereby grant you an
12+
* additional permission to link the program and your derivative works
13+
* with the separately licensed software that they have included with
14+
* MySQL.
15+
*
16+
* Without limiting anything contained in the foregoing, this file,
17+
* which is part of MySQL Connector/Node.js, is also subject to the
18+
* Universal FOSS Exception, version 1.0, a copy of which can be found at
19+
* http://oss.oracle.com/licenses/universal-foss-exception.
20+
*
21+
* This program is distributed in the hope that it will be useful, but
22+
* WITHOUT ANY WARRANTY; without even the implied warranty of
23+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
24+
* See the GNU General Public License, version 2.0, for more details.
25+
*
26+
* You should have received a copy of the GNU General Public License
27+
* along with this program; if not, write to the Free Software Foundation, Inc.,
28+
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29+
*/
30+
31+
'use strict';
32+
33+
const mysqlx = require('../index');
34+
const config = require('../test/config');
35+
36+
const schemaName = config.schema || 'mysql-connector-nodejs_benchmark';
37+
const times = parseInt(process.argv[2]) || 10000;
38+
const label = `collection-add benchmark (${times} times)`;
39+
40+
const doc = [{
41+
literal: 'foo',
42+
nestedArray: [{
43+
nestedDoc: {
44+
literal: true
45+
}
46+
}]
47+
}];
48+
49+
const benchmark = async () => {
50+
let session;
51+
52+
try {
53+
session = await mysqlx.getSession(config);
54+
let schema = session.getSchema(schemaName);
55+
56+
if (!(await schema.existsInDatabase())) {
57+
schema = await session.createSchema(schemaName);
58+
}
59+
60+
const collection = await schema.createCollection('benchmark_collection_add', { reuseExisting: true });
61+
console.time(label);
62+
63+
const benchmark = collection.add(doc);
64+
65+
await Promise.allSettled([...Array(times)].map(() => benchmark.execute()));
66+
console.timeEnd(label);
67+
} finally {
68+
if (session) {
69+
await session.dropSchema(schemaName);
70+
await session.close();
71+
}
72+
}
73+
};
74+
75+
benchmark();

benchmarks/collection-find.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates.
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License, version 2.0, as
6+
* published by the Free Software Foundation.
7+
*
8+
* This program is also distributed with certain software (including
9+
* but not limited to OpenSSL) that is licensed under separate terms,
10+
* as designated in a particular file or component or in included license
11+
* documentation. The authors of MySQL hereby grant you an
12+
* additional permission to link the program and your derivative works
13+
* with the separately licensed software that they have included with
14+
* MySQL.
15+
*
16+
* Without limiting anything contained in the foregoing, this file,
17+
* which is part of MySQL Connector/Node.js, is also subject to the
18+
* Universal FOSS Exception, version 1.0, a copy of which can be found at
19+
* http://oss.oracle.com/licenses/universal-foss-exception.
20+
*
21+
* This program is distributed in the hope that it will be useful, but
22+
* WITHOUT ANY WARRANTY; without even the implied warranty of
23+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
24+
* See the GNU General Public License, version 2.0, for more details.
25+
*
26+
* You should have received a copy of the GNU General Public License
27+
* along with this program; if not, write to the Free Software Foundation, Inc.,
28+
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29+
*/
30+
31+
'use strict';
32+
33+
const mysqlx = require('../index');
34+
const config = require('../test/config');
35+
36+
const schemaName = config.schema || 'mysql-connector-nodejs_benchmark';
37+
const times = parseInt(process.argv[2]) || 10000;
38+
const label = `collection-find benchmark (${times} times)`;
39+
40+
const docs = [{
41+
firstName: 'foo',
42+
lastName: 'bar',
43+
age: 42
44+
}, {
45+
firstName: 'foo',
46+
lastName: 'baz',
47+
age: 50
48+
}, {
49+
firstName: 'baz',
50+
lastName: 'qux',
51+
age: 31
52+
}, {
53+
firstName: 'qux',
54+
lastName: 'quux',
55+
age: 40
56+
}];
57+
58+
const benchmark = async () => {
59+
let session;
60+
61+
try {
62+
session = await mysqlx.getSession(config);
63+
let schema = session.getSchema(schemaName);
64+
65+
if (!(await schema.existsInDatabase())) {
66+
schema = await session.createSchema(schemaName);
67+
}
68+
69+
const collection = await schema.createCollection('benchmark_collection_find', { reuseExisting: true });
70+
await collection.add(docs)
71+
.execute();
72+
73+
console.time(label);
74+
const tests = [...Array(times)].map(() => {
75+
return collection.find('firstName = :name')
76+
.fields(mysqlx.expr('{ "name": concat(firstName, lastName), "year": year(curdate()) - age }'))
77+
.sort('age ASC')
78+
.bind('name', 'foo');
79+
});
80+
81+
await Promise.allSettled(tests.map(test => test.execute()));
82+
console.timeEnd(label);
83+
} finally {
84+
if (session) {
85+
await session.dropSchema(schemaName);
86+
await session.close();
87+
}
88+
}
89+
};
90+
91+
benchmark();

benchmarks/collection-modify.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates.
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License, version 2.0, as
6+
* published by the Free Software Foundation.
7+
*
8+
* This program is also distributed with certain software (including
9+
* but not limited to OpenSSL) that is licensed under separate terms,
10+
* as designated in a particular file or component or in included license
11+
* documentation. The authors of MySQL hereby grant you an
12+
* additional permission to link the program and your derivative works
13+
* with the separately licensed software that they have included with
14+
* MySQL.
15+
*
16+
* Without limiting anything contained in the foregoing, this file,
17+
* which is part of MySQL Connector/Node.js, is also subject to the
18+
* Universal FOSS Exception, version 1.0, a copy of which can be found at
19+
* http://oss.oracle.com/licenses/universal-foss-exception.
20+
*
21+
* This program is distributed in the hope that it will be useful, but
22+
* WITHOUT ANY WARRANTY; without even the implied warranty of
23+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
24+
* See the GNU General Public License, version 2.0, for more details.
25+
*
26+
* You should have received a copy of the GNU General Public License
27+
* along with this program; if not, write to the Free Software Foundation, Inc.,
28+
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29+
*/
30+
31+
'use strict';
32+
33+
const mysqlx = require('../index');
34+
const config = require('../test/config');
35+
36+
const schemaName = config.schema || 'mysql-connector-nodejs_benchmark';
37+
const times = parseInt(process.argv[2]) || 10000;
38+
const label = `collection-modify benchmark (${times} times)`;
39+
40+
const docs = [{
41+
firstName: 'foo',
42+
lastName: 'bar',
43+
age: 42
44+
}, {
45+
firstName: 'foo',
46+
lastName: 'baz',
47+
age: 50
48+
}, {
49+
firstName: 'baz',
50+
lastName: 'qux',
51+
age: 31
52+
}, {
53+
firstName: 'qux',
54+
lastName: 'quux',
55+
age: 40
56+
}];
57+
58+
const benchmark = async () => {
59+
let session;
60+
61+
try {
62+
session = await mysqlx.getSession(config);
63+
let schema = session.getSchema(schemaName);
64+
65+
if (!(await schema.existsInDatabase())) {
66+
schema = await session.createSchema(schemaName);
67+
}
68+
69+
const collection = await schema.createCollection('benchmark_collection_modify', { reuseExisting: true });
70+
await collection.add(docs)
71+
.execute();
72+
73+
console.time(label);
74+
const tests = [...Array(times)].map((_, i) => {
75+
return collection.modify('firstName = :name')
76+
.bind('name', 'foo')
77+
.unset('lastName')
78+
.set('age', mysqlx.expr(`age + ${i + 1}`));
79+
});
80+
81+
await Promise.allSettled(tests.map(test => test.execute()));
82+
console.timeEnd(label);
83+
} finally {
84+
if (session) {
85+
await session.dropSchema(schemaName);
86+
await session.close();
87+
}
88+
}
89+
};
90+
91+
benchmark();

benchmarks/collection-remove.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates.
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License, version 2.0, as
6+
* published by the Free Software Foundation.
7+
*
8+
* This program is also distributed with certain software (including
9+
* but not limited to OpenSSL) that is licensed under separate terms,
10+
* as designated in a particular file or component or in included license
11+
* documentation. The authors of MySQL hereby grant you an
12+
* additional permission to link the program and your derivative works
13+
* with the separately licensed software that they have included with
14+
* MySQL.
15+
*
16+
* Without limiting anything contained in the foregoing, this file,
17+
* which is part of MySQL Connector/Node.js, is also subject to the
18+
* Universal FOSS Exception, version 1.0, a copy of which can be found at
19+
* http://oss.oracle.com/licenses/universal-foss-exception.
20+
*
21+
* This program is distributed in the hope that it will be useful, but
22+
* WITHOUT ANY WARRANTY; without even the implied warranty of
23+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
24+
* See the GNU General Public License, version 2.0, for more details.
25+
*
26+
* You should have received a copy of the GNU General Public License
27+
* along with this program; if not, write to the Free Software Foundation, Inc.,
28+
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
29+
*/
30+
31+
'use strict';
32+
33+
const mysqlx = require('../index');
34+
const config = require('../test/config');
35+
36+
const schemaName = config.schema || 'mysql-connector-nodejs_benchmark';
37+
const times = parseInt(process.argv[2]) || 10000;
38+
const label = `collection-remove benchmark (${times} times)`;
39+
40+
const docs = [{
41+
firstName: 'foo',
42+
lastName: 'bar',
43+
age: 42
44+
}, {
45+
firstName: 'foo',
46+
lastName: 'baz',
47+
age: 50
48+
}, {
49+
firstName: 'baz',
50+
lastName: 'qux',
51+
age: 31
52+
}, {
53+
firstName: 'qux',
54+
lastName: 'quux',
55+
age: 40
56+
}];
57+
58+
const benchmark = async () => {
59+
let session;
60+
61+
try {
62+
session = await mysqlx.getSession(config);
63+
let schema = session.getSchema(schemaName);
64+
65+
if (!(await schema.existsInDatabase())) {
66+
schema = await session.createSchema(schemaName);
67+
}
68+
69+
const collection = await schema.createCollection('benchmark_collection_remove', { reuseExisting: true });
70+
// Add enough documents to be removed by the test (total = $times).
71+
const enoughDocs = docs.map(JSON.stringify)
72+
.join(' ')
73+
.concat(' ')
74+
.repeat(times / docs.length)
75+
.trim()
76+
.split(' ')
77+
.map(JSON.parse);
78+
79+
await collection.add(enoughDocs)
80+
.execute();
81+
82+
console.time(label);
83+
const tests = [...Array(times)].map(() => {
84+
return collection.remove('firstName = :name')
85+
.sort('age desc')
86+
.bind('name', 'foo');
87+
});
88+
89+
await Promise.allSettled(tests.map(test => test.execute()));
90+
console.timeEnd(label);
91+
} finally {
92+
if (session) {
93+
await session.dropSchema(schemaName);
94+
await session.close();
95+
}
96+
}
97+
};
98+
99+
benchmark();

0 commit comments

Comments
 (0)