Backend half

This commit is contained in:
2025-07-11 19:56:28 +02:00
parent fa868e7c1d
commit 8600fa7c1d
19426 changed files with 3750448 additions and 8108 deletions
+502
View File
@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+157
View File
@@ -0,0 +1,157 @@
<p style="text-align: center;">
<a href="https://mariadb.com/">
<img src="https://mariadb.com/kb/static/images/logo-2018-black.png"/>
</a>
</p>
# MariaDB Node.js connector
[![npm package][npm-image]][npm-url]
[![CI Tests][ci-image]][ci-url]
[![License (LGPL version 2.1)][licence-image]][licence-url]
[![codecov][codecov-image]][codecov-url]
**Non-blocking MariaDB and MySQL client for Node.js.**
MariaDB and MySQL client, 100% JavaScript, with TypeScript definition, with the Promise API, distributed under the LGPL license version 2.1 or later (LGPL-2.1-or-later)
## Documentation
See [promise documentation](https://mariadb.com/docs/connectors/mariadb-connector-nodejs/connector-nodejs-promise-api) for detailed API.
[Callback documentation](https://mariadb.com/docs/connectors/mariadb-connector-nodejs/connector-nodejs-callback-api) describes the callback wrapper for compatibility with existing drivers.
See [dedicated part](https://mariadb.com/docs/connectors/mariadb-connector-nodejs/connector-nodejs-promise-api#migrating-from-2.x-or-mysql-mysql2-to-3.x) for migration from mysql/mysql2 or from 2.x version.
## Why a specific MariaDB Client?
While there are existing MySQL clients that work with MariaDB, (such as the [`mysql`](https://www.npmjs.com/package/mysql) and [`mysql2`](https://www.npmjs.com/package/mysql2) clients), the MariaDB Node.js Connector offers new functionality, like [Insert Streaming](#insert-streaming), [Pipelining](#pipelining), [ed25519 plugin authentication](https://mariadb.org/history-of-mysql-mariadb-authentication-protocols/) while making no compromises on performance.
The Connector is production grade quality, with multiple features:
* [zero configuration ssl](https://mariadb.org/mission-impossible-zero-configuration-ssl/)
* superfast batching
* fast pool
* easy debugging, trace pointing to code line on error
* allows data streaming without high memory consumption
* pipelining
* metadata skipping (for MariaDB server only)
* sql file import
* ...
see some of those features:
### Insert Streaming
Using a Readable stream in your application, you can stream `INSERT` statements to MariaDB through the Connector.
```javascript
https.get('https://someContent', readableStream => {
//readableStream implement Readable, driver will stream data to database
connection.query("INSERT INTO myTable VALUE (?)", [readableStream]);
});
```
### Pipelining
With Pipelining, the Connector sends commands without waiting for server results, preserving order. For instance, consider the use of executing two `INSERT` statements.
<p style="text-align: center;">
<img src="./documentation/misc/pip.png" alt="pipelining example"/>
</p>
The Connector doesn't wait for query results before sending the next `INSERT` statement. Instead, it sends queries one after the other, avoiding much of the network latency.
For more information, see the [Pipelining](https://mariadb.com/docs/connectors/mariadb-connector-nodejs/connector-nodejs-pipelining) documentation.
### Bulk insert
Some use cases require a large amount of data to be inserted into a database table. By using batch processing, these queries can be sent to the database in one call, thus improving performance.
For more information, see the [Batch](https://mariadb.com/docs/connectors/mariadb-connector-nodejs/connector-nodejs-batch-api) documentation.
## Benchmarks
MariaDB provides benchmarks comparing the Connector with other Node.js MariaDB/MySQL clients, including:
* [`promise-mysql`](https://www.npmjs.com/package/promise-mysql) version 5.2.0 + [`mysql`](https://www.npmjs.com/package/mysql) version 2.18.1
* [`mysql2`](https://www.npmjs.com/package/mysql2) version 3.1.0
See the [Benchmarks](./documentation/benchmarks.md) page for multiple results.
#### query
```
select 100 int
mysql : 2,738.7 ops/s ± 1.3%
mysql2 : 2,404.9 ops/s ± 1.3% ( -12.2% )
mariadb : 5,650.8 ops/s ± 1.4% ( +106.3% )
```
![select 100 int benchmark results](https://quickchart.io/chart/render/zm-ef74089a-be91-49f1-b5a0-5b9ac5752435?data1=2739&data2=2405&data3=5651)
#### execute
## select 100 int - BINARY
```
select 100 int - BINARY
mysql2 : 2,473.4 ops/s ± 1.3%
mariadb : 10,533 ops/s ± 1.7% ( +325.9% )
```
![select 100 int - BINARY benchmark results](https://quickchart.io/chart/render/zm-36b213f4-8efe-4943-8f94-82edf94fce83?data1=2473&data2=10533)
## Quick Start
The MariaDB Connector is available through the Node.js repositories. You can install it using npm :
```
$ npm install mariadb
```
example:
```js
const mariadb = require('mariadb');
const pool = mariadb.createPool({host: process.env.DB_HOST, user: process.env.DB_USER, connectionLimit: 5});
async function asyncFunction() {
let conn;
try {
conn = await pool.getConnection();
const rows = await conn.query("SELECT 1 as val");
// rows: [ {val: 1}, meta: ... ]
const res = await conn.query("INSERT INTO myTable value (?, ?)", [1, "mariadb"]);
// res: { affectedRows: 1, insertId: 1, warningStatus: 0 }
} finally {
if (conn) conn.release(); //release to pool
}
}
```
## Contributors
A big thanks to all contributors
<a href="https://github.com/mariadb-corporation/mariadb-connector-nodejs/graphs/contributors">
<img src="https://contrib.rocks/image?repo=mariadb-corporation/mariadb-connector-nodejs&max=180&columns=15" alt="contributors list"/>
</a>
## Contributing
If you would like to contribute to the MariaDB Node.js Connector, please follow the instructions given in the [contributing guide.](/CONTRIBUTING.md)
To file an issue or follow the development, see [JIRA](https://jira.mariadb.org/projects/CONJS/issues/).
[ci-image]:https://github.com/mariadb-corporation/mariadb-connector-nodejs/actions/workflows/ci.yml/badge.svg?branch=master
[ci-url]:https://github.com/mariadb-corporation/mariadb-connector-nodejs/actions/workflows/ci.yml
[npm-image]:https://img.shields.io/npm/v/mariadb.svg
[npm-url]:http://npmjs.org/package/mariadb
[licence-image]:https://img.shields.io/badge/license-GNU%20LGPL%20version%202.1-green.svg?style=flat-square
[licence-url]:http://opensource.org/licenses/LGPL-2.1
[codecov-image]:https://codecov.io/gh/mariadb-corporation/mariadb-connector-nodejs/branch/master/graph/badge.svg
[codecov-url]:https://codecov.io/gh/mariadb-corporation/mariadb-connector-nodejs
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
require('./check-node');
const ConnectionCallback = require('./lib/connection-callback');
const ClusterCallback = require('./lib/cluster-callback');
const PoolCallback = require('./lib/pool-callback');
const ConnOptions = require('./lib/config/connection-options');
const PoolOptions = require('./lib/config/pool-options');
const ClusterOptions = require('./lib/config/cluster-options');
const Connection = require('./lib/connection');
module.exports.version = require('./package.json').version;
module.exports.SqlError = require('./lib/misc/errors').SqlError;
module.exports.defaultOptions = function defaultOptions(opts) {
const connOpts = new ConnOptions(opts);
const res = {};
for (const [key, value] of Object.entries(connOpts)) {
if (!key.startsWith('_')) {
res[key] = value;
}
}
return res;
};
module.exports.createConnection = function createConnection(opts) {
const conn = new Connection(new ConnOptions(opts));
const connCallback = new ConnectionCallback(conn);
conn
.connect()
.then(
function () {
conn.emit('connect');
}.bind(conn)
)
.catch(conn.emit.bind(conn, 'connect'));
return connCallback;
};
exports.createPool = function createPool(opts) {
const options = new PoolOptions(opts);
const pool = new PoolCallback(options);
// adding a default error handler to avoid exiting application on connection error.
pool.on('error', (err) => {});
return pool;
};
exports.createPoolCluster = function createPoolCluster(opts) {
const options = new ClusterOptions(opts);
return new ClusterCallback(options);
};
module.exports.importFile = function importFile(opts, callback) {
const cb = callback ? callback : () => {};
try {
const options = new ConnOptions(opts);
const conn = new Connection(options);
conn
.connect()
.then(() => {
return new Promise(conn.importFile.bind(conn, Object.assign({ skipDbCheck: true }, opts)));
})
.then(() => cb())
.catch((err) => cb(err))
.finally(() => {
new Promise(conn.end.bind(conn, {})).catch(console.log);
});
} catch (err) {
cb(err);
}
};
+30
View File
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const hasMinVersion = function (nodeVersionStr, connectorRequirement) {
const versNode = nodeVersionStr.split('.');
const versReq = connectorRequirement.split('.');
const majorNode = Number(versNode[0]);
const majorReq = Number(versReq[0]);
if (majorNode > majorReq) return true;
if (majorNode < majorReq) return false;
if (versReq.length === 1) return true;
const minorNode = Number(versNode[1]);
const minorReq = Number(versReq[1]);
return minorNode >= minorReq;
};
module.exports.hasMinVersion = hasMinVersion;
const requirement = require('./package.json').engines.node;
const connectorRequirement = requirement.replace('>=', '').trim();
const currentNodeVersion = process.version.replace('v', '');
if (!hasMinVersion(currentNodeVersion, connectorRequirement)) {
console.error(`please upgrade node: mariadb requires at least version ${connectorRequirement}`);
process.exit(1);
}
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Cluster = require('./cluster');
/**
* Create a new Cluster.
* Cluster handle pools with patterns and handle failover / distributed load
* according to selectors (round-robin / random / ordered )
*
* @param args cluster arguments. see pool-cluster-options.
* @constructor
*/
class ClusterCallback {
#cluster;
constructor(args) {
this.#cluster = new Cluster(args);
this.#cluster._setCallback();
this.on = this.#cluster.on.bind(this.#cluster);
this.once = this.#cluster.once.bind(this.#cluster);
}
/**
* End cluster (and underlying pools).
*
* @param callback - not mandatory
*/
end(callback) {
if (callback && typeof callback !== 'function') {
throw new Error('callback parameter must be a function');
}
const endingFct = callback ? callback : () => {};
this.#cluster
.end()
.then(() => {
endingFct();
})
.catch(endingFct);
}
/**
* Get connection from an available pools matching pattern, according to selector
*
* @param pattern pattern filter (not mandatory)
* @param selector node selector ('RR','RANDOM' or 'ORDER')
* @param callback callback function
*/
getConnection(pattern, selector, callback) {
let pat = pattern,
sel = selector,
cal = callback;
if (typeof pattern === 'function') {
pat = null;
sel = null;
cal = pattern;
} else if (typeof selector === 'function') {
sel = null;
cal = selector;
}
const endingFct = cal ? cal : (err, conn) => {};
this.#cluster.getConnection(pat, sel, endingFct);
}
add(id, config) {
this.#cluster.add(id, config);
}
of(pattern, selector) {
return this.#cluster.of(pattern, selector);
}
remove(pattern) {
this.#cluster.remove(pattern);
}
get __tests() {
return this.#cluster.__tests;
}
}
module.exports = ClusterCallback;
+446
View File
@@ -0,0 +1,446 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const ClusterOptions = require('./config/cluster-options');
const PoolOptions = require('./config/pool-options');
const PoolCallback = require('./pool-callback');
const PoolPromise = require('./pool-promise');
const FilteredCluster = require('./filtered-cluster');
const FilteredClusterCallback = require('./filtered-cluster-callback');
const EventEmitter = require('events');
/**
* Create a new Cluster.
* Cluster handle pools with patterns and handle failover / distributed load
* according to selectors (round-robin / random / ordered )
*
* @param args cluster arguments. see pool-cluster-options.
* @constructor
*/
class Cluster extends EventEmitter {
#opts;
#nodes = {};
#cachedPatterns = {};
#nodeCounter = 0;
constructor(args) {
super();
this.#opts = new ClusterOptions(args);
}
/**
* Add a new pool node to the cluster.
*
* @param id identifier
* @param config pool configuration
*/
add(id, config) {
let identifier;
if (typeof id === 'string' || id instanceof String) {
identifier = id;
if (this.#nodes[identifier]) throw new Error(`Node identifier '${identifier}' already exist !`);
} else {
identifier = 'PoolNode-' + this.#nodeCounter++;
config = id;
}
const options = new PoolOptions(config);
this.#nodes[identifier] = this._createPool(options);
}
/**
* End cluster (and underlying pools).
*
* @return {Promise<any[]>}
*/
end() {
const cluster = this;
this.#cachedPatterns = {};
const poolEndPromise = [];
Object.keys(this.#nodes).forEach((pool) => {
const res = cluster.#nodes[pool].end();
if (res) poolEndPromise.push(res);
});
this.#nodes = null;
return Promise.all(poolEndPromise);
}
of(pattern, selector) {
return new FilteredCluster(this, pattern, selector);
}
_ofCallback(pattern, selector) {
return new FilteredClusterCallback(this, pattern, selector);
}
/**
* Remove nodes according to pattern.
*
* @param pattern pattern
*/
remove(pattern) {
if (!pattern) throw new Error('pattern parameter in Cluster.remove(pattern) is mandatory');
const regex = RegExp(pattern);
Object.keys(this.#nodes).forEach(
function (key) {
if (regex.test(key)) {
this.#nodes[key].end();
delete this.#nodes[key];
this.#cachedPatterns = {};
}
}.bind(this)
);
}
/**
* Get connection from an available pools matching pattern, according to selector
*
* @param pattern pattern filter (not mandatory)
* @param selector node selector ('RR','RANDOM' or 'ORDER')
* @return {Promise}
*/
getConnection(pattern, selector) {
return this._getConnection(pattern, selector, undefined, undefined, undefined);
}
/**
* Force using callback methods.
*/
_setCallback() {
this.getConnection = this._getConnectionCallback;
this._createPool = this._createPoolCallback;
this.of = this._ofCallback;
}
/**
* Get connection from an available pools matching pattern, according to selector
* with additional parameter to avoid reusing failing node
*
* @param pattern pattern filter (not mandatory)
* @param selector node selector ('RR','RANDOM' or 'ORDER')
* @param avoidNodeKey failing node
* @param lastError last error
* @param remainingRetry remaining possible retry
* @return {Promise}
* @private
*/
_getConnection(pattern, selector, remainingRetry, avoidNodeKey, lastError) {
const matchingNodeList = this._matchingNodes(pattern || /^/);
if (matchingNodeList.length === 0) {
if (Object.keys(this.#nodes).length === 0 && !lastError) {
return Promise.reject(
new Error('No node have been added to cluster or nodes have been removed due to too much connection error')
);
}
if (avoidNodeKey === undefined) return Promise.reject(new Error(`No node found for pattern '${pattern}'`));
const errMsg = `No Connection available for '${pattern}'${
lastError ? '. Last connection error was: ' + lastError.message : ''
}`;
return Promise.reject(new Error(errMsg));
}
if (remainingRetry === undefined) remainingRetry = matchingNodeList.length;
const retry = --remainingRetry >= 0 ? this._getConnection.bind(this, pattern, selector, remainingRetry) : null;
try {
const nodeKey = this._selectPool(matchingNodeList, selector, avoidNodeKey);
return this._handleConnectionError(matchingNodeList, nodeKey, retry);
} catch (e) {
return Promise.reject(e);
}
}
_createPool(options) {
const pool = new PoolPromise(options);
pool.on('error', (err) => {});
return pool;
}
_createPoolCallback(options) {
const pool = new PoolCallback(options);
pool.on('error', (err) => {});
return pool;
}
/**
* Get connection from an available pools matching pattern, according to selector
* with additional parameter to avoid reusing failing node
*
* @param pattern pattern filter (not mandatory)
* @param selector node selector ('RR','RANDOM' or 'ORDER')
* @param callback callback function
* @param remainingRetry remaining retry
* @param avoidNodeKey failing node
* @param lastError last error
* @private
*/
_getConnectionCallback(pattern, selector, callback, remainingRetry, avoidNodeKey, lastError) {
const matchingNodeList = this._matchingNodes(pattern || /^/);
if (matchingNodeList.length === 0) {
if (Object.keys(this.#nodes).length === 0 && !lastError) {
callback(
new Error('No node have been added to cluster or nodes have been removed due to too much connection error')
);
return;
}
if (avoidNodeKey === undefined) callback(new Error(`No node found for pattern '${pattern}'`));
const errMsg = `No Connection available for '${pattern}'${
lastError ? '. Last connection error was: ' + lastError.message : ''
}`;
callback(new Error(errMsg));
return;
}
if (remainingRetry === undefined) remainingRetry = matchingNodeList.length;
const retry =
--remainingRetry >= 0
? this._getConnectionCallback.bind(this, pattern, selector, callback, remainingRetry)
: null;
try {
const nodeKey = this._selectPool(matchingNodeList, selector, avoidNodeKey);
this._handleConnectionCallbackError(matchingNodeList, nodeKey, retry, callback);
} catch (e) {
callback(e);
}
}
/**
* Selecting nodes according to pattern.
*
* @param pattern pattern
* @return {*}
* @private
*/
_matchingNodes(pattern) {
if (this.#cachedPatterns[pattern]) return this.#cachedPatterns[pattern];
const regex = RegExp(pattern);
const matchingNodeList = [];
Object.keys(this.#nodes).forEach((key) => {
if (regex.test(key)) {
matchingNodeList.push(key);
}
});
this.#cachedPatterns[pattern] = matchingNodeList;
return matchingNodeList;
}
/**
* Select the next node to be chosen in the nodeList according to selector and failed nodes.
*
* @param nodeList current node list
* @param selectorParam selector
* @param avoidNodeKey last failing node to avoid selecting this one.
* @return {Promise}
* @private
*/
_selectPool(nodeList, selectorParam, avoidNodeKey) {
const selector = selectorParam || this.#opts.defaultSelector;
let selectorFct;
switch (selector) {
case 'RR':
selectorFct = roundRobinSelector;
break;
case 'RANDOM':
selectorFct = randomSelector;
break;
case 'ORDER':
selectorFct = orderedSelector;
break;
default:
throw new Error(`Wrong selector value '${selector}'. Possible values are 'RR','RANDOM' or 'ORDER'`);
}
let nodeIdx = 0;
let nodeKey = selectorFct(nodeList, nodeIdx);
// first loop : search for node not blacklisted AND not the avoided key
while (
(avoidNodeKey === nodeKey ||
(this.#nodes[nodeKey].blacklistedUntil && this.#nodes[nodeKey].blacklistedUntil > Date.now())) &&
nodeIdx < nodeList.length - 1
) {
nodeIdx++;
nodeKey = selectorFct(nodeList, nodeIdx);
}
if (avoidNodeKey === nodeKey) {
// second loop, search even in blacklisted node in order to choose a different node than to be avoided
nodeIdx = 0;
while (avoidNodeKey === nodeKey && nodeIdx < nodeList.length - 1) {
nodeIdx++;
nodeKey = selectorFct(nodeList, nodeIdx);
}
}
return nodeKey;
}
/**
* Handle node blacklisting and potential removal after a connection error
*
* @param {string} nodeKey - The key of the node that failed
* @param {Array<string>} nodeList - List of available nodes
* @returns {void}
* @private
*/
_handleNodeFailure(nodeKey, nodeList) {
const node = this.#nodes[nodeKey];
if (!node) return;
const cluster = this;
// Increment error count and blacklist node temporarily
node.errorCount = node.errorCount ? node.errorCount + 1 : 1;
node.blacklistedUntil = Date.now() + cluster.#opts.restoreNodeTimeout;
// Check if node should be removed due to excessive errors
if (
cluster.#opts.removeNodeErrorCount &&
node.errorCount >= cluster.#opts.removeNodeErrorCount &&
cluster.#nodes[nodeKey]
) {
delete cluster.#nodes[nodeKey];
cluster.#cachedPatterns = {};
delete nodeList.lastRrIdx;
setImmediate(cluster.emit.bind(cluster, 'remove', nodeKey));
if (node instanceof PoolCallback) {
node.end(() => {
// Intentionally ignore error during cleanup
});
} else {
node.end().catch((err) => {
// Intentionally ignore error during cleanup
});
}
}
}
/**
* Connect, or if fail handle retry / set timeout error
*
* @param nodeList current node list
* @param nodeKey node name to connect
* @param retryFct retry function
* @return {Promise}
* @private
*/
_handleConnectionError(nodeList, nodeKey, retryFct) {
const cluster = this;
const node = this.#nodes[nodeKey];
return node
.getConnection()
.then((conn) => {
// Connection successful, reset error state
node.blacklistedUntil = null;
node.errorCount = 0;
return conn;
})
.catch((err) => {
this._handleNodeFailure(nodeKey, nodeList);
if (nodeList.length !== 0 && cluster.#opts.canRetry && retryFct) {
return retryFct(nodeKey, err);
}
return Promise.reject(err);
});
}
/**
* Connect, or if fail handle retry / set timeout error
*
* @param nodeList current node list
* @param nodeKey node name to connect
* @param retryFct retry function
* @param callback callback function
* @private
*/
_handleConnectionCallbackError(nodeList, nodeKey, retryFct, callback) {
const cluster = this;
const node = this.#nodes[nodeKey];
node.getConnection((err, conn) => {
if (err) {
this._handleNodeFailure(nodeKey, nodeList);
if (nodeList.length !== 0 && cluster.#opts.canRetry && retryFct) {
return retryFct(nodeKey, err);
}
callback(err);
} else {
// Connection successful, reset error state
node.blacklistedUntil = null;
node.errorCount = 0;
callback(null, conn);
}
});
}
//*****************************************************************
// internal public testing methods
//*****************************************************************
get __tests() {
return new TestMethods(this.#nodes);
}
}
class TestMethods {
#nodes;
constructor(nodes) {
this.#nodes = nodes;
}
getNodes() {
return this.#nodes;
}
}
/**
* Round robin selector: using nodes one after the other.
*
* @param nodeList node list
* @return {String}
*/
const roundRobinSelector = (nodeList) => {
let lastRoundRobin = nodeList.lastRrIdx;
if (lastRoundRobin === undefined) lastRoundRobin = -1;
if (++lastRoundRobin >= nodeList.length) lastRoundRobin = 0;
nodeList.lastRrIdx = lastRoundRobin;
return nodeList[lastRoundRobin];
};
/**
* Random selector: use a random node.
*
* @param {Array<string>} nodeList - List of available nodes
* @return {String} - Selected node key
*/
const randomSelector = (nodeList) => {
const randomIdx = Math.floor(Math.random() * nodeList.length);
return nodeList[randomIdx];
};
/**
* Ordered selector: always use the nodes in sequence, unless failing.
*
* @param nodeList node list
* @param retry sequence number if last node is tagged has failing
* @return {String}
*/
const orderedSelector = (nodeList, retry) => {
return nodeList[retry];
};
module.exports = Cluster;
+677
View File
@@ -0,0 +1,677 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Parser = require('./parser');
const Errors = require('../misc/errors');
const BinaryEncoder = require('./encoder/binary-encoder');
const FieldType = require('../const/field-type');
const OkPacket = require('./class/ok-packet');
const Capabilities = require('../const/capabilities');
const ServerStatus = require('../const/server-status');
// GeoJSON types supported by MariaDB
const GEOJSON_TYPES = [
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
];
/**
* Protocol COM_STMT_BULK_EXECUTE implementation
* Provides efficient batch operations for MariaDB servers >= 10.2.7
*
* @see https://mariadb.com/kb/en/library/com_stmt_bulk_execute/
*/
class BatchBulk extends Parser {
constructor(resolve, reject, connOpts, prepare, cmdParam) {
super(resolve, reject, connOpts, cmdParam);
this.cmdOpts = cmdParam.opts;
this.binary = true;
this.prepare = prepare;
this.canSkipMeta = true;
this.bulkPacketNo = 0;
this.sending = false;
this.firstError = null;
}
/**
* Initiates the batch operation
*
* @param {Object} out - Output writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
*/
start(out, opts, info) {
this.info = info;
this.values = this.initialValues;
// Batch operations don't support timeouts
if (this.cmdOpts && this.cmdOpts.timeout) {
return this.handleTimeoutError(info);
}
this.onPacketReceive = this.readResponsePacket;
// Process named placeholders if needed
if (this.opts.namedPlaceholders && this.prepare._placeHolderIndex) {
this.processNamedPlaceholders();
}
// Validate parameters before proceeding
if (!this.validateParameters(info)) return;
// Send the bulk execute command
this.sendComStmtBulkExecute(out, opts, info);
}
/**
* Handle timeout error case
* @param {Object} info - Connection information
* @private
*/
handleTimeoutError(info) {
this.bulkPacketNo = 1;
this.sending = false;
return this.sendCancelled('Cannot use timeout for Batch statement', Errors.ER_TIMEOUT_NOT_SUPPORTED);
}
/**
* Process named placeholders to positional parameters
* @private
*/
processNamedPlaceholders() {
this.values = [];
if (!this.initialValues) return;
const placeHolderIndex = this.prepare._placeHolderIndex;
const paramCount = this.prepare.parameterCount;
for (let r = 0; r < this.initialValues.length; r++) {
const val = this.initialValues[r];
const newRow = new Array(paramCount);
for (let i = 0; i < placeHolderIndex.length; i++) {
newRow[i] = val[placeHolderIndex[i]];
}
this.values[r] = newRow;
}
}
/**
* Determine parameter header types based on value types
*
* @param {Array} value - Parameter values
* @param {Number} parameterCount - Number of parameters
* @returns {Array} Array of parameter header types
*/
parameterHeaderFromValue(value, parameterCount) {
const parameterHeaderType = new Array(parameterCount);
for (let i = 0; i < parameterCount; i++) {
const val = value[i];
if (val == null) {
parameterHeaderType[i] = FieldType.VAR_STRING;
continue;
}
const type = typeof val;
switch (type) {
case 'boolean':
parameterHeaderType[i] = FieldType.TINY;
break;
case 'bigint':
parameterHeaderType[i] = val >= 2n ** 63n ? FieldType.NEWDECIMAL : FieldType.BIGINT;
break;
case 'number':
if (Number.isInteger(val) && val >= -2147483648 && val < 2147483647) {
parameterHeaderType[i] = FieldType.INT;
} else {
parameterHeaderType[i] = FieldType.DOUBLE;
}
break;
case 'string':
parameterHeaderType[i] = FieldType.VAR_STRING;
break;
case 'object':
parameterHeaderType[i] = this.getObjectFieldType(val);
break;
default:
parameterHeaderType[i] = FieldType.BLOB;
}
}
return parameterHeaderType;
}
/**
* Determine field type for object values
*
* @param {Object} val - Object value
* @returns {Number} Field type constant
* @private
*/
getObjectFieldType(val) {
if (Object.prototype.toString.call(val) === '[object Date]') {
return FieldType.DATETIME;
}
if (Buffer.isBuffer(val)) {
return FieldType.BLOB;
}
if (typeof val.toSqlString === 'function') {
return FieldType.VAR_STRING;
}
if (val.type != null && GEOJSON_TYPES.includes(val.type)) {
return FieldType.BLOB;
}
return FieldType.VAR_STRING;
}
/**
* Check if current value has same header as set in initial BULK header
*
* @param {Array} parameterHeaderType - Current header types
* @param {Array} value - Current values
* @param {Number} parameterCount - Number of parameters
* @returns {Boolean} True if headers are identical
*/
checkSameHeader(parameterHeaderType, value, parameterCount) {
for (let i = 0; i < parameterCount; i++) {
const val = value[i];
if (val == null) continue;
const type = typeof val;
switch (type) {
case 'boolean':
if (parameterHeaderType[i] !== FieldType.TINY) return false;
break;
case 'bigint':
if (val >= 2n ** 63n) {
if (parameterHeaderType[i] !== FieldType.VAR_STRING) return false;
} else {
if (parameterHeaderType[i] !== FieldType.BIGINT) return false;
}
break;
case 'number':
if (Number.isInteger(val) && val >= -2147483648 && val < 2147483647) {
if (parameterHeaderType[i] !== FieldType.INT) return false;
} else {
if (parameterHeaderType[i] !== FieldType.DOUBLE) return false;
}
break;
case 'string':
if (parameterHeaderType[i] !== FieldType.VAR_STRING) return false;
break;
case 'object':
if (!this.checkObjectHeaderType(val, parameterHeaderType[i])) {
return false;
}
break;
default:
if (parameterHeaderType[i] !== FieldType.BLOB) return false;
}
}
return true;
}
/**
* Check if object value matches expected header type
*
* @param {Object} val - Object value
* @param {Number} headerType - Expected header type
* @returns {Boolean} True if types match
* @private
*/
checkObjectHeaderType(val, headerType) {
if (Object.prototype.toString.call(val) === '[object Date]') {
return headerType === FieldType.TIMESTAMP;
}
if (Buffer.isBuffer(val)) {
return headerType === FieldType.BLOB;
}
if (typeof val.toSqlString === 'function') {
return headerType === FieldType.VAR_STRING;
}
if (val.type != null && GEOJSON_TYPES.includes(val.type)) {
return headerType === FieldType.BLOB;
}
return headerType === FieldType.VAR_STRING;
}
/**
* Send a COM_STMT_BULK_EXECUTE command
*
* @param {Object} out - Output packet writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
*/
sendComStmtBulkExecute(out, opts, info) {
if (opts.logger.query) {
opts.logger.query(`BULK: (${this.prepare.id}) sql: ${opts.logParam ? this.displaySql() : this.sql}`);
}
const parameterCount = this.prepare.parameterCount;
this.rowIdx = 0;
this.vals = this.values[this.rowIdx++];
let parameterHeaderType = this.parameterHeaderFromValue(this.vals, parameterCount);
let lastCmdData = null;
this.bulkPacketNo = 0;
this.sending = true;
// Main processing loop for batching parameters
main_loop: while (true) {
this.bulkPacketNo++;
out.startPacket(this);
out.writeInt8(0xfa); // COM_STMT_BULK_EXECUTE
out.writeInt32(this.prepare.id); // Statement id
// Set flags: SEND_TYPES_TO_SERVER + SEND_UNIT_RESULTS if possible
this.useUnitResult = (info.clientCapabilities & Capabilities.BULK_UNIT_RESULTS) > 0;
out.writeInt16(this.useUnitResult ? 192 : 128);
// Write parameter header types
for (let i = 0; i < parameterCount; i++) {
out.writeInt16(parameterHeaderType[i]);
}
// Handle leftover data from previous packet
if (lastCmdData != null) {
const err = out.checkMaxAllowedLength(lastCmdData.length, info);
if (err) {
this.sending = false;
this.throwError(err, info);
return;
}
out.writeBuffer(lastCmdData, 0, lastCmdData.length);
out.mark();
lastCmdData = null;
if (this.rowIdx >= this.values.length) {
break;
}
this.vals = this.values[this.rowIdx++];
}
parameter_loop: while (true) {
// Write each parameter value
for (let i = 0; i < parameterCount; i++) {
const param = this.vals[i];
if (param != null) {
// Special handling for GeoJSON
if (param.type != null && GEOJSON_TYPES.includes(param.type)) {
this.writeGeoJSONParam(out, param, info);
} else {
out.writeInt8(0x00); // value follows
BinaryEncoder.writeParam(out, param, this.opts, info);
}
} else {
out.writeInt8(0x01); // value is null
}
}
// Buffer management for packet boundaries
if (out.isMarked() && (out.hasDataAfterMark() || out.bufIsAfterMaxPacketLength())) {
// Packet length was ok at last mark, but won't be with new data
out.flushBufferStopAtMark();
out.mark();
lastCmdData = out.resetMark();
break;
}
out.mark();
if (out.hasDataAfterMark()) {
// Flush has been done
lastCmdData = out.resetMark();
break;
}
if (this.rowIdx >= this.values.length) {
break main_loop;
}
this.vals = this.values[this.rowIdx++];
// Check if parameter types have changed
if (!this.checkSameHeader(parameterHeaderType, this.vals, parameterCount)) {
out.flush();
// Reset header type for new packet
parameterHeaderType = this.parameterHeaderFromValue(this.vals, parameterCount);
break parameter_loop;
}
}
}
out.flush();
this.sending = false;
this.emit('send_end');
}
/**
* Write GeoJSON parameter to output buffer
*
* @param {Object} out - Output buffer
* @param {Object} param - GeoJSON parameter
* @param {Object} info - connection info data
* @private
*/
writeGeoJSONParam(out, param, info) {
const geoBuff = BinaryEncoder.getBufferFromGeometryValue(param);
if (geoBuff == null) {
out.writeInt8(0x01); // value is null
} else {
out.writeInt8(0x00); // value follows
const paramBuff = Buffer.concat([
Buffer.from([0, 0, 0, 0]), // SRID
geoBuff // WKB
]);
BinaryEncoder.writeParam(out, paramBuff, this.opts, info);
}
}
/**
* Format SQL with parameters for logging
*
* @returns {String} Formatted SQL string
*/
displaySql() {
if (this.sql.length > this.opts.debugLen) {
return this.sql.substring(0, this.opts.debugLen) + '...';
}
let sqlMsg = this.sql + ' - parameters:[';
for (let i = 0; i < this.initialValues.length; i++) {
if (i !== 0) sqlMsg += ',';
let param = this.initialValues[i];
sqlMsg = Parser.logParameters(this.opts, sqlMsg, param);
if (sqlMsg.length > this.opts.debugLen) {
return sqlMsg.substring(0, this.opts.debugLen) + '...';
}
}
sqlMsg += ']';
return sqlMsg;
}
/**
* Process successful query execution
*
* @param {Object} initVal - Query result
*/
success(initVal) {
this.bulkPacketNo--;
if (!this.sending && this.bulkPacketNo === 0) {
this.packet = null;
if (this.firstError) {
this.resolve = null;
this.onPacketReceive = null;
this._columns = null;
this._rows = null;
process.nextTick(this.reject, this.firstError);
this.reject = null;
this.emit('end', this.firstError);
} else {
this.processResults();
}
return;
}
if (!this.firstError) {
this._responseIndex++;
this.onPacketReceive = this.readResponsePacket;
}
}
/**
* Process successful results based on result type
* @private
*/
processResults() {
if (this._rows[0] && this._rows[0][0] && this._rows[0][0]['Affected_rows'] !== undefined) {
this.processUnitResults();
} else if (
this._rows[0].affectedRows !== undefined &&
!(this.opts.fullResult === undefined || this.opts.fullResult === true)
) {
this.processAggregatedResults();
} else {
this.processRowResults();
}
this._columns = null;
this._rows = null;
}
/**
* Process unit results (for bulk operations with unit results)
* @private
*/
processUnitResults() {
if (this.opts.fullResult === undefined || this.opts.fullResult === true) {
const rs = [];
this._rows.forEach((row) => {
row.forEach((unitRow) => {
rs.push(new OkPacket(Number(unitRow['Affected_rows']), BigInt(unitRow['Id']), 0));
});
});
this.successEnd(this.opts.metaAsArray ? [rs, []] : rs);
} else {
let totalAffectedRows = 0;
this._rows.forEach((row) => {
row.forEach((unitRow) => {
totalAffectedRows += Number(unitRow['Affected_rows']);
});
});
const rs = new OkPacket(totalAffectedRows, BigInt(this._rows[0][0]['Id']), 0);
this.successEnd(this.opts.metaAsArray ? [rs, []] : rs);
}
}
/**
* Process aggregated results (for non-fullResult mode)
* @private
*/
processAggregatedResults() {
let totalAffectedRows = 0;
this._rows.forEach((row) => {
totalAffectedRows += row.affectedRows;
});
const rs = new OkPacket(totalAffectedRows, this._rows[0].insertId, this._rows[this._rows.length - 1].warningStatus);
this.successEnd(this.opts.metaAsArray ? [rs, []] : rs);
}
/**
* Process row results (for SELECT queries)
* @private
*/
processRowResults() {
if (this._rows.length === 1) {
this.successEnd(this.opts.metaAsArray ? [this._rows[0], this._columns] : this._rows[0]);
return;
}
if (this.opts.metaAsArray) {
if (this.useUnitResult) {
const rs = [];
this._rows.forEach((row, i) => {
if (i % 2 === 0) rs.push(...row);
});
this.successEnd([rs, this.prepare.columns]);
} else {
const rs = [];
this._rows.forEach((row) => {
rs.push(...row);
});
this.successEnd([rs, this._columns]);
}
} else {
if (this.useUnitResult) {
const rs = [];
this._rows.forEach((row, i) => {
if (i % 2 === 0) rs.push(...row);
});
Object.defineProperty(rs, 'meta', {
value: this._columns,
writable: true,
enumerable: this.opts.metaEnumerable
});
this.successEnd(rs);
} else {
if (this._rows.length === 1) {
this.successEnd(this._rows[0]);
} else {
const rs = [];
if (Array.isArray(this._rows[0])) {
this._rows.forEach((row) => {
rs.push(...row);
});
} else rs.push(...this._rows);
Object.defineProperty(rs, 'meta', {
value: this._columns,
writable: true,
enumerable: this.opts.metaEnumerable
});
this.successEnd(rs);
}
}
}
}
/**
* Handle OK packet success
*
* @param {Object} okPacket - OK packet
* @param {Object} info - Connection information
*/
okPacketSuccess(okPacket, info) {
this._rows.push(okPacket);
if (info.status & ServerStatus.MORE_RESULTS_EXISTS) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
if (this.opts.metaAsArray) {
if (!this._meta) {
this._meta = new Array(this._responseIndex);
}
this._meta[this._responseIndex] = null;
this.success([this._rows, this._meta]);
} else {
this.success(this._rows);
}
}
/**
* Handle errors during query execution
*
* @param {Error} err - Error object
* @param {Object} info - Connection information
*/
throwError(err, info) {
this.bulkPacketNo--;
if (!this.firstError) {
if (err.fatal) {
this.bulkPacketNo = 0;
}
if (this.cmdParam.stack) {
err = Errors.createError(
err.message,
err.errno,
info,
err.sqlState,
this.sql,
err.fatal,
this.cmdParam.stack,
false
);
}
this.firstError = err;
}
if (!this.sending && this.bulkPacketNo === 0) {
this.resolve = null;
this.emit('send_end');
process.nextTick(this.reject, this.firstError);
this.reject = null;
this.onPacketReceive = null;
this.emit('end', this.firstError);
} else {
this.onPacketReceive = this.readResponsePacket;
}
}
/**
* Validate that parameters exist and are defined
*
* @param {Object} info - Connection information
* @returns {Boolean} Returns false if any error occurs
*/
validateParameters(info) {
const nbParameter = this.prepare.parameterCount;
for (let r = 0; r < this.values.length; r++) {
if (!Array.isArray(this.values[r])) {
this.values[r] = [this.values[r]];
}
if (this.values[r].length < nbParameter) {
this.emit('send_end');
this.throwNewError(
`Expect ${nbParameter} parameters, but at index ${r}, parameters only contains ${this.values[r].length}\n ${
this.opts.logParam ? this.displaySql() : this.sql
}`,
false,
info,
'HY000',
Errors.ER_PARAMETER_UNDEFINED
);
return false;
}
}
return true;
}
}
module.exports = BatchBulk;
+159
View File
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
// noinspection JSBitwiseOperatorUsage
'use strict';
const Iconv = require('iconv-lite');
const Capabilities = require('../const/capabilities');
const Ed25519PasswordAuth = require('./handshake/auth/ed25519-password-auth');
const NativePasswordAuth = require('./handshake/auth/native-password-auth');
const Collations = require('../const/collations');
const Authentication = require('./handshake/authentication');
/**
* send a COM_CHANGE_USER: resets the connection and re-authenticates with the given credentials
* see https://mariadb.com/kb/en/library/com_change_user/
*/
class ChangeUser extends Authentication {
constructor(cmdParam, connOpts, resolve, reject, getSocket) {
super(cmdParam, resolve, reject, () => {}, getSocket);
this.configAssign(connOpts, cmdParam.opts);
}
start(out, opts, info) {
if (opts.logger.query) opts.logger.query(`CHANGE USER to '${this.opts.user || ''}'`);
let authToken;
const pwd = Array.isArray(this.opts.password) ? this.opts.password[0] : this.opts.password;
switch (info.defaultPluginName) {
case 'mysql_native_password':
case '':
authToken = NativePasswordAuth.encryptSha1Password(pwd, info.seed);
break;
case 'client_ed25519':
authToken = Ed25519PasswordAuth.encryptPassword(pwd, info.seed);
break;
default:
authToken = Buffer.alloc(0);
break;
}
out.startPacket(this);
out.writeInt8(0x11);
out.writeString(this.opts.user || '');
out.writeInt8(0);
if (info.serverCapabilities & Capabilities.SECURE_CONNECTION) {
out.writeInt8(authToken.length);
out.writeBuffer(authToken, 0, authToken.length);
} else {
out.writeBuffer(authToken, 0, authToken.length);
out.writeInt8(0);
}
if (info.clientCapabilities & Capabilities.CONNECT_WITH_DB) {
out.writeString(this.opts.database);
out.writeInt8(0);
info.database = this.opts.database;
}
// handle default collation.
if (this.opts.collation) {
// collation has been set using charset.
// If server use same charset, use server collation.
if (!this.opts.charset || info.collation.charset !== this.opts.collation.charset) {
info.collation = this.opts.collation;
}
} else {
// if not utf8mb4 and no configuration, force to use UTF8MB4_UNICODE_CI
if (info.collation.charset !== 'utf8' || info.collation.maxLength === 3) {
info.collation = Collations.fromIndex(224);
}
}
out.writeInt16(info.collation.index);
if (info.clientCapabilities & Capabilities.PLUGIN_AUTH) {
out.writeString(info.defaultPluginName);
out.writeInt8(0);
}
if (info.clientCapabilities & Capabilities.CONNECT_ATTRS) {
out.writeInt8(0xfc);
let initPos = out.pos; //save position, assuming connection attributes length will be less than 2 bytes length
out.writeInt16(0);
const encoding = info.collation.charset;
writeAttribute(out, '_client_name', encoding);
writeAttribute(out, 'MariaDB connector/Node', encoding);
let packageJson = require('../../package.json');
writeAttribute(out, '_client_version', encoding);
writeAttribute(out, packageJson.version, encoding);
writeAttribute(out, '_node_version', encoding);
writeAttribute(out, process.versions.node, encoding);
if (opts.connectAttributes !== true) {
let attrNames = Object.keys(this.opts.connectAttributes);
for (let k = 0; k < attrNames.length; ++k) {
writeAttribute(out, attrNames[k], encoding);
writeAttribute(out, this.opts.connectAttributes[attrNames[k]], encoding);
}
}
//write end size
out.writeInt16AtPos(initPos);
}
out.flush();
this.plugin.onPacketReceive = this.handshakeResult.bind(this);
}
/**
* Assign global configuration option used by result-set to current query option.
* a little faster than Object.assign() since doest copy all information
*
* @param connOpts connection global configuration
* @param cmdOpts current options
*/
configAssign(connOpts, cmdOpts) {
if (!cmdOpts) {
this.opts = connOpts;
return;
}
this.opts = cmdOpts ? Object.assign({}, connOpts, cmdOpts) : connOpts;
if (cmdOpts.charset && typeof cmdOpts.charset === 'string') {
this.opts.collation = Collations.fromCharset(cmdOpts.charset.toLowerCase());
if (this.opts.collation === undefined) {
this.opts.collation = Collations.fromName(cmdOpts.charset.toUpperCase());
if (this.opts.collation !== undefined) {
this.opts.logger.warning(
"warning: please use option 'collation' " +
"in replacement of 'charset' when using a collation name ('" +
cmdOpts.charset +
"')\n" +
"(collation looks like 'UTF8MB4_UNICODE_CI', charset like 'utf8')."
);
}
}
if (this.opts.collation === undefined) throw new RangeError("Unknown charset '" + cmdOpts.charset + "'");
} else if (cmdOpts.collation && typeof cmdOpts.collation === 'string') {
const initial = cmdOpts.collation;
this.opts.collation = Collations.fromName(initial.toUpperCase());
if (this.opts.collation === undefined) throw new RangeError("Unknown collation '" + initial + "'");
} else {
this.opts.collation = Collations.fromIndex(cmdOpts.charsetNumber) || connOpts.collation;
}
connOpts.password = cmdOpts.password;
}
}
function writeAttribute(out, val, encoding) {
let param = Buffer.isEncoding(encoding) ? Buffer.from(val, encoding) : Iconv.encode(val, encoding);
out.writeLengthCoded(param.length);
out.writeBuffer(param, 0, param.length);
}
module.exports = ChangeUser;
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
/**
* Ok_Packet
* see https://mariadb.com/kb/en/ok_packet/
*/
class OkPacket {
constructor(affectedRows, insertId, warningStatus) {
this.affectedRows = affectedRows;
this.insertId = insertId;
this.warningStatus = warningStatus;
}
}
module.exports = OkPacket;
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const PrepareWrapper = require('./prepare-wrapper');
/**
* Prepare cache wrapper
* see https://mariadb.com/kb/en/com_stmt_prepare/#com_stmt_prepare_ok
*/
class PrepareCacheWrapper {
#use = 0;
#cached;
#prepare;
constructor(prepare) {
this.#prepare = prepare;
this.#cached = true;
}
incrementUse() {
this.#use += 1;
return new PrepareWrapper(this, this.#prepare);
}
unCache() {
this.#cached = false;
if (this.#use === 0) {
this.#prepare.close();
}
}
decrementUse() {
this.#use -= 1;
if (this.#use === 0 && !this.#cached) {
this.#prepare.close();
}
}
toString() {
return 'Prepare{use:' + this.#use + ',cached:' + this.#cached + '}';
}
}
module.exports = PrepareCacheWrapper;
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Errors = require('../../misc/errors');
const ExecuteStream = require('../execute-stream');
const Parser = require('../parser');
/**
* Prepare result
* see https://mariadb.com/kb/en/com_stmt_prepare/#com_stmt_prepare_ok
*/
class PrepareResultPacket {
#conn;
constructor(statementId, parameterCount, columns, database, sql, placeHolderIndex, conn) {
this.id = statementId;
this.parameterCount = parameterCount;
this.columns = columns;
this.database = database;
this.query = sql;
this.closed = false;
this._placeHolderIndex = placeHolderIndex;
this.#conn = conn;
}
get conn() {
return this.#conn;
}
execute(values, opts, cb, stack) {
let _opts = opts,
_cb = cb;
if (typeof _opts === 'function') {
_cb = _opts;
_opts = undefined;
}
if (this.isClose()) {
let sql = this.query;
if (this.conn.opts.logParam) {
if (this.query.length > this.conn.opts.debugLen) {
sql = this.query.substring(0, this.conn.opts.debugLen) + '...';
} else {
let sqlMsg = this.query + ' - parameters:';
sql = Parser.logParameters(this.conn.opts, sqlMsg, values);
}
}
const error = Errors.createError(
`Execute fails, prepare command as already been closed`,
Errors.ER_PREPARE_CLOSED,
null,
'22000',
sql
);
if (!_cb) {
return Promise.reject(error);
} else {
_cb(error);
return;
}
}
const cmdParam = {
sql: this.query,
values: values,
opts: _opts,
callback: _cb
};
if (stack) cmdParam.stack = stack;
const conn = this.conn;
const promise = new Promise((resolve, reject) => conn.executePromise.call(conn, cmdParam, this, resolve, reject));
if (!_cb) {
return promise;
} else {
promise
.then((res) => {
if (_cb) _cb(null, res, null);
})
.catch(_cb || function (err) {});
}
}
executeStream(values, opts, cb, stack) {
let _opts = opts,
_cb = cb;
if (typeof _opts === 'function') {
_cb = _opts;
_opts = undefined;
}
if (this.isClose()) {
const error = Errors.createError(
`Execute fails, prepare command as already been closed`,
Errors.ER_PREPARE_CLOSED,
null,
'22000',
this.query
);
if (!_cb) {
throw error;
} else {
_cb(error);
return;
}
}
const cmdParam = {
sql: this.query,
values: values,
opts: _opts,
callback: _cb
};
if (stack) cmdParam.stack = stack;
const cmd = new ExecuteStream(cmdParam, this.conn.opts, this, this.conn.socket);
if (this.conn.opts.logger.error) cmd.on('error', this.conn.opts.logger.error);
this.conn.addCommand(cmd, true);
return cmd.inStream;
}
isClose() {
return this.closed;
}
close() {
if (!this.closed) {
this.closed = true;
this.#conn.emit('close_prepare', this);
}
}
toString() {
return 'Prepare{closed:' + this.closed + '}';
}
}
module.exports = PrepareResultPacket;
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
/**
* Prepare result wrapper
* This permit to ensure that cache can be close only one time cache.
*/
class PrepareWrapper {
#closed = false;
#cacheWrapper;
#prepare;
#conn;
constructor(cacheWrapper, prepare) {
this.#cacheWrapper = cacheWrapper;
this.#prepare = prepare;
this.#conn = prepare.conn;
this.execute = this.#prepare.execute;
this.executeStream = this.#prepare.executeStream;
}
get conn() {
return this.#conn;
}
get id() {
return this.#prepare.id;
}
get parameterCount() {
return this.#prepare.parameterCount;
}
get _placeHolderIndex() {
return this.#prepare._placeHolderIndex;
}
get columns() {
return this.#prepare.columns;
}
set columns(columns) {
this.#prepare.columns = columns;
}
get database() {
return this.#prepare.database;
}
get query() {
return this.#prepare.query;
}
isClose() {
return this.#closed;
}
close() {
if (!this.#closed) {
this.#closed = true;
this.#cacheWrapper.decrementUse();
}
}
toString() {
return 'PrepareWrapper{closed:' + this.#closed + ',cache:' + this.#cacheWrapper + '}';
}
}
module.exports = PrepareWrapper;
+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Command = require('./command');
/**
* Close prepared statement
* see https://mariadb.com/kb/en/3-binary-protocol-prepared-statements-com_stmt_close/
*/
class ClosePrepare extends Command {
constructor(cmdParam, resolve, reject, prepare) {
super(cmdParam, resolve, reject);
this.prepare = prepare;
}
start(out, opts, info) {
if (opts.logger.query) opts.logger.query(`CLOSE PREPARE: (${this.prepare.id}) ${this.prepare.query}`);
const closeCmd = new Uint8Array([
5,
0,
0,
0,
0x19,
this.prepare.id,
this.prepare.id >> 8,
this.prepare.id >> 16,
this.prepare.id >> 24
]);
out.fastFlush(this, closeCmd);
this.onPacketReceive = null;
this.emit('send_end');
this.emit('end');
}
}
module.exports = ClosePrepare;
+200
View File
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Collations = require('../const/collations.js');
const FieldType = require('../const/field-type');
const FieldDetails = require('../const/field-detail');
const Capabilities = require('../const/capabilities');
// noinspection JSBitwiseOperatorUsage
/**
* Column definition
* see https://mariadb.com/kb/en/library/resultset/#column-definition-packet
*/
class ColumnDef {
#stringParser;
constructor(packet, info, skipName) {
this.#stringParser = skipName ? new StringParser(packet) : new StringParserWithName(packet);
if (info.clientCapabilities & Capabilities.MARIADB_CLIENT_EXTENDED_METADATA) {
const len = packet.readUnsignedLength();
if (len > 0) {
const subPacket = packet.subPacketLengthEncoded(len);
while (subPacket.remaining()) {
switch (subPacket.readUInt8()) {
case 0:
this.dataTypeName = subPacket.readAsciiStringLengthEncoded();
break;
case 1:
this.dataTypeFormat = subPacket.readAsciiStringLengthEncoded();
break;
default:
subPacket.skip(subPacket.readUnsignedLength());
break;
}
}
}
}
packet.skip(1); // length of fixed fields
this.collation = Collations.fromIndex(packet.readUInt16());
this.columnLength = packet.readUInt32();
this.columnType = packet.readUInt8();
this.flags = packet.readUInt16();
this.scale = packet.readUInt8();
this.type = FieldType.TYPES[this.columnType];
}
__getDefaultGeomVal() {
if (this.dataTypeName) {
switch (this.dataTypeName) {
case 'point':
return { type: 'Point' };
case 'linestring':
return { type: 'LineString' };
case 'polygon':
return { type: 'Polygon' };
case 'multipoint':
return { type: 'MultiPoint' };
case 'multilinestring':
return { type: 'MultiLineString' };
case 'multipolygon':
return { type: 'MultiPolygon' };
default:
return { type: this.dataTypeName };
}
}
return null;
}
db() {
return this.#stringParser.db();
}
schema() {
return this.#stringParser.schema();
}
table() {
return this.#stringParser.table();
}
orgTable() {
return this.#stringParser.orgTable();
}
name() {
return this.#stringParser.name();
}
orgName() {
return this.#stringParser.orgName();
}
signed() {
return (this.flags & FieldDetails.UNSIGNED) === 0;
}
isSet() {
return (this.flags & FieldDetails.SET) !== 0;
}
}
/**
* String parser.
* This object permits avoiding listing all private information to a metadata object.
*/
class BaseStringParser {
constructor(encoding, readFct, saveBuf, initialPos) {
this.buf = saveBuf;
this.encoding = encoding;
this.readString = readFct;
this.initialPos = initialPos;
}
_readIdentifier(skip) {
let pos = this.initialPos;
while (skip-- > 0) {
const type = this.buf[pos++];
pos += type < 0xfb ? type : 2 + this.buf[pos] + this.buf[pos + 1] * 2 ** 8;
}
const type = this.buf[pos++];
const len = type < 0xfb ? type : this.buf[pos++] + this.buf[pos++] * 2 ** 8;
return this.readString(this.encoding, this.buf, pos, len);
}
name() {
return this._readIdentifier(3);
}
db() {
let pos = this.initialPos;
return this.readString(this.encoding, this.buf, pos + 1, this.buf[pos]);
}
schema() {
return this.db();
}
table() {
let pos = this.initialPos + 1 + this.buf[this.initialPos];
const type = this.buf[pos++];
const len = type < 0xfb ? type : this.buf[pos++] + this.buf[pos++] * 2 ** 8;
return this.readString(this.encoding, this.buf, pos, len);
}
orgTable() {
return this._readIdentifier(2);
}
orgName() {
return this._readIdentifier(4);
}
}
class StringParser extends BaseStringParser {
constructor(packet) {
packet.skip(packet.readUInt8()); //catalog
const initPos = packet.pos;
packet.skip(packet.readUInt8()); //schema
packet.skip(packet.readMetadataLength()); //table alias
packet.skip(packet.readUInt8()); //table
packet.skip(packet.readMetadataLength()); //column alias
packet.skip(packet.readUInt8()); //column
super(packet.encoding, packet.constructor.readString, packet.buf, initPos);
}
}
/**
* String parser.
* This object permits to avoid listing all private information to metadata object.
*/
class StringParserWithName extends BaseStringParser {
colName;
constructor(packet) {
packet.skip(packet.readUInt8()); //catalog
const initPos = packet.pos;
packet.skip(packet.readUInt8()); //schema
packet.skip(packet.readMetadataLength()); //table alias
packet.skip(packet.readUInt8()); //table
const colName = packet.readStringLengthEncoded(); //column alias
packet.skip(packet.readUInt8()); //column
super(packet.encoding, packet.constructor.readString, packet.buf, initPos);
this.colName = colName;
}
name() {
return this.colName;
}
}
module.exports = ColumnDef;
+134
View File
@@ -0,0 +1,134 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const EventEmitter = require('events');
const Errors = require('../misc/errors');
/**
* Default command interface.
*/
class Command extends EventEmitter {
constructor(cmdParam, resolve, reject) {
super();
this.cmdParam = cmdParam;
this.sequenceNo = -1;
this.compressSequenceNo = -1;
this.resolve = resolve;
this.reject = reject;
this.sending = false;
this.unexpectedError = this.throwUnexpectedError.bind(this);
}
displaySql() {
return null;
}
/**
* Throw an unexpected error.
* server exchange will still be read to keep connection in a good state, but promise will be rejected.
*
* @param msg message
* @param fatal is error fatal for connection
* @param info current server state information
* @param sqlState error sqlState
* @param errno error number
*/
throwUnexpectedError(msg, fatal, info, sqlState, errno) {
const err = Errors.createError(
msg,
errno,
info,
sqlState,
this.opts && this.opts.logParam ? this.displaySql() : this.sql,
fatal,
this.cmdParam ? this.cmdParam.stack : null,
false
);
if (this.reject) {
process.nextTick(this.reject, err);
this.resolve = null;
this.reject = null;
}
return err;
}
/**
* Create and throw new Error from error information
* only first called throwing an error or successfully end will be executed.
*
* @param msg message
* @param fatal is error fatal for connection
* @param info current server state information
* @param sqlState error sqlState
* @param errno error number
*/
throwNewError(msg, fatal, info, sqlState, errno) {
this.onPacketReceive = null;
const err = this.throwUnexpectedError(msg, fatal, info, sqlState, errno);
this.emit('end');
return err;
}
/**
* When command cannot be sent due to error.
* (this is only on start command)
*
* @param msg error message
* @param errno error number
* @param info connection information
*/
sendCancelled(msg, errno, info) {
const err = Errors.createError(msg, errno, info, 'HY000', this.opts.logParam ? this.displaySql() : this.sql);
this.emit('send_end');
this.throwError(err, info);
}
/**
* Throw Error
* only first called throwing an error or successfully end will be executed.
*
* @param err error to be thrown
* @param info current server state information
*/
throwError(err, info) {
this.onPacketReceive = null;
if (this.reject) {
if (this.cmdParam && this.cmdParam.stack) {
err = Errors.createError(
err.text ? err.text : err.message,
err.errno,
info,
err.sqlState,
err.sql,
err.fatal,
this.cmdParam.stack,
false
);
}
this.resolve = null;
process.nextTick(this.reject, err);
this.reject = null;
}
this.emit('end', err);
}
/**
* Successfully end command.
* only first called throwing an error or successfully end will be executed.
*
* @param val return value.
*/
successEnd(val) {
this.onPacketReceive = null;
if (this.resolve) {
this.reject = null;
process.nextTick(this.resolve, val);
this.resolve = null;
}
this.emit('end');
}
}
module.exports = Command;
@@ -0,0 +1,282 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const FieldType = require('../../const/field-type');
const Errors = require('../../misc/errors');
module.exports.newRow = function (packet, columns) {
packet.skip(1); // skip 0x00 header.
const len = ~~((columns.length + 9) / 8);
const nullBitMap = new Array(len);
for (let i = 0; i < len; i++) nullBitMap[i] = packet.readUInt8();
return nullBitMap;
};
module.exports.castWrapper = function (column, packet, opts, nullBitmap, index) {
column.string = () => (isNullBitmap(index, nullBitmap) ? null : packet.readStringLengthEncoded());
column.buffer = () => (isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded());
column.float = () => (isNullBitmap(index, nullBitmap) ? null : packet.readFloat());
column.tiny = () =>
isNullBitmap(index, nullBitmap) ? null : column.signed() ? packet.readInt8() : packet.readUInt8();
column.short = () =>
isNullBitmap(index, nullBitmap) ? null : column.signed() ? packet.readInt16() : packet.readUInt16();
column.int = () => (isNullBitmap(index, nullBitmap) ? null : packet.readInt32());
column.long = () => (isNullBitmap(index, nullBitmap) ? null : packet.readBigInt64());
column.decimal = () => (isNullBitmap(index, nullBitmap) ? null : packet.readDecimalLengthEncoded());
column.date = () => (isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDate(opts));
column.datetime = () => (isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTime());
column.geometry = () => {
let defaultVal = null;
if (column.dataTypeName) {
switch (column.dataTypeName) {
case 'point':
defaultVal = { type: 'Point' };
break;
case 'linestring':
defaultVal = { type: 'LineString' };
break;
case 'polygon':
defaultVal = { type: 'Polygon' };
break;
case 'multipoint':
defaultVal = { type: 'MultiPoint' };
break;
case 'multilinestring':
defaultVal = { type: 'MultiLineString' };
break;
case 'multipolygon':
defaultVal = { type: 'MultiPolygon' };
break;
default:
defaultVal = { type: column.dataTypeName };
break;
}
}
if (isNullBitmap(index, nullBitmap)) {
return defaultVal;
}
return packet.readGeometry(defaultVal);
};
};
module.exports.parser = function (col, opts) {
// set reader function read(col, packet, index, nullBitmap, opts, throwUnexpectedError)
// this permit for multi-row result-set to avoid resolving type parsing each data.
// return constant parser (function not depending on column info other than type)
const defaultParser = col.signed()
? DEFAULT_SIGNED_PARSER_TYPE[col.columnType]
: DEFAULT_UNSIGNED_PARSER_TYPE[col.columnType];
if (defaultParser) return defaultParser;
// parser depending on column info
switch (col.columnType) {
case FieldType.BIGINT:
if (col.signed()) {
return opts.bigIntAsNumber || opts.supportBigNumbers ? readBigintAsIntBinarySigned : readBigintBinarySigned;
}
return opts.bigIntAsNumber || opts.supportBigNumbers ? readBigintAsIntBinaryUnsigned : readBigintBinaryUnsigned;
case FieldType.DATETIME:
case FieldType.TIMESTAMP:
return opts.dateStrings ? readTimestampStringBinary.bind(null, col.scale) : readTimestampBinary;
case FieldType.DECIMAL:
case FieldType.NEWDECIMAL:
return col.scale === 0 ? readDecimalAsIntBinary : readDecimalBinary;
case FieldType.GEOMETRY:
let defaultVal = col.__getDefaultGeomVal();
return readGeometryBinary.bind(null, defaultVal);
case FieldType.BIT:
if (col.columnLength === 1 && opts.bitOneIsBoolean) {
return readBitBinaryBoolean;
}
return readBinaryBuffer;
case FieldType.JSON:
return opts.jsonStrings ? readStringBinary : readJsonBinary;
default:
if (col.dataTypeFormat && col.dataTypeFormat === 'json' && opts.autoJsonMap) {
return readJsonBinary;
}
if (col.collation.index === 63) {
return readBinaryBuffer;
}
if (col.isSet()) {
return readBinarySet;
}
return readStringBinary;
}
};
const isNullBitmap = (index, nullBitmap) => {
return (nullBitmap[~~((index + 2) / 8)] & (1 << (index + 2) % 8)) > 0;
};
const readTinyBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readInt8();
const readTinyBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readUInt8();
const readShortBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readInt16();
const readShortBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readUInt16();
const readMediumBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
if (isNullBitmap(index, nullBitmap)) {
return null;
}
const result = packet.readInt24();
packet.skip(1); // MEDIUMINT is encoded on 4 bytes in exchanges !
return result;
};
const readMediumBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
if (isNullBitmap(index, nullBitmap)) {
return null;
}
const result = packet.readUInt24();
packet.skip(1); // MEDIUMINT is encoded on 4 bytes in exchanges !
return result;
};
const readIntBinarySigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readInt32();
const readIntBinaryUnsigned = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readUInt32();
const readFloatBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readFloat();
const readDoubleBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readDouble();
const readBigintBinaryUnsigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
if (isNullBitmap(index, nullBitmap)) return null;
return packet.readBigUInt64();
};
const readBigintBinarySigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
if (isNullBitmap(index, nullBitmap)) return null;
return packet.readBigInt64();
};
const readBigintAsIntBinaryUnsigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
if (isNullBitmap(index, nullBitmap)) return null;
const val = packet.readBigUInt64();
if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
return throwUnexpectedError(
`value ${val} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(val)))) {
return val.toString();
}
return Number(val);
};
const readBigintAsIntBinarySigned = function (packet, opts, throwUnexpectedError, nullBitmap, index) {
if (isNullBitmap(index, nullBitmap)) return null;
const val = packet.readBigInt64();
if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
return throwUnexpectedError(
`value ${val} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(val)))) {
return val.toString();
}
return Number(val);
};
const readGeometryBinary = (defaultVal, packet, opts, throwUnexpectedError, nullBitmap, index) => {
if (isNullBitmap(index, nullBitmap)) {
return defaultVal;
}
return packet.readGeometry(defaultVal);
};
const readDateBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDate(opts);
const readTimestampBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTime();
const readTimestampStringBinary = (scale, packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryDateTimeAsString(scale);
const readTimeBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBinaryTime();
const readDecimalAsIntBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
//checkNumberRange additional check is only done when
// resulting value is an integer
if (isNullBitmap(index, nullBitmap)) return null;
const valDec = packet.readDecimalLengthEncoded();
if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
if (opts.decimalAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(valDec))) {
return throwUnexpectedError(
`value ${valDec} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(Number(valDec)))) {
return valDec;
}
return Number(valDec);
}
return valDec;
};
const readDecimalBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
if (isNullBitmap(index, nullBitmap)) return null;
const valDec = packet.readDecimalLengthEncoded();
if (valDec != null && (opts.decimalAsNumber || opts.supportBigNumbers)) {
const numberValue = Number(valDec);
if (
opts.supportBigNumbers &&
(opts.bigNumberStrings || (Number.isInteger(numberValue) && !Number.isSafeInteger(numberValue)))
) {
return valDec;
}
return numberValue;
}
return valDec;
};
const readJsonBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : JSON.parse(packet.readStringLengthEncoded());
const readBitBinaryBoolean = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded()[0] === 1;
const readBinaryBuffer = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readBufferLengthEncoded();
const readBinarySet = (packet, opts, throwUnexpectedError, nullBitmap, index) => {
if (isNullBitmap(index, nullBitmap)) return null;
const string = packet.readStringLengthEncoded();
return string == null ? null : string === '' ? [] : string.split(',');
};
const readStringBinary = (packet, opts, throwUnexpectedError, nullBitmap, index) =>
isNullBitmap(index, nullBitmap) ? null : packet.readStringLengthEncoded();
const DEFAULT_SIGNED_PARSER_TYPE = Array(256);
DEFAULT_SIGNED_PARSER_TYPE[FieldType.TINY] = readTinyBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.YEAR] = readShortBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.SHORT] = readShortBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.INT24] = readMediumBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.INT] = readIntBinarySigned;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.FLOAT] = readFloatBinary;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.DOUBLE] = readDoubleBinary;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.DATE] = readDateBinary;
DEFAULT_SIGNED_PARSER_TYPE[FieldType.TIME] = readTimeBinary;
const DEFAULT_UNSIGNED_PARSER_TYPE = Array(256);
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.TINY] = readTinyBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.YEAR] = readShortBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.SHORT] = readShortBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.INT24] = readMediumBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.INT] = readIntBinaryUnsigned;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.FLOAT] = readFloatBinary;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.DOUBLE] = readDoubleBinary;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.DATE] = readDateBinary;
DEFAULT_UNSIGNED_PARSER_TYPE[FieldType.TIME] = readTimeBinary;
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const FieldType = require('../../const/field-type');
const Errors = require('../../misc/errors');
module.exports.parser = function (col, opts) {
// Fast path: For most types, we can directly return the default parser
// This avoids the cost of the switch statement for common types
const defaultParser = DEFAULT_PARSER_TYPE[col.columnType];
if (defaultParser) return defaultParser;
// Parser depending on column info
switch (col.columnType) {
case FieldType.DECIMAL:
case FieldType.NEWDECIMAL:
return col.scale === 0 ? readDecimalAsIntLengthCoded : readDecimalLengthCoded;
case FieldType.BIGINT:
if (opts.bigIntAsNumber || opts.supportBigNumbers) return readBigIntAsNumberLengthCoded;
return readBigIntLengthCoded;
case FieldType.GEOMETRY:
const defaultVal = col.__getDefaultGeomVal();
return function (packet, opts, throwUnexpectedError) {
return packet.readGeometry(defaultVal);
};
case FieldType.BIT:
if (col.columnLength === 1 && opts.bitOneIsBoolean) {
return readBitAsBoolean;
}
return readBufferLengthEncoded;
case FieldType.JSON:
return opts.jsonStrings ? readStringLengthEncoded : readJson;
default:
if (col.dataTypeFormat === 'json' && opts.autoJsonMap) {
return readJson;
}
if (col.collation.index === 63) {
return readBufferLengthEncoded;
}
if (col.isSet()) {
return readSet;
}
return readStringLengthEncoded;
}
};
module.exports.castWrapper = function (column, packet, opts, nullBitmap, index) {
const p = packet;
column.string = () => p.readStringLengthEncoded();
column.buffer = () => p.readBufferLengthEncoded();
column.float = () => p.readFloatLengthCoded();
column.tiny = column.short = column.int = () => p.readIntLengthEncoded();
column.long = () => p.readBigIntLengthEncoded();
column.decimal = () => p.readDecimalLengthEncoded();
column.date = () => p.readDate(opts);
column.datetime = () => p.readDateTime();
// Only define geometry method if needed (likely less common)
// Inline the geometry switch case for better performance
column.geometry = () => {
let defaultVal = null;
if (column.dataTypeName) {
// Use object lookup instead of switch for better performance
const geoTypes = {
point: { type: 'Point' },
linestring: { type: 'LineString' },
polygon: { type: 'Polygon' },
multipoint: { type: 'MultiPoint' },
multilinestring: { type: 'MultiLineString' },
multipolygon: { type: 'MultiPolygon' }
};
defaultVal = geoTypes[column.dataTypeName] || { type: column.dataTypeName };
}
return p.readGeometry(defaultVal);
};
};
const readIntLengthEncoded = (packet, opts, throwUnexpectedError) => packet.readIntLengthEncoded();
const readStringLengthEncoded = (packet, opts, throwUnexpectedError) => packet.readStringLengthEncoded();
const readFloatLengthCoded = (packet, opts, throwUnexpectedError) => packet.readFloatLengthCoded();
const readBigIntLengthCoded = (packet, opts, throwUnexpectedError) => packet.readBigIntLengthEncoded();
const readAsciiStringLengthEncoded = (packet, opts, throwUnexpectedError) => packet.readAsciiStringLengthEncoded();
const readBitAsBoolean = (packet, opts, throwUnexpectedError) => {
const val = packet.readBufferLengthEncoded();
return val == null ? null : val[0] === 1;
};
const readBufferLengthEncoded = (packet, opts, throwUnexpectedError) => packet.readBufferLengthEncoded();
const readJson = (packet, opts, throwUnexpectedError) => {
const jsonStr = packet.readStringLengthEncoded();
return jsonStr === null ? null : JSON.parse(jsonStr);
};
const readSet = (packet, opts, throwUnexpectedError) => {
const string = packet.readStringLengthEncoded();
return string == null ? null : string === '' ? [] : string.split(',');
};
const readDate = (packet, opts, throwUnexpectedError) =>
opts.dateStrings ? packet.readAsciiStringLengthEncoded() : packet.readDate();
const readTimestamp = (packet, opts, throwUnexpectedError) =>
opts.dateStrings ? packet.readAsciiStringLengthEncoded() : packet.readDateTime();
// Initialize the DEFAULT_PARSER_TYPE array with frequently used types
// Use a typed array for performance when accessing elements
const DEFAULT_PARSER_TYPE = new Array(256);
DEFAULT_PARSER_TYPE[FieldType.TINY] = readIntLengthEncoded;
DEFAULT_PARSER_TYPE[FieldType.SHORT] = readIntLengthEncoded;
DEFAULT_PARSER_TYPE[FieldType.INT] = readIntLengthEncoded;
DEFAULT_PARSER_TYPE[FieldType.INT24] = readIntLengthEncoded;
DEFAULT_PARSER_TYPE[FieldType.YEAR] = readIntLengthEncoded;
DEFAULT_PARSER_TYPE[FieldType.FLOAT] = readFloatLengthCoded;
DEFAULT_PARSER_TYPE[FieldType.DOUBLE] = readFloatLengthCoded;
DEFAULT_PARSER_TYPE[FieldType.DATE] = readDate;
DEFAULT_PARSER_TYPE[FieldType.DATETIME] = readTimestamp;
DEFAULT_PARSER_TYPE[FieldType.TIMESTAMP] = readTimestamp;
DEFAULT_PARSER_TYPE[FieldType.TIME] = readAsciiStringLengthEncoded;
const readBigIntAsNumberLengthCoded = (packet, opts, throwUnexpectedError) => {
const len = packet.readUnsignedLength();
if (len === null) return null;
// Fast path for small integers
if (len < 16) {
const val = packet._atoi(len);
// We know we're here because either bigIntAsNumber or supportBigNumbers is true
if (opts.supportBigNumbers && opts.bigNumberStrings) {
return `${val}`;
}
return val;
}
const val = packet.readBigIntFromLen(len);
if (opts.bigIntAsNumber && opts.checkNumberRange && !Number.isSafeInteger(Number(val))) {
return throwUnexpectedError(
`value ${val} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
const numVal = Number(val);
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(numVal))) {
return val.toString();
}
return numVal;
};
const readDecimalAsIntLengthCoded = (packet, opts, throwUnexpectedError) => {
const valDec = packet.readDecimalLengthEncoded();
if (valDec === null) return null;
// Only perform conversions if needed based on options
if (!(opts.decimalAsNumber || opts.supportBigNumbers)) return valDec;
// Convert once
const numValue = Number(valDec);
// Check number range if required
if (opts.decimalAsNumber && opts.checkNumberRange && !Number.isSafeInteger(numValue)) {
return throwUnexpectedError(
`value ${valDec} can't safely be converted to number`,
false,
null,
'42000',
Errors.ER_PARSING_PRECISION
);
}
// Return string representation for big numbers if needed
if (opts.supportBigNumbers && (opts.bigNumberStrings || !Number.isSafeInteger(numValue))) {
return valDec;
}
return numValue;
};
const readDecimalLengthCoded = (packet, opts, throwUnexpectedError) => {
const valDec = packet.readDecimalLengthEncoded();
if (valDec === null) return null;
// Only perform conversions if needed based on options
if (!(opts.decimalAsNumber || opts.supportBigNumbers)) return valDec;
const numberValue = Number(valDec);
// Handle big numbers specifically
if (
opts.supportBigNumbers &&
(opts.bigNumberStrings || (Number.isInteger(numberValue) && !Number.isSafeInteger(numberValue)))
) {
return valDec;
}
return numberValue;
};
@@ -0,0 +1,284 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
class BinaryEncoder {
/**
* Write (and escape) current parameter value to output writer
*
* @param out output writer
* @param value current parameter
* @param opts connection options
* @param info connection information
*/
static writeParam(out, value, opts, info) {
// GEOJSON are not checked, because change to null/Buffer on parameter validation
switch (typeof value) {
case 'boolean':
out.writeInt8(value ? 0x01 : 0x00);
break;
case 'bigint':
if (value >= 2n ** 63n) {
out.writeLengthEncodedString(value.toString());
} else {
out.writeBigInt(value);
}
break;
case 'number':
// additional verification, to permit query without type,
// like 'SELECT ?' returning same type of value
if (Number.isInteger(value) && value >= -2147483648 && value < 2147483647) {
out.writeInt32(value);
break;
}
out.writeDouble(value);
break;
case 'string':
out.writeLengthEncodedString(value);
break;
case 'object':
if (Object.prototype.toString.call(value) === '[object Date]') {
out.writeBinaryDate(value);
} else if (Buffer.isBuffer(value)) {
out.writeLengthEncodedBuffer(value);
} else if (typeof value.toSqlString === 'function') {
out.writeLengthEncodedString(String(value.toSqlString()));
} else {
out.writeLengthEncodedString(JSON.stringify(value));
}
break;
default:
out.writeLengthEncodedBuffer(value);
}
}
static getBufferFromGeometryValue(value, headerType) {
let geoBuff;
let pos;
let type;
if (!headerType) {
switch (value.type) {
case 'Point':
geoBuff = Buffer.allocUnsafe(21);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(1, 1); //wkbPoint
if (
value.coordinates &&
Array.isArray(value.coordinates) &&
value.coordinates.length >= 2 &&
!isNaN(value.coordinates[0]) &&
!isNaN(value.coordinates[1])
) {
geoBuff.writeDoubleLE(value.coordinates[0], 5); //X
geoBuff.writeDoubleLE(value.coordinates[1], 13); //Y
return geoBuff;
} else {
return null;
}
case 'LineString':
if (value.coordinates && Array.isArray(value.coordinates)) {
const pointNumber = value.coordinates.length;
geoBuff = Buffer.allocUnsafe(9 + 16 * pointNumber);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(2, 1); //wkbLineString
geoBuff.writeInt32LE(pointNumber, 5);
for (let i = 0; i < pointNumber; i++) {
if (
value.coordinates[i] &&
Array.isArray(value.coordinates[i]) &&
value.coordinates[i].length >= 2 &&
!isNaN(value.coordinates[i][0]) &&
!isNaN(value.coordinates[i][1])
) {
geoBuff.writeDoubleLE(value.coordinates[i][0], 9 + 16 * i); //X
geoBuff.writeDoubleLE(value.coordinates[i][1], 17 + 16 * i); //Y
} else {
return null;
}
}
return geoBuff;
} else {
return null;
}
case 'Polygon':
if (value.coordinates && Array.isArray(value.coordinates)) {
const numRings = value.coordinates.length;
let size = 0;
for (let i = 0; i < numRings; i++) {
size += 4 + 16 * value.coordinates[i].length;
}
geoBuff = Buffer.allocUnsafe(9 + size);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(3, 1); //wkbPolygon
geoBuff.writeInt32LE(numRings, 5);
pos = 9;
for (let i = 0; i < numRings; i++) {
const lineString = value.coordinates[i];
if (lineString && Array.isArray(lineString)) {
geoBuff.writeInt32LE(lineString.length, pos);
pos += 4;
for (let j = 0; j < lineString.length; j++) {
if (
lineString[j] &&
Array.isArray(lineString[j]) &&
lineString[j].length >= 2 &&
!isNaN(lineString[j][0]) &&
!isNaN(lineString[j][1])
) {
geoBuff.writeDoubleLE(lineString[j][0], pos); //X
geoBuff.writeDoubleLE(lineString[j][1], pos + 8); //Y
pos += 16;
} else {
return null;
}
}
}
}
return geoBuff;
} else {
return null;
}
case 'MultiPoint':
type = 'MultiPoint';
geoBuff = Buffer.allocUnsafe(9);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(4, 1); //wkbMultiPoint
break;
case 'MultiLineString':
type = 'MultiLineString';
geoBuff = Buffer.allocUnsafe(9);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(5, 1); //wkbMultiLineString
break;
case 'MultiPolygon':
type = 'MultiPolygon';
geoBuff = Buffer.allocUnsafe(9);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(6, 1); //wkbMultiPolygon
break;
case 'GeometryCollection':
geoBuff = Buffer.allocUnsafe(9);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(7, 1); //wkbGeometryCollection
if (value.geometries && Array.isArray(value.geometries)) {
const coordinateLength = value.geometries.length;
const subArrays = [geoBuff];
for (let i = 0; i < coordinateLength; i++) {
const tmpBuf = this.getBufferFromGeometryValue(value.geometries[i]);
if (tmpBuf === null) break;
subArrays.push(tmpBuf);
}
geoBuff.writeInt32LE(subArrays.length - 1, 5);
return Buffer.concat(subArrays);
} else {
geoBuff.writeInt32LE(0, 5);
return geoBuff;
}
default:
return null;
}
if (value.coordinates && Array.isArray(value.coordinates)) {
const coordinateLength = value.coordinates.length;
const subArrays = [geoBuff];
for (let i = 0; i < coordinateLength; i++) {
const tmpBuf = this.getBufferFromGeometryValue(value.coordinates[i], type);
if (tmpBuf === null) break;
subArrays.push(tmpBuf);
}
geoBuff.writeInt32LE(subArrays.length - 1, 5);
return Buffer.concat(subArrays);
} else {
geoBuff.writeInt32LE(0, 5);
return geoBuff;
}
} else {
switch (headerType) {
case 'MultiPoint':
if (value && Array.isArray(value) && value.length >= 2 && !isNaN(value[0]) && !isNaN(value[1])) {
geoBuff = Buffer.allocUnsafe(21);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(1, 1); //wkbPoint
geoBuff.writeDoubleLE(value[0], 5); //X
geoBuff.writeDoubleLE(value[1], 13); //Y
return geoBuff;
}
return null;
case 'MultiLineString':
if (value && Array.isArray(value)) {
const pointNumber = value.length;
geoBuff = Buffer.allocUnsafe(9 + 16 * pointNumber);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(2, 1); //wkbLineString
geoBuff.writeInt32LE(pointNumber, 5);
for (let i = 0; i < pointNumber; i++) {
if (
value[i] &&
Array.isArray(value[i]) &&
value[i].length >= 2 &&
!isNaN(value[i][0]) &&
!isNaN(value[i][1])
) {
geoBuff.writeDoubleLE(value[i][0], 9 + 16 * i); //X
geoBuff.writeDoubleLE(value[i][1], 17 + 16 * i); //Y
} else {
return null;
}
}
return geoBuff;
}
return null;
case 'MultiPolygon':
if (value && Array.isArray(value)) {
const numRings = value.length;
let size = 0;
for (let i = 0; i < numRings; i++) {
size += 4 + 16 * value[i].length;
}
geoBuff = Buffer.allocUnsafe(9 + size);
geoBuff.writeInt8(0x01, 0); //LITTLE ENDIAN
geoBuff.writeInt32LE(3, 1); //wkbPolygon
geoBuff.writeInt32LE(numRings, 5);
pos = 9;
for (let i = 0; i < numRings; i++) {
const lineString = value[i];
if (lineString && Array.isArray(lineString)) {
geoBuff.writeInt32LE(lineString.length, pos);
pos += 4;
for (let j = 0; j < lineString.length; j++) {
if (
lineString[j] &&
Array.isArray(lineString[j]) &&
lineString[j].length >= 2 &&
!isNaN(lineString[j][0]) &&
!isNaN(lineString[j][1])
) {
geoBuff.writeDoubleLE(lineString[j][0], pos); //X
geoBuff.writeDoubleLE(lineString[j][1], pos + 8); //Y
pos += 16;
} else {
return null;
}
}
}
}
return geoBuff;
}
return null;
}
return null;
}
}
}
module.exports = BinaryEncoder;
@@ -0,0 +1,311 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const QUOTE = 0x27;
// Cache common GeoJSON types
const GEO_TYPES = new Set([
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
]);
// Optimized function to pad numbers with leading zeros
const formatDigit = function (val, significantDigit) {
const str = `${val}`;
return str.length < significantDigit ? '0'.repeat(significantDigit - str.length) + str : str;
};
class TextEncoder {
/**
* Write (and escape) current parameter value to output writer
*
* @param out output writer
* @param value current parameter. Expected to be non-null
* @param opts connection options
* @param info connection information
*/
static writeParam(out, value, opts, info) {
switch (typeof value) {
case 'boolean':
out.writeStringAscii(value ? 'true' : 'false');
break;
case 'bigint':
case 'number':
out.writeStringAscii(`${value}`);
break;
case 'string':
out.writeStringEscapeQuote(value);
break;
case 'object':
if (Object.prototype.toString.call(value) === '[object Date]') {
out.writeStringAscii(TextEncoder.getLocalDate(value));
} else if (Buffer.isBuffer(value)) {
out.writeStringAscii("_BINARY '");
out.writeBufferEscape(value);
out.writeInt8(QUOTE);
} else if (typeof value.toSqlString === 'function') {
out.writeStringEscapeQuote(String(value.toSqlString()));
} else if (Array.isArray(value)) {
if (opts.arrayParenthesis) {
out.writeStringAscii('(');
}
for (let i = 0; i < value.length; i++) {
if (i !== 0) out.writeStringAscii(',');
if (value[i] == null) {
out.writeStringAscii('NULL');
} else TextEncoder.writeParam(out, value[i], opts, info);
}
if (opts.arrayParenthesis) {
out.writeStringAscii(')');
}
} else {
if (value.type != null && GEO_TYPES.has(value.type)) {
//GeoJSON format.
const isMariaDb = info.isMariaDB();
const prefix =
(isMariaDb && info.hasMinVersion(10, 1, 4)) || (!isMariaDb && info.hasMinVersion(5, 7, 6)) ? 'ST_' : '';
switch (value.type) {
case 'Point':
out.writeStringAscii(
prefix + "PointFromText('POINT(" + TextEncoder.geoPointToString(value.coordinates) + ")')"
);
break;
case 'LineString':
out.writeStringAscii(
prefix + "LineFromText('LINESTRING(" + TextEncoder.geoArrayPointToString(value.coordinates) + ")')"
);
break;
case 'Polygon':
out.writeStringAscii(
prefix +
"PolygonFromText('POLYGON(" +
TextEncoder.geoMultiArrayPointToString(value.coordinates) +
")')"
);
break;
case 'MultiPoint':
out.writeStringAscii(
prefix +
"MULTIPOINTFROMTEXT('MULTIPOINT(" +
TextEncoder.geoArrayPointToString(value.coordinates) +
")')"
);
break;
case 'MultiLineString':
out.writeStringAscii(
prefix +
"MLineFromText('MULTILINESTRING(" +
TextEncoder.geoMultiArrayPointToString(value.coordinates) +
")')"
);
break;
case 'MultiPolygon':
out.writeStringAscii(
prefix +
"MPolyFromText('MULTIPOLYGON(" +
TextEncoder.geoMultiPolygonToString(value.coordinates) +
")')"
);
break;
case 'GeometryCollection':
out.writeStringAscii(
prefix +
"GeomCollFromText('GEOMETRYCOLLECTION(" +
TextEncoder.geometricCollectionToString(value.geometries) +
")')"
);
break;
}
} else if (String === value.constructor) {
out.writeStringEscapeQuote(value);
break;
} else {
if (opts.permitSetMultiParamEntries) {
let first = true;
for (const key in value) {
const val = value[key];
if (typeof val === 'function') continue;
if (first) {
first = false;
} else {
out.writeStringAscii(',');
}
out.writeString('`' + key + '`');
if (val == null) {
out.writeStringAscii('=NULL');
} else {
out.writeStringAscii('=');
TextEncoder.writeParam(out, val, opts, info);
}
}
if (first) out.writeStringEscapeQuote(JSON.stringify(value));
} else {
out.writeStringEscapeQuote(JSON.stringify(value));
}
}
}
break;
}
}
static geometricCollectionToString(geo) {
if (!geo) return '';
const len = geo.length;
let st = '';
for (let i = 0; i < len; i++) {
const item = geo[i];
//GeoJSON format.
if (i !== 0) st += ',';
switch (item.type) {
case 'Point':
st += `POINT(${TextEncoder.geoPointToString(item.coordinates)})`;
break;
case 'LineString':
st += `LINESTRING(${TextEncoder.geoArrayPointToString(item.coordinates)})`;
break;
case 'Polygon':
st += `POLYGON(${TextEncoder.geoMultiArrayPointToString(item.coordinates)})`;
break;
case 'MultiPoint':
st += `MULTIPOINT(${TextEncoder.geoArrayPointToString(item.coordinates)})`;
break;
case 'MultiLineString':
st += `MULTILINESTRING(${TextEncoder.geoMultiArrayPointToString(item.coordinates)})`;
break;
case 'MultiPolygon':
st += `MULTIPOLYGON(${TextEncoder.geoMultiPolygonToString(item.coordinates)})`;
break;
}
}
return st;
}
static geoMultiPolygonToString(coords) {
if (!coords) return '';
const len = coords.length;
if (len === 0) return '';
let st = '(';
for (let i = 0; i < len; i++) {
if (i !== 0) st += ',(';
st += TextEncoder.geoMultiArrayPointToString(coords[i]) + ')';
}
return st;
}
static geoMultiArrayPointToString(coords) {
if (!coords) return '';
const len = coords.length;
if (len === 0) return '';
let st = '(';
for (let i = 0; i < len; i++) {
if (i !== 0) st += ',(';
st += TextEncoder.geoArrayPointToString(coords[i]) + ')';
}
return st;
}
static geoArrayPointToString(coords) {
if (!coords) return '';
const len = coords.length;
if (len === 0) return '';
let st = '';
for (let i = 0; i < len; i++) {
if (i !== 0) st += ',';
st += TextEncoder.geoPointToString(coords[i]);
}
return st;
}
static geoPointToString(coords) {
if (!coords) return '';
const x = isNaN(coords[0]) ? '' : coords[0];
const y = isNaN(coords[1]) ? '' : coords[1];
return x + ' ' + y;
}
static getLocalDate(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const ms = date.getMilliseconds();
const d = "'" + year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
if (ms === 0) return d + "'";
return d + '.' + (ms < 10 ? '00' : ms < 100 ? '0' : '') + ms + "'";
}
static getFixedFormatDate(date) {
const year = date.getFullYear();
const mon = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const min = date.getMinutes();
const sec = date.getSeconds();
const ms = date.getMilliseconds();
let result =
"'" +
formatDigit(year, 4) +
'-' +
formatDigit(mon, 2) +
'-' +
formatDigit(day, 2) +
' ' +
formatDigit(hour, 2) +
':' +
formatDigit(min, 2) +
':' +
formatDigit(sec, 2);
if (ms > 0) {
result += '.' + formatDigit(ms, 3);
}
return result + "'";
}
}
module.exports = TextEncoder;
+61
View File
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Execute = require('./execute');
const { Readable } = require('stream');
/**
* Protocol COM_STMT_EXECUTE with streaming events.
* see : https://mariadb.com/kb/en/com_stmt_execute/
*/
class ExecuteStream extends Execute {
constructor(cmdParam, connOpts, prepare, socket) {
super(
() => {},
() => {},
connOpts,
cmdParam,
prepare
);
this.socket = socket;
this.inStream = new Readable({
objectMode: true,
read: () => {
this.socket.resume();
}
});
this.on('fields', function (meta) {
this.inStream.emit('fields', meta);
});
this.on('error', function (err) {
this.inStream.emit('error', err);
});
this.on('close', function (err) {
this.inStream.emit('error', err);
});
this.on('end', function (err) {
if (err) this.inStream.emit('error', err);
this.socket.resume();
this.inStream.push(null);
});
this.inStream.close = function () {
this.handleNewRows = () => {};
this.socket.resume();
}.bind(this);
}
handleNewRows(row) {
if (!this.inStream.push(row)) {
this.socket.pause();
}
}
}
module.exports = ExecuteStream;
+338
View File
@@ -0,0 +1,338 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Parser = require('./parser');
const Errors = require('../misc/errors');
const BinaryEncoder = require('./encoder/binary-encoder');
const FieldType = require('../const/field-type');
const Parse = require('../misc/parse');
/**
* Protocol COM_STMT_EXECUTE
* see : https://mariadb.com/kb/en/com_stmt_execute/
*/
class Execute extends Parser {
constructor(resolve, reject, connOpts, cmdParam, prepare) {
super(resolve, reject, connOpts, cmdParam);
this.binary = true;
this.prepare = prepare;
this.canSkipMeta = true;
}
/**
* Send COM_QUERY
*
* @param out output writer
* @param opts connection options
* @param info connection information
*/
start(out, opts, info) {
this.onPacketReceive = this.readResponsePacket;
this.values = [];
if (this.opts.namedPlaceholders) {
if (this.prepare) {
// using named placeholders, so change values accordingly
this.values = new Array(this.prepare.parameterCount);
this.placeHolderIndex = this.prepare._placeHolderIndex;
} else {
const res = Parse.searchPlaceholder(this.sql);
this.placeHolderIndex = res.placeHolderIndex;
this.values = new Array(this.placeHolderIndex.length);
}
if (this.initialValues) {
for (let i = 0; i < this.placeHolderIndex.length; i++) {
this.values[i] = this.initialValues[this.placeHolderIndex[i]];
}
}
} else {
if (this.initialValues)
this.values = Array.isArray(this.initialValues) ? this.initialValues : [this.initialValues];
}
this.parameterCount = this.prepare ? this.prepare.parameterCount : this.values.length;
if (!this.validateParameters(info)) return;
// fill parameter data type
this.parametersType = new Array(this.parameterCount);
let hasLongData = false; // send long data
let val;
for (let i = 0; i < this.parameterCount; i++) {
val = this.values[i];
// special check for GEOJSON that can be null even if object is not
if (
val &&
val.type != null &&
[
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
].includes(val.type)
) {
const geoBuff = BinaryEncoder.getBufferFromGeometryValue(val);
if (geoBuff == null) {
this.values[i] = null;
val = null;
} else {
this.values[i] = Buffer.concat([
Buffer.from([0, 0, 0, 0]), // SRID
geoBuff // WKB
]);
val = this.values[i];
}
}
if (val == null) {
this.parametersType[i] = NULL_PARAM_TYPE;
} else {
switch (typeof val) {
case 'boolean':
this.parametersType[i] = BOOLEAN_TYPE;
break;
case 'bigint':
if (val >= 2n ** 63n) {
this.parametersType[i] = BIG_BIGINT_TYPE;
} else {
this.parametersType[i] = BIGINT_TYPE;
}
break;
case 'number':
// additional verification, to permit query without type,
// like 'SELECT ?' returning same type of value
if (Number.isInteger(val) && val >= -2147483648 && val < 2147483647) {
this.parametersType[i] = INT_TYPE;
break;
}
this.parametersType[i] = DOUBLE_TYPE;
break;
case 'string':
this.parametersType[i] = STRING_TYPE;
break;
case 'object':
if (Object.prototype.toString.call(val) === '[object Date]') {
this.parametersType[i] = DATE_TYPE;
} else if (Buffer.isBuffer(val)) {
if (val.length < 16384 || !this.prepare) {
this.parametersType[i] = BLOB_TYPE;
} else {
this.parametersType[i] = LONGBLOB_TYPE;
hasLongData = true;
}
} else if (typeof val.toSqlString === 'function') {
this.parametersType[i] = STRING_FCT_TYPE;
} else if (typeof val.pipe === 'function' && typeof val.read === 'function') {
hasLongData = true;
this.parametersType[i] = STREAM_TYPE;
} else if (String === val.constructor) {
this.parametersType[i] = STRING_TOSTR_TYPE;
} else {
this.parametersType[i] = STRINGIFY_TYPE;
}
break;
}
}
}
// send long data using COM_STMT_SEND_LONG_DATA
this.longDataStep = false; // send long data
if (hasLongData) {
for (let i = 0; i < this.parameterCount; i++) {
if (this.parametersType[i].isLongData()) {
if (opts.logger.query)
opts.logger.query(
`EXECUTE: (${this.prepare ? this.prepare.id : -1}) sql: ${opts.logParam ? this.displaySql() : this.sql}`
);
if (!this.longDataStep) {
this.longDataStep = true;
this.registerStreamSendEvent(out, info);
this.currentParam = i;
}
this.sendComStmtLongData(out, info, this.values[i]);
return;
}
}
}
if (!this.longDataStep) {
// no stream parameter, so can send directly
if (opts.logger.query)
opts.logger.query(
`EXECUTE: (${this.prepare ? this.prepare.id : -1}) sql: ${opts.logParam ? this.displaySql() : this.sql}`
);
this.sendComStmtExecute(out, info);
}
}
/**
* Validate that parameters exists and are defined.
*
* @param info connection info
* @returns {boolean} return false if any error occur.
*/
validateParameters(info) {
//validate parameter size.
if (this.parameterCount > this.values.length) {
this.sendCancelled(
`Parameter at position ${this.values.length} is not set\\nsql: ${
this.opts.logParam ? this.displaySql() : this.sql
}`,
Errors.ER_MISSING_PARAMETER,
info
);
return false;
}
// validate placeholder
if (this.opts.namedPlaceholders && this.placeHolderIndex) {
for (let i = 0; i < this.parameterCount; i++) {
if (this.values[i] === undefined) {
let errMsg = `Parameter named ${this.placeHolderIndex[i]} is not set`;
if (this.placeHolderIndex.length < this.parameterCount) {
errMsg = `Command expect ${this.parameterCount} parameters, but found only ${this.placeHolderIndex.length} named parameters. You probably use question mark in place of named parameters`;
}
this.sendCancelled(errMsg, Errors.ER_PARAMETER_UNDEFINED, info);
return false;
}
}
}
return true;
}
sendComStmtLongData(out, info, value) {
out.startPacket(this);
out.writeInt8(0x18);
out.writeInt32(this.prepare.id);
out.writeInt16(this.currentParam);
if (Buffer.isBuffer(value)) {
out.writeBuffer(value, 0, value.length);
out.flush();
this.currentParam++;
return this.paramWritten();
}
this.sending = true;
// streaming
value.on('data', function (chunk) {
out.writeBuffer(chunk, 0, chunk.length);
});
value.on(
'end',
function () {
out.flush();
this.currentParam++;
this.paramWritten();
}.bind(this)
);
}
/**
* Send a COM_STMT_EXECUTE
* @param out
* @param info
*/
sendComStmtExecute(out, info) {
let nullCount = ~~((this.parameterCount + 7) / 8);
const nullBitsBuffer = Buffer.alloc(nullCount);
for (let i = 0; i < this.parameterCount; i++) {
if (this.values[i] == null) {
nullBitsBuffer[~~(i / 8)] |= 1 << i % 8;
}
}
out.startPacket(this);
out.writeInt8(0x17); // COM_STMT_EXECUTE
out.writeInt32(this.prepare ? this.prepare.id : -1); // Statement id
out.writeInt8(0); // no cursor flag
out.writeInt32(1); // 1 command
out.writeBuffer(nullBitsBuffer, 0, nullCount); // null buffer
out.writeInt8(1); // always send type to server
// send types
for (let i = 0; i < this.parameterCount; i++) {
out.writeInt8(this.parametersType[i].type);
out.writeInt8(0);
}
//********************************************
// send not null / not streaming values
//********************************************
for (let i = 0; i < this.parameterCount; i++) {
const parameterType = this.parametersType[i];
if (parameterType.encoder) parameterType.encoder(out, this.values[i]);
}
out.flush();
this.sending = false;
this.emit('send_end');
}
/**
* Define params events.
* Each parameter indicate that he is written to socket,
* emitting event so next stream parameter can be written.
*/
registerStreamSendEvent(out, info) {
// note : Implementation use recursive calls, but stack won't get near v8 max call stack size
//since event launched for stream parameter only
this.paramWritten = function () {
if (this.longDataStep) {
for (; this.currentParam < this.parameterCount; this.currentParam++) {
if (this.parametersType[this.currentParam].isLongData()) {
const value = this.values[this.currentParam];
this.sendComStmtLongData(out, info, value);
return;
}
}
this.longDataStep = false; // all streams have been send
}
if (!this.longDataStep) {
this.sendComStmtExecute(out, info);
}
}.bind(this);
}
}
class ParameterType {
constructor(type, encoder, pipe = false, isNull = false) {
this.pipe = pipe;
this.type = type;
this.encoder = encoder;
this.isNull = isNull;
}
isLongData() {
return this.encoder === null && !this.isNull;
}
}
const NULL_PARAM_TYPE = new ParameterType(FieldType.VAR_STRING, null, false, true);
const BOOLEAN_TYPE = new ParameterType(FieldType.TINY, (out, value) => out.writeInt8(value ? 0x01 : 0x00));
const BIG_BIGINT_TYPE = new ParameterType(FieldType.NEWDECIMAL, (out, value) =>
out.writeLengthEncodedString(value.toString())
);
const BIGINT_TYPE = new ParameterType(FieldType.BIGINT, (out, value) => out.writeBigInt(value));
const INT_TYPE = new ParameterType(FieldType.INT, (out, value) => out.writeInt32(value));
const DOUBLE_TYPE = new ParameterType(FieldType.DOUBLE, (out, value) => out.writeDouble(value));
const STRING_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) => out.writeLengthEncodedString(value));
const STRING_TOSTR_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) =>
out.writeLengthEncodedString(value.toString())
);
const DATE_TYPE = new ParameterType(FieldType.DATETIME, (out, value) => out.writeBinaryDate(value));
const BLOB_TYPE = new ParameterType(FieldType.BLOB, (out, value) => out.writeLengthEncodedBuffer(value));
const LONGBLOB_TYPE = new ParameterType(FieldType.BLOB, null);
const STRING_FCT_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) =>
out.writeLengthEncodedString(String(value.toSqlString()))
);
const STREAM_TYPE = new ParameterType(FieldType.BLOB, null, true);
const STRINGIFY_TYPE = new ParameterType(FieldType.VAR_STRING, (out, value) =>
out.writeLengthEncodedString(JSON.stringify(value))
);
module.exports = Execute;
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
const PluginAuth = require('./plugin-auth');
const fs = require('fs');
const Errors = require('../../../misc/errors');
const Sha256PasswordAuth = require('./sha256-password-auth');
const State = {
INIT: 'INIT',
FAST_AUTH_RESULT: 'FAST_AUTH_RESULT',
REQUEST_SERVER_KEY: 'REQUEST_SERVER_KEY',
SEND_AUTH: 'SEND_AUTH'
};
/**
* Use caching Sha2 password authentication
*/
class CachingSha2PasswordAuth extends PluginAuth {
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.multiAuthResolver = multiAuthResolver;
this.pluginData = pluginData;
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
this.counter = 0;
this.state = State.INIT;
}
start(out, opts, info) {
this.exchange(this.pluginData, out, opts, info);
this.onPacketReceive = this.response;
}
exchange(packet, out, opts, info) {
switch (this.state) {
case State.INIT:
const truncatedSeed = this.pluginData.slice(0, this.pluginData.length - 1);
const encPwd = Sha256PasswordAuth.encryptSha256Password(opts.password, truncatedSeed);
out.startPacket(this);
if (encPwd.length > 0) {
out.writeBuffer(encPwd, 0, encPwd.length);
out.flushPacket();
} else {
out.writeEmptyPacket(true);
}
this.state = State.FAST_AUTH_RESULT;
return;
case State.FAST_AUTH_RESULT:
// length encoded numeric : 0x01 0x03/0x04
const fastAuthResult = packet[1];
switch (fastAuthResult) {
case 0x03:
// success authentication
// an OK_Packet will follow
return;
case 0x04:
if (opts.ssl) {
// using SSL, so sending password in clear
out.startPacket(this);
out.writeString(opts.password);
out.writeInt8(0);
out.flushPacket();
return;
}
// retrieve public key from configuration or from server
if (opts.cachingRsaPublicKey) {
try {
let key = opts.cachingRsaPublicKey;
if (!key.includes('-----BEGIN')) {
// rsaPublicKey contain path
key = fs.readFileSync(key, 'utf8');
}
this.publicKey = Sha256PasswordAuth.retrievePublicKey(key);
} catch (err) {
return this.throwError(err, info);
}
// send Sha256Password Packet
Sha256PasswordAuth.sendSha256PwdPacket(this, this.pluginData, this.publicKey, opts.password, out);
} else {
if (!opts.allowPublicKeyRetrieval) {
return this.throwError(
Errors.createFatalError(
'RSA public key is not available client side. Either set option `cachingRsaPublicKey` to indicate' +
' public key path, or allow public key retrieval with option `allowPublicKeyRetrieval`',
Errors.ER_CANNOT_RETRIEVE_RSA_KEY,
info
),
info
);
}
this.state = State.REQUEST_SERVER_KEY;
// ask caching public Key Retrieval
out.startPacket(this);
out.writeInt8(0x02);
out.flushPacket();
}
}
return;
case State.REQUEST_SERVER_KEY:
this.publicKey = Sha256PasswordAuth.retrievePublicKey(packet.toString(undefined, 1));
this.state = State.SEND_AUTH;
Sha256PasswordAuth.sendSha256PwdPacket(this, this.pluginData, this.publicKey, opts.password, out);
return;
}
}
response(packet, out, opts, info) {
const marker = packet.peek();
switch (marker) {
//*********************************************************************************************************
//* OK_Packet and Err_Packet ending packet
//*********************************************************************************************************
case 0x00:
case 0xff:
this.emit('send_end');
return this.multiAuthResolver(packet, out, opts, info);
default:
let promptData = packet.readBufferRemaining();
this.exchange(promptData, out, opts, info);
this.onPacketReceive = this.response;
}
}
}
module.exports = CachingSha2PasswordAuth;
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
const PluginAuth = require('./plugin-auth');
/**
* Send password in clear.
* (used only when SSL is active)
*/
class ClearPasswordAuth extends PluginAuth {
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
this.counter = 0;
this.multiAuthResolver = multiAuthResolver;
}
start(out, opts, info) {
out.startPacket(this);
const pwd = opts.password;
if (pwd) {
if (Array.isArray(pwd)) {
out.writeString(pwd[this.counter++]);
} else {
out.writeString(pwd);
}
}
out.writeInt8(0);
out.flushPacket();
this.onPacketReceive = this.response;
}
response(packet, out, opts, info) {
const marker = packet.peek();
switch (marker) {
//*********************************************************************************************************
//* OK_Packet and Err_Packet ending packet
//*********************************************************************************************************
case 0x00:
case 0xff:
this.emit('send_end');
return this.multiAuthResolver(packet, out, opts, info);
default:
packet.readBuffer(); // prompt
out.startPacket(this);
out.writeString('password');
out.writeInt8(0);
out.flushPacket();
}
}
}
module.exports = ClearPasswordAuth;
@@ -0,0 +1,793 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const PluginAuth = require('./plugin-auth');
const Crypto = require('crypto');
/**
* Standard authentication plugin
*/
class Ed25519PasswordAuth extends PluginAuth {
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.pluginData = pluginData;
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
}
start(out, opts, info) {
//seed is ended with a null byte value.
const data = this.pluginData;
const sign = Ed25519PasswordAuth.encryptPassword(opts.password, data);
out.startPacket(this);
out.writeBuffer(sign, 0, sign.length);
out.flushPacket();
this.emit('send_end');
}
static encryptPassword(password, seed) {
if (!password) return Buffer.alloc(0);
let i, j;
let p = [gf(), gf(), gf(), gf()];
const signedMsg = Buffer.alloc(96);
const bytePwd = Buffer.from(password);
let hash = Crypto.createHash('sha512');
const d = hash.update(bytePwd).digest();
d[0] &= 248;
d[31] &= 127;
d[31] |= 64;
for (i = 0; i < 32; i++) signedMsg[64 + i] = seed[i];
for (i = 0; i < 32; i++) signedMsg[32 + i] = d[32 + i];
hash = Crypto.createHash('sha512');
const r = hash.update(signedMsg.subarray(32, 96)).digest();
reduce(r);
scalarbase(p, r);
pack(signedMsg, p);
p = [gf(), gf(), gf(), gf()];
scalarbase(p, d);
const tt = Buffer.alloc(32);
pack(tt, p);
for (i = 32; i < 64; i++) signedMsg[i] = tt[i - 32];
hash = Crypto.createHash('sha512');
const h = hash.update(signedMsg).digest();
reduce(h);
const x = new Float64Array(64);
for (i = 0; i < 64; i++) x[i] = 0;
for (i = 0; i < 32; i++) x[i] = r[i];
for (i = 0; i < 32; i++) {
for (j = 0; j < 32; j++) {
x[i + j] += h[i] * d[j];
}
}
modL(signedMsg.subarray(32), x);
return signedMsg.subarray(0, 64);
}
permitHash() {
return true;
}
hash(conf) {
let i;
let p = [gf(), gf(), gf(), gf()];
const signedMsg = Buffer.alloc(96);
const bytePwd = Buffer.from(conf.password);
let hash = Crypto.createHash('sha512');
const d = hash.update(bytePwd).digest();
d[0] &= 248;
d[31] &= 127;
d[31] |= 64;
for (i = 0; i < 32; i++) signedMsg[64 + i] = seed[i];
for (i = 0; i < 32; i++) signedMsg[32 + i] = d[32 + i];
hash = Crypto.createHash('sha512');
const r = hash.update(signedMsg.subarray(32, 96)).digest();
reduce(r);
scalarbase(p, r);
return r;
}
}
/*******************************************************
*
* This plugin uses the following public domain tweetnacl-js code by Dmitry Chestnykh
* (from https://github.com/dchest/tweetnacl-js/blob/master/nacl-fast.js).
* tweetnacl cannot be used directly (secret key mandatory size is 32 in nacl + implementation differ :
* second scalarbase use hash of secret key, not secret key).
*
*******************************************************/
const gf = function (init) {
const r = new Float64Array(16);
if (init) for (let i = 0; i < init.length; i++) r[i] = init[i];
return r;
};
const gf0 = gf(),
gf1 = gf([1]),
D2 = gf([
0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df,
0xd9dc, 0x2406
]),
X = gf([
0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e,
0x36d3, 0x2169
]),
Y = gf([
0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666,
0x6666, 0x6666
]);
const L = new Float64Array([
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x10
]);
function reduce(r) {
const x = new Float64Array(64);
let i;
for (i = 0; i < 64; i++) x[i] = r[i];
for (i = 0; i < 64; i++) r[i] = 0;
modL(r, x);
}
function modL(r, x) {
let carry, i, j, k;
for (i = 63; i >= 32; --i) {
carry = 0;
for (j = i - 32, k = i - 12; j < k; ++j) {
x[j] += carry - 16 * x[i] * L[j - (i - 32)];
carry = (x[j] + 128) >> 8;
x[j] -= carry * 256;
}
x[j] += carry;
x[i] = 0;
}
carry = 0;
for (j = 0; j < 32; j++) {
x[j] += carry - (x[31] >> 4) * L[j];
carry = x[j] >> 8;
x[j] &= 255;
}
for (j = 0; j < 32; j++) x[j] -= carry * L[j];
for (i = 0; i < 32; i++) {
x[i + 1] += x[i] >> 8;
r[i] = x[i] & 255;
}
}
function scalarbase(p, s) {
const q = [gf(), gf(), gf(), gf()];
set25519(q[0], X);
set25519(q[1], Y);
set25519(q[2], gf1);
M(q[3], X, Y);
scalarmult(p, q, s);
}
function set25519(r, a) {
for (let i = 0; i < 16; i++) r[i] = a[i] | 0;
}
function M(o, a, b) {
let v,
c,
t0 = 0,
t1 = 0,
t2 = 0,
t3 = 0,
t4 = 0,
t5 = 0,
t6 = 0,
t7 = 0,
t8 = 0,
t9 = 0,
t10 = 0,
t11 = 0,
t12 = 0,
t13 = 0,
t14 = 0,
t15 = 0,
t16 = 0,
t17 = 0,
t18 = 0,
t19 = 0,
t20 = 0,
t21 = 0,
t22 = 0,
t23 = 0,
t24 = 0,
t25 = 0,
t26 = 0,
t27 = 0,
t28 = 0,
t29 = 0,
t30 = 0;
const b0 = b[0],
b1 = b[1],
b2 = b[2],
b3 = b[3],
b4 = b[4],
b5 = b[5],
b6 = b[6],
b7 = b[7],
b8 = b[8],
b9 = b[9],
b10 = b[10],
b11 = b[11],
b12 = b[12],
b13 = b[13],
b14 = b[14],
b15 = b[15];
v = a[0];
t0 += v * b0;
t1 += v * b1;
t2 += v * b2;
t3 += v * b3;
t4 += v * b4;
t5 += v * b5;
t6 += v * b6;
t7 += v * b7;
t8 += v * b8;
t9 += v * b9;
t10 += v * b10;
t11 += v * b11;
t12 += v * b12;
t13 += v * b13;
t14 += v * b14;
t15 += v * b15;
v = a[1];
t1 += v * b0;
t2 += v * b1;
t3 += v * b2;
t4 += v * b3;
t5 += v * b4;
t6 += v * b5;
t7 += v * b6;
t8 += v * b7;
t9 += v * b8;
t10 += v * b9;
t11 += v * b10;
t12 += v * b11;
t13 += v * b12;
t14 += v * b13;
t15 += v * b14;
t16 += v * b15;
v = a[2];
t2 += v * b0;
t3 += v * b1;
t4 += v * b2;
t5 += v * b3;
t6 += v * b4;
t7 += v * b5;
t8 += v * b6;
t9 += v * b7;
t10 += v * b8;
t11 += v * b9;
t12 += v * b10;
t13 += v * b11;
t14 += v * b12;
t15 += v * b13;
t16 += v * b14;
t17 += v * b15;
v = a[3];
t3 += v * b0;
t4 += v * b1;
t5 += v * b2;
t6 += v * b3;
t7 += v * b4;
t8 += v * b5;
t9 += v * b6;
t10 += v * b7;
t11 += v * b8;
t12 += v * b9;
t13 += v * b10;
t14 += v * b11;
t15 += v * b12;
t16 += v * b13;
t17 += v * b14;
t18 += v * b15;
v = a[4];
t4 += v * b0;
t5 += v * b1;
t6 += v * b2;
t7 += v * b3;
t8 += v * b4;
t9 += v * b5;
t10 += v * b6;
t11 += v * b7;
t12 += v * b8;
t13 += v * b9;
t14 += v * b10;
t15 += v * b11;
t16 += v * b12;
t17 += v * b13;
t18 += v * b14;
t19 += v * b15;
v = a[5];
t5 += v * b0;
t6 += v * b1;
t7 += v * b2;
t8 += v * b3;
t9 += v * b4;
t10 += v * b5;
t11 += v * b6;
t12 += v * b7;
t13 += v * b8;
t14 += v * b9;
t15 += v * b10;
t16 += v * b11;
t17 += v * b12;
t18 += v * b13;
t19 += v * b14;
t20 += v * b15;
v = a[6];
t6 += v * b0;
t7 += v * b1;
t8 += v * b2;
t9 += v * b3;
t10 += v * b4;
t11 += v * b5;
t12 += v * b6;
t13 += v * b7;
t14 += v * b8;
t15 += v * b9;
t16 += v * b10;
t17 += v * b11;
t18 += v * b12;
t19 += v * b13;
t20 += v * b14;
t21 += v * b15;
v = a[7];
t7 += v * b0;
t8 += v * b1;
t9 += v * b2;
t10 += v * b3;
t11 += v * b4;
t12 += v * b5;
t13 += v * b6;
t14 += v * b7;
t15 += v * b8;
t16 += v * b9;
t17 += v * b10;
t18 += v * b11;
t19 += v * b12;
t20 += v * b13;
t21 += v * b14;
t22 += v * b15;
v = a[8];
t8 += v * b0;
t9 += v * b1;
t10 += v * b2;
t11 += v * b3;
t12 += v * b4;
t13 += v * b5;
t14 += v * b6;
t15 += v * b7;
t16 += v * b8;
t17 += v * b9;
t18 += v * b10;
t19 += v * b11;
t20 += v * b12;
t21 += v * b13;
t22 += v * b14;
t23 += v * b15;
v = a[9];
t9 += v * b0;
t10 += v * b1;
t11 += v * b2;
t12 += v * b3;
t13 += v * b4;
t14 += v * b5;
t15 += v * b6;
t16 += v * b7;
t17 += v * b8;
t18 += v * b9;
t19 += v * b10;
t20 += v * b11;
t21 += v * b12;
t22 += v * b13;
t23 += v * b14;
t24 += v * b15;
v = a[10];
t10 += v * b0;
t11 += v * b1;
t12 += v * b2;
t13 += v * b3;
t14 += v * b4;
t15 += v * b5;
t16 += v * b6;
t17 += v * b7;
t18 += v * b8;
t19 += v * b9;
t20 += v * b10;
t21 += v * b11;
t22 += v * b12;
t23 += v * b13;
t24 += v * b14;
t25 += v * b15;
v = a[11];
t11 += v * b0;
t12 += v * b1;
t13 += v * b2;
t14 += v * b3;
t15 += v * b4;
t16 += v * b5;
t17 += v * b6;
t18 += v * b7;
t19 += v * b8;
t20 += v * b9;
t21 += v * b10;
t22 += v * b11;
t23 += v * b12;
t24 += v * b13;
t25 += v * b14;
t26 += v * b15;
v = a[12];
t12 += v * b0;
t13 += v * b1;
t14 += v * b2;
t15 += v * b3;
t16 += v * b4;
t17 += v * b5;
t18 += v * b6;
t19 += v * b7;
t20 += v * b8;
t21 += v * b9;
t22 += v * b10;
t23 += v * b11;
t24 += v * b12;
t25 += v * b13;
t26 += v * b14;
t27 += v * b15;
v = a[13];
t13 += v * b0;
t14 += v * b1;
t15 += v * b2;
t16 += v * b3;
t17 += v * b4;
t18 += v * b5;
t19 += v * b6;
t20 += v * b7;
t21 += v * b8;
t22 += v * b9;
t23 += v * b10;
t24 += v * b11;
t25 += v * b12;
t26 += v * b13;
t27 += v * b14;
t28 += v * b15;
v = a[14];
t14 += v * b0;
t15 += v * b1;
t16 += v * b2;
t17 += v * b3;
t18 += v * b4;
t19 += v * b5;
t20 += v * b6;
t21 += v * b7;
t22 += v * b8;
t23 += v * b9;
t24 += v * b10;
t25 += v * b11;
t26 += v * b12;
t27 += v * b13;
t28 += v * b14;
t29 += v * b15;
v = a[15];
t15 += v * b0;
t16 += v * b1;
t17 += v * b2;
t18 += v * b3;
t19 += v * b4;
t20 += v * b5;
t21 += v * b6;
t22 += v * b7;
t23 += v * b8;
t24 += v * b9;
t25 += v * b10;
t26 += v * b11;
t27 += v * b12;
t28 += v * b13;
t29 += v * b14;
t30 += v * b15;
t0 += 38 * t16;
t1 += 38 * t17;
t2 += 38 * t18;
t3 += 38 * t19;
t4 += 38 * t20;
t5 += 38 * t21;
t6 += 38 * t22;
t7 += 38 * t23;
t8 += 38 * t24;
t9 += 38 * t25;
t10 += 38 * t26;
t11 += 38 * t27;
t12 += 38 * t28;
t13 += 38 * t29;
t14 += 38 * t30;
// t15 left as is
// first car
c = 1;
v = t0 + c + 65535;
c = Math.floor(v / 65536);
t0 = v - c * 65536;
v = t1 + c + 65535;
c = Math.floor(v / 65536);
t1 = v - c * 65536;
v = t2 + c + 65535;
c = Math.floor(v / 65536);
t2 = v - c * 65536;
v = t3 + c + 65535;
c = Math.floor(v / 65536);
t3 = v - c * 65536;
v = t4 + c + 65535;
c = Math.floor(v / 65536);
t4 = v - c * 65536;
v = t5 + c + 65535;
c = Math.floor(v / 65536);
t5 = v - c * 65536;
v = t6 + c + 65535;
c = Math.floor(v / 65536);
t6 = v - c * 65536;
v = t7 + c + 65535;
c = Math.floor(v / 65536);
t7 = v - c * 65536;
v = t8 + c + 65535;
c = Math.floor(v / 65536);
t8 = v - c * 65536;
v = t9 + c + 65535;
c = Math.floor(v / 65536);
t9 = v - c * 65536;
v = t10 + c + 65535;
c = Math.floor(v / 65536);
t10 = v - c * 65536;
v = t11 + c + 65535;
c = Math.floor(v / 65536);
t11 = v - c * 65536;
v = t12 + c + 65535;
c = Math.floor(v / 65536);
t12 = v - c * 65536;
v = t13 + c + 65535;
c = Math.floor(v / 65536);
t13 = v - c * 65536;
v = t14 + c + 65535;
c = Math.floor(v / 65536);
t14 = v - c * 65536;
v = t15 + c + 65535;
c = Math.floor(v / 65536);
t15 = v - c * 65536;
t0 += c - 1 + 37 * (c - 1);
// second car
c = 1;
v = t0 + c + 65535;
c = Math.floor(v / 65536);
t0 = v - c * 65536;
v = t1 + c + 65535;
c = Math.floor(v / 65536);
t1 = v - c * 65536;
v = t2 + c + 65535;
c = Math.floor(v / 65536);
t2 = v - c * 65536;
v = t3 + c + 65535;
c = Math.floor(v / 65536);
t3 = v - c * 65536;
v = t4 + c + 65535;
c = Math.floor(v / 65536);
t4 = v - c * 65536;
v = t5 + c + 65535;
c = Math.floor(v / 65536);
t5 = v - c * 65536;
v = t6 + c + 65535;
c = Math.floor(v / 65536);
t6 = v - c * 65536;
v = t7 + c + 65535;
c = Math.floor(v / 65536);
t7 = v - c * 65536;
v = t8 + c + 65535;
c = Math.floor(v / 65536);
t8 = v - c * 65536;
v = t9 + c + 65535;
c = Math.floor(v / 65536);
t9 = v - c * 65536;
v = t10 + c + 65535;
c = Math.floor(v / 65536);
t10 = v - c * 65536;
v = t11 + c + 65535;
c = Math.floor(v / 65536);
t11 = v - c * 65536;
v = t12 + c + 65535;
c = Math.floor(v / 65536);
t12 = v - c * 65536;
v = t13 + c + 65535;
c = Math.floor(v / 65536);
t13 = v - c * 65536;
v = t14 + c + 65535;
c = Math.floor(v / 65536);
t14 = v - c * 65536;
v = t15 + c + 65535;
c = Math.floor(v / 65536);
t15 = v - c * 65536;
t0 += c - 1 + 37 * (c - 1);
o[0] = t0;
o[1] = t1;
o[2] = t2;
o[3] = t3;
o[4] = t4;
o[5] = t5;
o[6] = t6;
o[7] = t7;
o[8] = t8;
o[9] = t9;
o[10] = t10;
o[11] = t11;
o[12] = t12;
o[13] = t13;
o[14] = t14;
o[15] = t15;
}
function scalarmult(p, q, s) {
let b, i;
set25519(p[0], gf0);
set25519(p[1], gf1);
set25519(p[2], gf1);
set25519(p[3], gf0);
for (i = 255; i >= 0; --i) {
b = (s[(i / 8) | 0] >> (i & 7)) & 1;
cswap(p, q, b);
add(q, p);
add(p, p);
cswap(p, q, b);
}
}
function pack(r, p) {
const tx = gf(),
ty = gf(),
zi = gf();
inv25519(zi, p[2]);
M(tx, p[0], zi);
M(ty, p[1], zi);
pack25519(r, ty);
r[31] ^= par25519(tx) << 7;
}
function inv25519(o, i) {
const c = gf();
let a;
for (a = 0; a < 16; a++) c[a] = i[a];
for (a = 253; a >= 0; a--) {
S(c, c);
if (a !== 2 && a !== 4) M(c, c, i);
}
for (a = 0; a < 16; a++) o[a] = c[a];
}
function S(o, a) {
M(o, a, a);
}
function par25519(a) {
const d = new Uint8Array(32);
pack25519(d, a);
return d[0] & 1;
}
function car25519(o) {
let i,
v,
c = 1;
for (i = 0; i < 16; i++) {
v = o[i] + c + 65535;
c = Math.floor(v / 65536);
o[i] = v - c * 65536;
}
o[0] += c - 1 + 37 * (c - 1);
}
function pack25519(o, n) {
let i, j, b;
const m = gf(),
t = gf();
for (i = 0; i < 16; i++) t[i] = n[i];
car25519(t);
car25519(t);
car25519(t);
for (j = 0; j < 2; j++) {
m[0] = t[0] - 0xffed;
for (i = 1; i < 15; i++) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
sel25519(t, m, 1 - b);
}
for (i = 0; i < 16; i++) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
function cswap(p, q, b) {
for (let i = 0; i < 4; i++) {
sel25519(p[i], q[i], b);
}
}
function A(o, a, b) {
for (let i = 0; i < 16; i++) o[i] = a[i] + b[i];
}
function Z(o, a, b) {
for (let i = 0; i < 16; i++) o[i] = a[i] - b[i];
}
function add(p, q) {
const a = gf(),
b = gf(),
c = gf(),
d = gf(),
e = gf(),
f = gf(),
g = gf(),
h = gf(),
t = gf();
Z(a, p[1], p[0]);
Z(t, q[1], q[0]);
M(a, a, t);
A(b, p[0], p[1]);
A(t, q[0], q[1]);
M(b, b, t);
M(c, p[3], q[3]);
M(c, c, D2);
M(d, p[2], q[2]);
A(d, d, d);
Z(e, b, a);
Z(f, d, c);
A(g, d, c);
A(h, b, a);
M(p[0], e, f);
M(p[1], h, g);
M(p[2], g, f);
M(p[3], e, h);
}
function sel25519(p, q, b) {
const c = ~(b - 1);
let t;
for (let i = 0; i < 16; i++) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
module.exports = Ed25519PasswordAuth;
@@ -0,0 +1,198 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
const PluginAuth = require('./plugin-auth');
const InitialHandshake = require('./initial-handshake');
const ClientCapabilities = require('../client-capabilities');
const Capabilities = require('../../../const/capabilities');
const SslRequest = require('../ssl-request');
const Errors = require('../../../misc/errors');
const NativePasswordAuth = require('./native-password-auth');
const os = require('os');
const Iconv = require('iconv-lite');
const Crypto = require('crypto');
const driverVersion = require('../../../../package.json').version;
/**
* Handshake response
*/
class Handshake extends PluginAuth {
constructor(auth, getSocket, multiAuthResolver, reject) {
super(null, multiAuthResolver, reject);
this.sequenceNo = 0;
this.compressSequenceNo = 0;
this.auth = auth;
this.getSocket = getSocket;
this.counter = 0;
this.onPacketReceive = this.parseHandshakeInit;
}
start(out, opts, info) {}
parseHandshakeInit(packet, out, opts, info) {
if (packet.peek() === 0xff) {
//in case that some host is not permit to connect server
const authErr = packet.readError(info);
authErr.fatal = true;
return this.throwError(authErr, info);
}
let handshake = new InitialHandshake(packet, info);
ClientCapabilities.init(opts, info);
this.pluginName = handshake.pluginName;
if (opts.ssl) {
if (info.serverCapabilities & Capabilities.SSL) {
info.clientCapabilities |= Capabilities.SSL;
SslRequest.send(this, out, info, opts);
this.auth._createSecureContext(info, () => {
// mark self-signed error only if was not explicitly forced
const secureSocket = this.getSocket();
info.selfSignedCertificate = !secureSocket.authorized;
info.tlsAuthorizationError = secureSocket.authorizationError;
const serverCert = secureSocket.getPeerCertificate(false);
info.tlsCert = serverCert;
info.tlsFingerprint = serverCert ? serverCert.fingerprint256.replace(/:/gi, '').toLowerCase() : null;
Handshake.send.call(this, this, out, opts, handshake.pluginName, info);
});
} else {
return this.throwNewError(
'Trying to connect with ssl, but ssl not enabled in the server',
true,
info,
'08S01',
Errors.ER_SERVER_SSL_DISABLED
);
}
} else {
Handshake.send(this, out, opts, handshake.pluginName, info);
}
this.onPacketReceive = this.auth.handshakeResult.bind(this.auth);
}
permitHash() {
return this.pluginName !== 'mysql_clear_password';
}
hash(conf) {
// mysql_native_password hash
let hash = Crypto.createHash('sha1');
let stage1 = hash.update(conf.password, 'utf8').digest();
hash = Crypto.createHash('sha1');
return hash.update(stage1).digest();
}
/**
* Send Handshake response packet
* see https://mariadb.com/kb/en/library/1-connecting-connecting/#handshake-response-packet
*
* @param cmd current handshake command
* @param out output writer
* @param opts connection options
* @param pluginName plugin name
* @param info connection information
*/
static send(cmd, out, opts, pluginName, info) {
out.startPacket(cmd);
info.defaultPluginName = pluginName;
const pwd = Array.isArray(opts.password) ? opts.password[0] : opts.password;
let authToken;
let authPlugin;
switch (pluginName) {
case 'mysql_clear_password':
authToken = Buffer.from(pwd);
authPlugin = 'mysql_clear_password';
break;
default:
authToken = NativePasswordAuth.encryptSha1Password(pwd, info.seed);
authPlugin = 'mysql_native_password';
break;
}
out.writeInt32(Number(info.clientCapabilities & BigInt(0xffffffff)));
out.writeInt32(1024 * 1024 * 1024); // max packet size
// if collation and id < 255, set it directly
// is not, additional command SET NAMES xx [COLLATE yy] will be issued
out.writeInt8(opts.collation && opts.collation.index <= 255 ? opts.collation.index : 224);
for (let i = 0; i < 19; i++) {
out.writeInt8(0);
}
out.writeInt32(Number(info.clientCapabilities >> 32n));
//null encoded user
out.writeString(opts.user || '');
out.writeInt8(0);
if (info.serverCapabilities & Capabilities.PLUGIN_AUTH_LENENC_CLIENT_DATA) {
out.writeLengthCoded(authToken.length);
out.writeBuffer(authToken, 0, authToken.length);
} else if (info.serverCapabilities & Capabilities.SECURE_CONNECTION) {
out.writeInt8(authToken.length);
out.writeBuffer(authToken, 0, authToken.length);
} else {
out.writeBuffer(authToken, 0, authToken.length);
out.writeInt8(0);
}
if (info.clientCapabilities & Capabilities.CONNECT_WITH_DB) {
out.writeString(opts.database);
out.writeInt8(0);
info.database = opts.database;
}
if (info.clientCapabilities & Capabilities.PLUGIN_AUTH) {
out.writeString(authPlugin);
out.writeInt8(0);
}
if (info.clientCapabilities & Capabilities.CONNECT_ATTRS) {
out.writeInt8(0xfc);
let initPos = out.pos; //save position, assuming connection attributes length will be less than 2 bytes length
out.writeInt16(0);
const encoding = info.collation ? info.collation.charset : 'utf8';
Handshake.writeAttribute(out, '_client_name', encoding);
Handshake.writeAttribute(out, 'MariaDB connector/Node', encoding);
Handshake.writeAttribute(out, '_client_version', encoding);
Handshake.writeAttribute(out, driverVersion, encoding);
const address = cmd.getSocket().address().address;
if (address) {
Handshake.writeAttribute(out, '_server_host', encoding);
Handshake.writeAttribute(out, address, encoding);
}
Handshake.writeAttribute(out, '_os', encoding);
Handshake.writeAttribute(out, process.platform, encoding);
Handshake.writeAttribute(out, '_client_host', encoding);
Handshake.writeAttribute(out, os.hostname(), encoding);
Handshake.writeAttribute(out, '_node_version', encoding);
Handshake.writeAttribute(out, process.versions.node, encoding);
if (opts.connectAttributes !== true) {
let attrNames = Object.keys(opts.connectAttributes);
for (let k = 0; k < attrNames.length; ++k) {
Handshake.writeAttribute(out, attrNames[k], encoding);
Handshake.writeAttribute(out, opts.connectAttributes[attrNames[k]], encoding);
}
}
//write end size
out.writeInt16AtPos(initPos);
}
out.flushPacket();
}
static writeAttribute(out, val, encoding) {
let param = Buffer.isEncoding(encoding) ? Buffer.from(val, encoding) : Iconv.encode(val, encoding);
out.writeLengthCoded(param.length);
out.writeBuffer(param, 0, param.length);
}
}
module.exports = Handshake;
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Capabilities = require('../../../const/capabilities');
const Collations = require('../../../const/collations');
const ConnectionInformation = require('../../../misc/connection-information');
/**
* Parser server initial handshake.
* see https://mariadb.com/kb/en/library/1-connecting-connecting/#initial-handshake-packet
*/
class InitialHandshake {
constructor(packet, info) {
//protocolVersion
packet.skip(1);
info.serverVersion = {};
info.serverVersion.raw = packet.readStringNullEnded();
info.threadId = packet.readUInt32();
let seed1 = packet.readBuffer(8);
packet.skip(1); //reserved byte
let serverCapabilities = BigInt(packet.readUInt16());
info.collation = Collations.fromIndex(packet.readUInt8());
info.status = packet.readUInt16();
serverCapabilities += BigInt(packet.readUInt16()) << 16n;
let saltLength = 0;
if (serverCapabilities & Capabilities.PLUGIN_AUTH) {
saltLength = Math.max(12, packet.readUInt8() - 9);
} else {
packet.skip(1);
}
if (serverCapabilities & Capabilities.MYSQL) {
packet.skip(10);
} else {
packet.skip(6);
serverCapabilities += BigInt(packet.readUInt32()) << 32n;
}
if (serverCapabilities & Capabilities.SECURE_CONNECTION) {
let seed2 = packet.readBuffer(saltLength);
info.seed = Buffer.concat([seed1, seed2]);
} else {
info.seed = seed1;
}
packet.skip(1);
info.serverCapabilities = serverCapabilities;
/**
* check for MariaDB 10.x replication hack , remove fake prefix if needed
* MDEV-4088: in 10.0+, the real version string maybe prefixed with "5.5.5-",
* to workaround bugs in Oracle MySQL replication
**/
if (info.serverVersion.raw.startsWith('5.5.5-')) {
info.serverVersion.mariaDb = true;
info.serverVersion.raw = info.serverVersion.raw.substring('5.5.5-'.length);
} else {
//Support for MDEV-7780 faking server version
info.serverVersion.mariaDb =
info.serverVersion.raw.includes('MariaDB') || (serverCapabilities & Capabilities.MYSQL) === 0n;
}
if (serverCapabilities & Capabilities.PLUGIN_AUTH) {
this.pluginName = packet.readStringNullEnded();
} else {
this.pluginName = '';
}
ConnectionInformation.parseVersionString(info);
}
}
module.exports = InitialHandshake;
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const PluginAuth = require('./plugin-auth');
const Crypto = require('crypto');
/**
* Standard authentication plugin
*/
class NativePasswordAuth extends PluginAuth {
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.pluginData = pluginData;
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
}
start(out, opts, info) {
//seed is ended with a null byte value.
const data = this.pluginData.slice(0, 20);
let authToken = NativePasswordAuth.encryptSha1Password(opts.password, data);
out.startPacket(this);
if (authToken.length > 0) {
out.writeBuffer(authToken, 0, authToken.length);
out.flushPacket();
} else {
out.writeEmptyPacket(true);
}
this.emit('send_end');
}
static encryptSha1Password(password, seed) {
if (!password) return Buffer.alloc(0);
let hash = Crypto.createHash('sha1');
let stage1 = hash.update(password, 'utf8').digest();
hash = Crypto.createHash('sha1');
let stage2 = hash.update(stage1).digest();
hash = Crypto.createHash('sha1');
hash.update(seed);
hash.update(stage2);
let digest = hash.digest();
let returnBytes = Buffer.allocUnsafe(digest.length);
for (let i = 0; i < digest.length; i++) {
returnBytes[i] = stage1[i] ^ digest[i];
}
return returnBytes;
}
permitHash() {
return true;
}
hash(conf) {
let hash = Crypto.createHash('sha1');
let stage1 = hash.update(conf.password, 'utf8').digest();
hash = Crypto.createHash('sha1');
return hash.update(stage1).digest();
}
}
module.exports = NativePasswordAuth;
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
const PluginAuth = require('./plugin-auth');
/**
* Use PAM authentication
*/
class PamPasswordAuth extends PluginAuth {
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.pluginData = pluginData;
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
this.counter = 0;
this.multiAuthResolver = multiAuthResolver;
}
start(out, opts, info) {
this.exchange(this.pluginData, out, opts, info);
this.onPacketReceive = this.response;
}
exchange(buffer, out, opts, info) {
//conversation is :
// - first byte is information tell if question is a password (4) or clear text (2).
// - other bytes are the question to user
out.startPacket(this);
let pwd;
if (Array.isArray(opts.password)) {
pwd = opts.password[this.counter];
this.counter++;
} else {
pwd = opts.password;
}
if (pwd) out.writeString(pwd);
out.writeInt8(0);
out.flushPacket();
}
response(packet, out, opts, info) {
const marker = packet.peek();
switch (marker) {
//*********************************************************************************************************
//* OK_Packet and Err_Packet ending packet
//*********************************************************************************************************
case 0x00:
case 0xff:
this.emit('send_end');
return this.multiAuthResolver(packet, out, opts, info);
default:
let promptData = packet.readBuffer();
this.exchange(promptData, out, opts, info);
this.onPacketReceive = this.response;
}
}
}
module.exports = PamPasswordAuth;
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const PluginAuth = require('./plugin-auth');
const crypto = require('crypto');
const Errors = require('../../../misc/errors');
const pkcs8Ed25519header = Buffer.from([
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20
]);
/**
* Standard authentication plugin
*/
class ParsecAuth extends PluginAuth {
#hash;
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.multiAuthResolver = multiAuthResolver;
this.pluginData = pluginData;
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
}
start(out, opts, info) {
if (!info.extSalt) {
out.startPacket(this);
out.writeEmptyPacket(true); // indicate need salt
this.onPacketReceive = this.requestForSalt;
} else {
this.parseExtSalt(Buffer.from(info.extSalt, 'hex'), info);
this.sendScramble(out, opts, info);
}
}
requestForSalt(packet, out, opts, info) {
this.parseExtSalt(packet.readBufferRemaining(), info);
this.sendScramble(out, opts, info);
}
parseExtSalt(extSalt, info) {
if (extSalt.length < 2 || extSalt[0] !== 0x50 || extSalt[1] > 3) {
// expected 'P' for KDF algorithm (PBKDF2) and maximum iteration of 8192
return this.throwError(
Errors.createFatalError('Wrong parsec authentication format', Errors.ER_AUTHENTICATION_BAD_PACKET, info),
info
);
}
this.iterations = extSalt[1];
this.salt = extSalt.slice(2);
// disable for now until https://jira.mariadb.org/browse/MDEV-34846
// info.extSalt = extSalt.toString('hex');
}
sendScramble(out, opts, info) {
const derivedKey = crypto.pbkdf2Sync(opts.password || '', this.salt, 1024 << this.iterations, 32, 'sha512');
const privateKey = toPkcs8der(derivedKey);
const rawPublicKey = this.getEd25519PublicKeyFromPrivateKey(derivedKey);
this.#hash = Buffer.concat([Buffer.from([0x50, this.iterations]), this.salt, rawPublicKey]);
const client_scramble = crypto.randomBytes(32);
const message = Buffer.concat([this.pluginData, client_scramble]);
const signature = crypto.sign(null, message, privateKey);
out.startPacket(this);
out.writeBuffer(client_scramble, 0, 32);
out.writeBuffer(signature, 0, 64);
out.flushPacket();
this.emit('send_end');
this.onPacketReceive = this.multiAuthResolver;
}
getEd25519PublicKeyFromPrivateKey(privateKeyBuffer) {
// Create a KeyObject from the raw private key
const privateKey = crypto.createPrivateKey({
key: Buffer.concat([pkcs8Ed25519header, privateKeyBuffer]),
format: 'der',
type: 'pkcs8',
name: 'ed25519'
});
// Get the corresponding public key
const publicKey = crypto.createPublicKey(privateKey);
// Export the public key in raw format
return publicKey
.export({
type: 'spki',
format: 'der'
})
.subarray(-32); // The last 32 bytes contain the raw key
}
permitHash() {
return true;
}
hash(conf) {
return this.#hash;
}
}
const toPkcs8der = (rawB64) => {
// prefix for a private Ed25519
const prefixPrivateEd25519 = Buffer.from('302e020100300506032b657004220420', 'hex');
const der = Buffer.concat([prefixPrivateEd25519, rawB64]);
return crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
};
module.exports = ParsecAuth;
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Command = require('../../command');
/**
* Base authentication plugin
*/
class PluginAuth extends Command {
constructor(cmdParam, multiAuthResolver, reject) {
super(cmdParam, multiAuthResolver, reject);
this.onPacketReceive = multiAuthResolver;
}
permitHash() {
return true;
}
hash(conf) {
return null;
}
}
module.exports = PluginAuth;
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
const PluginAuth = require('./plugin-auth');
const fs = require('fs');
const crypto = require('crypto');
const Errors = require('../../../misc/errors');
const Crypto = require('crypto');
/**
* Use Sha256 authentication
*/
class Sha256PasswordAuth extends PluginAuth {
constructor(packSeq, compressPackSeq, pluginData, cmdParam, reject, multiAuthResolver) {
super(cmdParam, multiAuthResolver, reject);
this.pluginData = pluginData;
this.sequenceNo = packSeq;
this.compressSequenceNo = compressPackSeq;
this.counter = 0;
this.counter = 0;
this.initialState = true;
this.multiAuthResolver = multiAuthResolver;
}
start(out, opts, info) {
this.exchange(this.pluginData, out, opts, info);
this.onPacketReceive = this.response;
}
exchange(buffer, out, opts, info) {
if (this.initialState) {
if (!opts.password) {
out.startPacket(this);
out.writeEmptyPacket(true);
return;
} else if (opts.ssl) {
// using SSL, so sending password in clear
out.startPacket(this);
if (opts.password) {
out.writeString(opts.password);
}
out.writeInt8(0);
out.flushPacket();
return;
} else {
// retrieve public key from configuration or from server
if (opts.rsaPublicKey) {
try {
let key = opts.rsaPublicKey;
if (!key.includes('-----BEGIN')) {
// rsaPublicKey contain path
key = fs.readFileSync(key, 'utf8');
}
this.publicKey = Sha256PasswordAuth.retrievePublicKey(key);
} catch (err) {
return this.throwError(err, info);
}
} else {
if (!opts.allowPublicKeyRetrieval) {
return this.throwError(
Errors.createFatalError(
'RSA public key is not available client side. Either set option `rsaPublicKey` to indicate' +
' public key path, or allow public key retrieval with option `allowPublicKeyRetrieval`',
Errors.ER_CANNOT_RETRIEVE_RSA_KEY,
info
),
info
);
}
this.initialState = false;
// ask public Key Retrieval
out.startPacket(this);
out.writeInt8(0x01);
out.flushPacket();
return;
}
}
// send Sha256Password Packet
Sha256PasswordAuth.sendSha256PwdPacket(this, this.pluginData, this.publicKey, opts.password, out);
} else {
// has request public key
this.publicKey = Sha256PasswordAuth.retrievePublicKey(buffer.toString('utf8', 1));
Sha256PasswordAuth.sendSha256PwdPacket(this, this.pluginData, this.publicKey, opts.password, out);
}
}
static retrievePublicKey(key) {
return key.replace('(-+BEGIN PUBLIC KEY-+\\r?\\n|\\n?-+END PUBLIC KEY-+\\r?\\n?)', '');
}
static sendSha256PwdPacket(cmd, pluginData, publicKey, password, out) {
const truncatedSeed = pluginData.slice(0, pluginData.length - 1);
out.startPacket(cmd);
const enc = Sha256PasswordAuth.encrypt(truncatedSeed, password, publicKey);
out.writeBuffer(enc, 0, enc.length);
out.flushPacket();
}
static encryptSha256Password(password, seed) {
if (!password) return Buffer.alloc(0);
let hash = Crypto.createHash('sha256');
let stage1 = hash.update(password, 'utf8').digest();
hash = Crypto.createHash('sha256');
let stage2 = hash.update(stage1).digest();
hash = Crypto.createHash('sha256');
// order is different from sha 1 !!!!!
hash.update(stage2);
hash.update(seed);
let digest = hash.digest();
let returnBytes = Buffer.allocUnsafe(digest.length);
for (let i = 0; i < digest.length; i++) {
returnBytes[i] = stage1[i] ^ digest[i];
}
return returnBytes;
}
// encrypt password with public key
static encrypt(seed, password, publicKey) {
const nullFinishedPwd = Buffer.from(password + '\0');
const xorBytes = Buffer.allocUnsafe(nullFinishedPwd.length);
const seedLength = seed.length;
for (let i = 0; i < xorBytes.length; i++) {
xorBytes[i] = nullFinishedPwd[i] ^ seed[i % seedLength];
}
return crypto.publicEncrypt({ key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, xorBytes);
}
response(packet, out, opts, info) {
const marker = packet.peek();
switch (marker) {
//*********************************************************************************************************
//* OK_Packet and Err_Packet ending packet
//*********************************************************************************************************
case 0x00:
case 0xff:
this.emit('send_end');
return this.multiAuthResolver(packet, out, opts, info);
default:
let promptData = packet.readBufferRemaining();
this.exchange(promptData, out, opts, info);
this.onPacketReceive = this.response;
}
}
}
module.exports = Sha256PasswordAuth;
@@ -0,0 +1,335 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Command = require('../command');
const Errors = require('../../misc/errors');
const Capabilities = require('../../const/capabilities');
const Handshake = require('./auth/handshake');
const ServerStatus = require('../../const/server-status');
const StateChange = require('../../const/state-change');
const Collations = require('../../const/collations');
const Crypto = require('crypto');
const utils = require('../../misc/utils');
const tls = require('tls');
const authenticationPlugins = {
mysql_native_password: require('./auth/native-password-auth.js'),
mysql_clear_password: require('./auth/clear-password-auth'),
client_ed25519: require('./auth/ed25519-password-auth'),
parsec: require('./auth/parsec-auth'),
dialog: require('./auth/pam-password-auth'),
sha256_password: require('./auth/sha256-password-auth'),
caching_sha2_password: require('./auth/caching-sha2-password-auth')
};
/**
* Handle handshake.
* see https://mariadb.com/kb/en/library/1-connecting-connecting/
*/
class Authentication extends Command {
constructor(cmdParam, resolve, reject, _createSecureContext, getSocket) {
super(cmdParam, resolve, reject);
this.cmdParam = cmdParam;
this._createSecureContext = _createSecureContext;
this.getSocket = getSocket;
this.plugin = new Handshake(this, getSocket, this.handshakeResult, reject);
}
onPacketReceive(packet, out, opts, info) {
this.plugin.sequenceNo = this.sequenceNo;
this.plugin.compressSequenceNo = this.compressSequenceNo;
this.plugin.onPacketReceive(packet, out, opts, info);
}
/**
* Fast-path handshake results :
* - if plugin was the one expected by server, server will send OK_Packet / ERR_Packet.
* - if not, server send an AuthSwitchRequest packet, indicating the specific PLUGIN to use with this user.
* dispatching to plugin handler then.
*
* @param packet current packet
* @param out output buffer
* @param opts options
* @param info connection info
* @returns {*} return null if authentication succeed, depending on plugin conversation if not finished
*/
handshakeResult(packet, out, opts, info) {
const marker = packet.peek();
switch (marker) {
//*********************************************************************************************************
//* AuthSwitchRequest packet
//*********************************************************************************************************
case 0xfe:
this.dispatchAuthSwitchRequest(packet, out, opts, info);
return;
//*********************************************************************************************************
//* OK_Packet - authentication succeeded
//*********************************************************************************************************
case 0x00:
this.plugin.onPacketReceive = null;
packet.skip(1); //skip header
packet.skipLengthCodedNumber(); //skip affected rows
packet.skipLengthCodedNumber(); //skip last insert id
info.status = packet.readUInt16();
if (info.requireValidCert) {
if (info.selfSignedCertificate) {
// TLS was forced to trust, and certificate validation is required
packet.skip(2); //skip warning count
if (packet.remaining()) {
const validationHash = packet.readBufferLengthEncoded();
if (validationHash.length > 0) {
if (!this.plugin.permitHash() || !Boolean(this.cmdParam.opts.password)) {
return this.throwNewError(
'Self signed certificates. Either set `ssl: { rejectUnauthorized: false }` (trust mode) or provide server certificate to client',
true,
info,
'08000',
Errors.ER_SELF_SIGNED_NO_PWD
);
}
if (this.validateFingerPrint(validationHash, info)) {
return this.successEnd();
}
}
}
return this.throwNewError('self-signed certificate', true, info, '08000', Errors.ER_SELF_SIGNED);
} else {
// certificate is not self signed, validate server identity
const validationFunction =
opts.ssl === true || opts.ssl.checkServerIdentity === null
? tls.checkServerIdentity
: opts.ssl.checkServerIdentity;
const identityError = validationFunction(
typeof opts.ssl === 'object' && opts.ssl.servername ? opts.ssl.servername : opts.host,
info.tlsCert
);
if (identityError) {
return this.throwNewError(
'certificate identify Error: ' + identityError.message,
true,
info,
'08000',
Errors.ER_TLS_IDENTITY_ERROR
);
}
}
}
let mustRedirect = false;
if (info.status & ServerStatus.SESSION_STATE_CHANGED) {
packet.skip(2); //skip warning count
packet.skipLengthCodedNumber();
while (packet.remaining()) {
const len = packet.readUnsignedLength();
if (len > 0) {
const subPacket = packet.subPacketLengthEncoded(len);
while (subPacket.remaining()) {
const type = subPacket.readUInt8();
switch (type) {
case StateChange.SESSION_TRACK_SYSTEM_VARIABLES:
let subSubPacket;
do {
subSubPacket = subPacket.subPacketLengthEncoded(subPacket.readUnsignedLength());
const variable = subSubPacket.readStringLengthEncoded();
const value = subSubPacket.readStringLengthEncoded();
switch (variable) {
case 'character_set_client':
info.collation = Collations.fromCharset(value);
if (info.collation === undefined) {
this.throwError(new Error("unknown charset : '" + value + "'"), info);
return;
}
opts.emit('collation', info.collation);
break;
case 'redirect_url':
if (value !== '') {
mustRedirect = true;
info.redirect(value, this.successEnd.bind(this));
}
break;
case 'maxscale':
info.maxscaleVersion = value;
break;
case 'connection_id':
info.threadId = parseInt(value);
break;
default:
//variable not used by driver
}
} while (subSubPacket.remaining() > 0);
break;
case StateChange.SESSION_TRACK_SCHEMA:
const subSubPacket2 = subPacket.subPacketLengthEncoded(subPacket.readUnsignedLength());
info.database = subSubPacket2.readStringLengthEncoded();
break;
}
}
}
}
}
if (!mustRedirect) this.successEnd();
return;
//*********************************************************************************************************
//* ERR_Packet
//*********************************************************************************************************
case 0xff:
this.plugin.onPacketReceive = null;
const authErr = packet.readError(info, this.displaySql(), undefined);
authErr.fatal = true;
if (info.requireValidCert && info.selfSignedCertificate) {
// TLS was forced to trust, and certificate validation is required
return this.plugin.throwNewError(
'Self signed certificates. Either set `ssl: { rejectUnauthorized: false }` (trust mode) or provide server certificate to client',
true,
info,
'08000',
Errors.ER_SELF_SIGNED_NO_PWD
);
}
return this.plugin.throwError(authErr, info);
//*********************************************************************************************************
//* unexpected
//*********************************************************************************************************
default:
this.throwNewError(
`Unexpected type of packet during handshake phase : ${marker}`,
true,
info,
'42000',
Errors.ER_AUTHENTICATION_BAD_PACKET
);
}
}
validateFingerPrint(validationHash, info) {
if (validationHash.length === 0 || !info.tlsFingerprint) return false;
// 0x01 = SHA256 encryption
if (validationHash[0] !== 0x01) {
const err = Errors.createFatalError(
`Unexpected hash format for fingerprint hash encoding`,
Errors.ER_UNEXPECTED_PACKET,
this.info
);
if (this.opts.logger.error) this.opts.logger.error(err);
return false;
}
const pwdHash = this.plugin.hash(this.cmdParam.opts);
let hash = Crypto.createHash('sha256');
let digest = hash.update(pwdHash).update(info.seed).update(Buffer.from(info.tlsFingerprint, 'hex')).digest();
const hashHex = utils.toHexString(digest);
const serverValidationHex = validationHash.toString('ascii', 1, validationHash.length).toLowerCase();
return hashHex === serverValidationHex;
}
/**
* Handle authentication switch request : dispatch to plugin handler.
*
* @param packet packet
* @param out output writer
* @param opts options
* @param info connection information
*/
dispatchAuthSwitchRequest(packet, out, opts, info) {
let pluginName, pluginData;
if (info.clientCapabilities & Capabilities.PLUGIN_AUTH) {
packet.skip(1); //header
if (packet.remaining()) {
//AuthSwitchRequest packet.
pluginName = packet.readStringNullEnded();
pluginData = packet.readBufferRemaining();
} else {
//OldAuthSwitchRequest
pluginName = 'mysql_old_password';
pluginData = info.seed.subarray(0, 8);
}
} else {
pluginName = packet.readStringNullEnded('ascii');
pluginData = packet.readBufferRemaining();
}
if (
info.requireValidCert &&
info.selfSignedCertificate &&
Boolean(this.cmdParam.opts.password) &&
!this.plugin.permitHash()
) {
return this.throwNewError(
`Unsupported authentication plugin ${pluginName} with Self signed certificates. Either set 'ssl: { rejectUnauthorized: false }' (trust mode) or provide server certificate to client`,
true,
info,
'08000',
Errors.ER_SELF_SIGNED_BAD_PLUGIN
);
}
if (opts.restrictedAuth && !opts.restrictedAuth.includes(pluginName)) {
this.throwNewError(
`Unsupported authentication plugin ${pluginName}. Authorized plugin: ${opts.restrictedAuth.toString()}`,
true,
info,
'42000',
Errors.ER_NOT_SUPPORTED_AUTH_PLUGIN
);
return;
}
try {
this.plugin.emit('end');
this.plugin.onPacketReceive = null;
this.plugin = Authentication.pluginHandler(
pluginName,
this.plugin.sequenceNo,
this.plugin.compressSequenceNo,
pluginData,
info,
opts,
out,
this.cmdParam,
this.reject,
this.handshakeResult.bind(this)
);
this.plugin.start(out, opts, info);
} catch (err) {
this.reject(err);
}
}
static pluginHandler(
pluginName,
packSeq,
compressPackSeq,
pluginData,
info,
opts,
out,
cmdParam,
authReject,
multiAuthResolver
) {
let pluginAuth = authenticationPlugins[pluginName];
if (!pluginAuth) {
throw Errors.createFatalError(
`Client does not support authentication protocol '${pluginName}' requested by server.`,
Errors.ER_AUTHENTICATION_PLUGIN_NOT_SUPPORTED,
info,
'08004'
);
}
return new pluginAuth(packSeq, compressPackSeq, pluginData, cmdParam, authReject, multiAuthResolver);
}
}
module.exports = Authentication;
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
// noinspection JSBitwiseOperatorUsage
'use strict';
const Capabilities = require('../../const/capabilities');
/**
* Initialize client capabilities according to options and server capabilities
*
* @param opts options
* @param info information
*/
module.exports.init = function (opts, info) {
let capabilities =
Capabilities.IGNORE_SPACE |
Capabilities.PROTOCOL_41 |
Capabilities.TRANSACTIONS |
Capabilities.SECURE_CONNECTION |
Capabilities.MULTI_RESULTS |
Capabilities.PS_MULTI_RESULTS |
Capabilities.SESSION_TRACK |
Capabilities.CONNECT_ATTRS |
Capabilities.PLUGIN_AUTH_LENENC_CLIENT_DATA |
Capabilities.MARIADB_CLIENT_EXTENDED_METADATA |
Capabilities.PLUGIN_AUTH;
if (opts.foundRows) {
capabilities |= Capabilities.FOUND_ROWS;
}
if (opts.permitLocalInfile) {
capabilities |= Capabilities.LOCAL_FILES;
}
if (opts.multipleStatements) {
capabilities |= Capabilities.MULTI_STATEMENTS;
}
info.eofDeprecated = !opts.keepEof && (info.serverCapabilities & Capabilities.DEPRECATE_EOF) > 0;
if (info.eofDeprecated) {
capabilities |= Capabilities.DEPRECATE_EOF;
}
if (opts.database && info.serverCapabilities & Capabilities.CONNECT_WITH_DB) {
capabilities |= Capabilities.CONNECT_WITH_DB;
}
info.serverPermitSkipMeta = (info.serverCapabilities & Capabilities.MARIADB_CLIENT_CACHE_METADATA) > 0;
if (info.serverPermitSkipMeta) {
capabilities |= Capabilities.MARIADB_CLIENT_CACHE_METADATA;
}
// use compression only if requested by client and supported by server
if (opts.compress) {
if (info.serverCapabilities & Capabilities.COMPRESS) {
capabilities |= Capabilities.COMPRESS;
} else {
opts.compress = false;
}
}
if (opts.bulk && info.serverCapabilities & Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS) {
capabilities |= Capabilities.MARIADB_CLIENT_STMT_BULK_OPERATIONS;
capabilities |= Capabilities.BULK_UNIT_RESULTS;
}
if (opts.permitConnectionWhenExpired) {
capabilities |= Capabilities.CAN_HANDLE_EXPIRED_PASSWORDS;
}
info.clientCapabilities = capabilities & info.serverCapabilities;
};
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Capabilities = require('../../const/capabilities');
/**
* Send SSL Request packet.
* see : https://mariadb.com/kb/en/library/1-connecting-connecting/#sslrequest-packet
*
* @param cmd current command
* @param out output writer
* @param info client information
* @param opts connection options
*/
module.exports.send = function sendSSLRequest(cmd, out, info, opts) {
out.startPacket(cmd);
out.writeInt32(Number(info.clientCapabilities & BigInt(0xffffffff)));
out.writeInt32(1024 * 1024 * 1024); // max packet size
out.writeInt8(opts.collation && opts.collation.index <= 255 ? opts.collation.index : 224);
for (let i = 0; i < 19; i++) {
out.writeInt8(0);
}
if (info.serverCapabilities & Capabilities.MYSQL) {
out.writeInt32(0);
} else {
out.writeInt32(Number(info.clientCapabilities >> 32n));
}
out.flushPacket();
};
+861
View File
@@ -0,0 +1,861 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Command = require('./command');
const ServerStatus = require('../const/server-status');
const ColumnDefinition = require('./column-definition');
const Errors = require('../misc/errors');
const fs = require('fs');
const Parse = require('../misc/parse');
const BinaryDecoder = require('./decoder/binary-decoder');
const TextDecoder = require('./decoder/text-decoder');
const OkPacket = require('./class/ok-packet');
const StateChange = require('../const/state-change');
const Collations = require('../const/collations');
// Set of field names that are reserved for internal use
const privateFields = new Set([
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__',
'__proto__'
]);
/**
* Handle COM_QUERY / COM_STMT_EXECUTE results
* @see https://mariadb.com/kb/en/library/4-server-response-packets/
*/
class Parser extends Command {
/**
* Create a new Parser instance
*
* @param {Function} resolve - Promise resolve function
* @param {Function} reject - Promise reject function
* @param {Object} connOpts - Connection options
* @param {Object} cmdParam - Command parameters
*/
constructor(resolve, reject, connOpts, cmdParam) {
super(cmdParam, resolve, reject);
this._responseIndex = 0;
this._rows = [];
this.opts = cmdParam.opts ? Object.assign({}, connOpts, cmdParam.opts) : connOpts;
this.sql = cmdParam.sql;
this.initialValues = cmdParam.values;
this.canSkipMeta = false;
}
/**
* Read Query response packet.
* Packet can be:
* - a result-set
* - an ERR_Packet
* - an OK_Packet
* - LOCAL_INFILE Packet
*
* @param {Object} packet - Query response packet
* @param {Object} out - Output writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection info
* @returns {Function|null} Next packet handler or null
*/
readResponsePacket(packet, out, opts, info) {
switch (packet.peek()) {
case 0x00: // OK response
return this.readOKPacket(packet, out, opts, info);
case 0xff: // ERROR response
return this.handleErrorPacket(packet, info);
case 0xfb: // LOCAL INFILE response
return this.readLocalInfile(packet, out, opts, info);
default: // Result set
return this.readResultSet(packet, info);
}
}
/**
* Handle error packet
*
* @param {Object} packet - Error packet
* @param {Object} info - Connection info
* @returns {null} Always returns null
* @private
*/
handleErrorPacket(packet, info) {
// In case of timeout, free accumulated rows
this._columns = null;
const err = packet.readError(info, this.opts.logParam ? this.displaySql() : this.sql, this.cmdParam.stack);
// Force in transaction status, since query will have created a transaction if autocommit is off
// Goal is to avoid unnecessary COMMIT/ROLLBACK
info.status |= ServerStatus.STATUS_IN_TRANS;
return this.throwError(err, info);
}
/**
* Read result-set packets
* @see https://mariadb.com/kb/en/library/resultset/
*
* @param {Object} packet - Column count packet
* @param {Object} info - Connection information
* @returns {Function} Next packet handler
*/
readResultSet(packet, info) {
this._columnCount = packet.readUnsignedLength();
this._rows.push([]);
if (this.canSkipMeta && info.serverPermitSkipMeta && packet.readUInt8() === 0) {
// Command supports skipping meta
// Server permits it
// And tells that no columns follow, using prepare results
return this.handleSkippedMeta(info);
}
this._columns = [];
return (this.onPacketReceive = this.readColumn);
}
/**
* Handle skipped metadata case
*
* @param {Object} info - Connection information
* @returns {Function} Next packet handler
* @private
*/
handleSkippedMeta(info) {
this._columns = this.prepare.columns;
this._columnCount = this._columns.length;
this.emit('fields', this._columns);
this.setParser();
return (this.onPacketReceive = info.eofDeprecated ? this.readResultSetRow : this.readIntermediateEOF);
}
/**
* Read OK_Packet
* @see https://mariadb.com/kb/en/library/ok_packet/
*
* @param {Object} packet - OK_Packet
* @param {Object} out - Output writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
* @returns {Function|null} Next packet handler or null
*/
readOKPacket(packet, out, opts, info) {
packet.skip(1); // Skip header
const affectedRows = packet.readUnsignedLength();
// Handle insertId based on options
let insertId = this.processInsertId(packet.readInsertId(), info);
info.status = packet.readUInt16();
const okPacket = new OkPacket(affectedRows, insertId, packet.readUInt16());
let mustRedirect = false;
// Process session state changes if present
if (info.status & ServerStatus.SESSION_STATE_CHANGED) {
mustRedirect = this.processSessionStateChanges(packet, info, opts);
}
// Handle streaming case
if (this.inStream) {
this.handleNewRows(okPacket);
}
// Handle redirection
if (mustRedirect) {
return null; // Redirection is handled asynchronously
}
if (
info.redirectRequest &&
(info.status & ServerStatus.STATUS_IN_TRANS) === 0 &&
(info.status & ServerStatus.MORE_RESULTS_EXISTS) === 0
) {
info.redirect(info.redirectRequest, this.okPacketSuccess.bind(this, okPacket, info));
} else {
this.okPacketSuccess(okPacket, info);
}
return null;
}
/**
* Process insertId based on connection options
*
* @param {BigInt} insertId - Raw insertId from packet
* @param {Object} info - Connection info
* @returns {BigInt|Number|String} Processed insertId
* @private
*/
processInsertId(insertId, info) {
if (this.opts.supportBigNumbers || this.opts.insertIdAsNumber) {
if (this.opts.insertIdAsNumber && this.opts.checkNumberRange && !Number.isSafeInteger(Number(insertId))) {
this.onPacketReceive = info.status & ServerStatus.MORE_RESULTS_EXISTS ? this.readResponsePacket : null;
this.throwUnexpectedError(
`last insert id value ${insertId} can't safely be converted to number`,
false,
info,
'42000',
Errors.ER_PARSING_PRECISION
);
return insertId;
}
if (this.opts.supportBigNumbers && (this.opts.bigNumberStrings || !Number.isSafeInteger(Number(insertId)))) {
return insertId.toString();
} else {
return Number(insertId);
}
}
return insertId;
}
/**
* Process session state changes
*
* @param {Object} packet - Packet containing session state changes
* @param {Object} info - Connection information
* @param {Object} opts - Connection options
* @returns {Boolean} True if redirection is needed
* @private
*/
processSessionStateChanges(packet, info, opts) {
let mustRedirect = false;
packet.skipLengthCodedNumber();
while (packet.remaining()) {
const len = packet.readUnsignedLength();
if (len > 0) {
const subPacket = packet.subPacketLengthEncoded(len);
while (subPacket.remaining()) {
const type = subPacket.readUInt8();
switch (type) {
case StateChange.SESSION_TRACK_SYSTEM_VARIABLES:
mustRedirect = this.processSystemVariables(subPacket, info, opts) || mustRedirect;
break;
case StateChange.SESSION_TRACK_SCHEMA:
info.database = this.readSchemaChange(subPacket);
break;
}
}
}
}
return mustRedirect;
}
/**
* Process system variables changes
*
* @param {Object} subPacket - Packet containing system variables
* @param {Object} info - Connection information
* @param {Object} opts - Connection options
* @returns {Boolean} True if redirection is needed
* @private
*/
processSystemVariables(subPacket, info, opts) {
let mustRedirect = false;
let subSubPacket;
do {
subSubPacket = subPacket.subPacketLengthEncoded(subPacket.readUnsignedLength());
const variable = subSubPacket.readStringLengthEncoded();
const value = subSubPacket.readStringLengthEncoded();
switch (variable) {
case 'character_set_client':
info.collation = Collations.fromCharset(value);
if (info.collation === undefined) {
this.throwError(new Error(`unknown charset: '${value}'`), info);
return false;
}
opts.emit('collation', info.collation);
break;
case 'redirect_url':
if (value !== '') {
mustRedirect = true;
info.redirect(value, this.okPacketSuccess.bind(this, this.okPacket, info));
}
break;
case 'connection_id':
info.threadId = parseInt(value);
break;
}
} while (subSubPacket.remaining() > 0);
return mustRedirect;
}
/**
* Read schema change from packet
*
* @param {Object} subPacket - Packet containing schema change
* @returns {String} New schema name
* @private
*/
readSchemaChange(subPacket) {
const subSubPacket = subPacket.subPacketLengthEncoded(subPacket.readUnsignedLength());
return subSubPacket.readStringLengthEncoded();
}
/**
* Handle OK packet success
*
* @param {Object} okPacket - OK packet
* @param {Object} info - Connection information
*/
okPacketSuccess(okPacket, info) {
if (this._responseIndex === 0) {
// Fast path for standard single result
if (info.status & ServerStatus.MORE_RESULTS_EXISTS) {
this._rows.push(okPacket);
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
return this.success(this.opts.metaAsArray ? [okPacket, []] : okPacket);
}
this._rows.push(okPacket);
if (info.status & ServerStatus.MORE_RESULTS_EXISTS) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
if (this.opts.metaAsArray) {
if (!this._meta) {
this._meta = new Array(this._responseIndex);
}
this._meta[this._responseIndex] = null;
this.success([this._rows, this._meta]);
} else {
this.success(this._rows);
}
}
/**
* Complete query with success
*
* @param {*} val - Result value
*/
success(val) {
this.successEnd(val);
this._columns = null;
this._rows = [];
}
/**
* Read column information metadata
* @see https://mariadb.com/kb/en/library/resultset/#column-definition-packet
*
* @param {Object} packet - Column definition packet
* @param {Object} out - Output writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
*/
readColumn(packet, out, opts, info) {
this._columns.push(new ColumnDefinition(packet, info, this.opts.rowsAsArray));
// Last column
if (this._columns.length === this._columnCount) {
this.setParser();
if (this.canSkipMeta && info.serverPermitSkipMeta && this.prepare != null) {
// Server can skip meta, but have force sending it.
// Metadata have changed, updating prepare result accordingly
if (this._responseIndex === 0) this.prepare.columns = this._columns;
}
this.emit('fields', this._columns);
this.onPacketReceive = info.eofDeprecated ? this.readResultSetRow : this.readIntermediateEOF;
}
}
/**
* Set up row parsers based on column information
*/
setParser() {
this._parseFunction = new Array(this._columnCount);
if (this.opts.typeCast) {
for (let i = 0; i < this._columnCount; i++) {
this._parseFunction[i] = this.readCastValue.bind(this, this._columns[i]);
}
} else {
const dataParser = this.binary ? BinaryDecoder.parser : TextDecoder.parser;
for (let i = 0; i < this._columnCount; i++) {
this._parseFunction[i] = dataParser(this._columns[i], this.opts);
}
}
if (this.opts.rowsAsArray) {
this.parseRow = this.parseRowAsArray;
} else {
this.tableHeader = new Array(this._columnCount);
this.parseRow = this.binary ? this.parseRowStdBinary : this.parseRowStdText;
if (this.opts.nestTables) {
this.configureNestedTables();
} else {
for (let i = 0; i < this._columnCount; i++) {
this.tableHeader[i] = this._columns[i].name();
}
this.checkDuplicates();
}
}
}
/**
* Configure nested tables format
* @private
*/
configureNestedTables() {
if (typeof this.opts.nestTables === 'string') {
for (let i = 0; i < this._columnCount; i++) {
this.tableHeader[i] = this._columns[i].table() + this.opts.nestTables + this._columns[i].name();
}
this.checkDuplicates();
} else if (this.opts.nestTables === true) {
this.parseRow = this.parseRowNested;
for (let i = 0; i < this._columnCount; i++) {
this.tableHeader[i] = [this._columns[i].table(), this._columns[i].name()];
}
this.checkNestTablesDuplicatesAndPrivateFields();
}
}
/**
* Check for duplicate column names
*/
checkDuplicates() {
if (this.opts.checkDuplicate) {
for (let i = 0; i < this._columnCount; i++) {
if (this.tableHeader.indexOf(this.tableHeader[i], i + 1) > 0) {
const dupes = this.tableHeader.reduce(
(acc, v, i, arr) => (arr.indexOf(v) !== i && acc.indexOf(v) === -1 ? acc.concat(v) : acc),
[]
);
this.throwUnexpectedError(
`Error in results, duplicate field name \`${dupes[0]}\`.\n(see option \`checkDuplicate\`)`,
false,
null,
'42000',
Errors.ER_DUPLICATE_FIELD
);
}
}
}
}
/**
* Check for duplicates and private fields in nested tables
*/
checkNestTablesDuplicatesAndPrivateFields() {
if (this.opts.checkDuplicate) {
for (let i = 0; i < this._columnCount; i++) {
for (let j = 0; j < i; j++) {
if (this.tableHeader[j][0] === this.tableHeader[i][0] && this.tableHeader[j][1] === this.tableHeader[i][1]) {
this.throwUnexpectedError(
`Error in results, duplicate field name \`${this.tableHeader[i][0]}\`.\`${this.tableHeader[i][1]}\`\n(see option \`checkDuplicate\`)`,
false,
null,
'42000',
Errors.ER_DUPLICATE_FIELD
);
}
}
}
}
for (let i = 0; i < this._columnCount; i++) {
if (privateFields.has(this.tableHeader[i][0])) {
this.throwUnexpectedError(
`Use of \`${this.tableHeader[i][0]}\` is not permitted with option \`nestTables\``,
false,
null,
'42000',
Errors.ER_PRIVATE_FIELDS_USE
);
// Continue parsing results to keep connection state
// but without assigning possible dangerous value
this.parseRow = () => {
return {};
};
}
}
}
/**
* Read intermediate EOF
* Only for server before MariaDB 10.2 / MySQL 5.7 that doesn't have CLIENT_DEPRECATE_EOF capability
* @see https://mariadb.com/kb/en/library/eof_packet/
*
* @param {Object} packet - EOF Packet
* @param {Object} out - Output writer
* @param {Object} opts - Connection options
* @param {Object} info - Connection information
* @returns {Function|null} Next packet handler or null
*/
readIntermediateEOF(packet, out, opts, info) {
if (packet.peek() !== 0xfe) {
return this.throwNewError('Error in protocol, expected EOF packet', true, info, '42000', Errors.ER_EOF_EXPECTED);
}
// Before MySQL 5.7.5, last EOF doesn't contain the good flag SERVER_MORE_RESULTS_EXISTS
// for OUT parameters. It must be checked here
// (5.7.5 does have the CLIENT_DEPRECATE_EOF capability, so this packet is not even sent)
packet.skip(3);
info.status = packet.readUInt16();
this.isOutParameter = info.status & ServerStatus.PS_OUT_PARAMS;
return (this.onPacketReceive = this.readResultSetRow);
}
/**
* Add new rows to the result set
*
* @param {Object} row - Row data
*/
handleNewRows(row) {
this._rows[this._responseIndex].push(row);
}
/**
* Check if packet is result-set end = EOF of OK_Packet with EOF header according to CLIENT_DEPRECATE_EOF capability
* or a result-set row
*
* @param packet current packet
* @param out output writer
* @param opts connection options
* @param info connection information
* @returns {*}
*/
readResultSetRow(packet, out, opts, info) {
if (packet.peek() >= 0xfe) {
if (packet.peek() === 0xff) {
//force in transaction status, since query will have created a transaction if autocommit is off
//goal is to avoid unnecessary COMMIT/ROLLBACK.
info.status |= ServerStatus.STATUS_IN_TRANS;
return this.throwError(
packet.readError(info, this.opts.logParam ? this.displaySql() : this.sql, this.cmdParam.err),
info
);
}
if ((!info.eofDeprecated && packet.length() < 13) || (info.eofDeprecated && packet.length() < 0xffffff)) {
if (!info.eofDeprecated) {
packet.skip(3);
info.status = packet.readUInt16();
} else {
packet.skip(1); //skip header
packet.skipLengthCodedNumber(); //skip update count
packet.skipLengthCodedNumber(); //skip insert id
info.status = packet.readUInt16();
}
if (
info.redirectRequest &&
(info.status & ServerStatus.STATUS_IN_TRANS) === 0 &&
(info.status & ServerStatus.MORE_RESULTS_EXISTS) === 0
) {
info.redirect(info.redirectRequest, this.resultSetEndingPacketResult.bind(this, info));
} else {
this.resultSetEndingPacketResult(info);
}
return;
}
}
this.handleNewRows(this.parseRow(packet));
}
resultSetEndingPacketResult(info) {
if (this.opts.metaAsArray) {
//return promise object as array :
// example for SELECT 1 =>
// [
// [ {"1": 1} ], //rows
// [ColumnDefinition] //meta
// ]
if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) {
if (!this._meta) this._meta = [];
this._meta[this._responseIndex] = this._columns;
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
if (this._responseIndex === 0) {
this.success([this._rows[0], this._columns]);
} else {
if (!this._meta) this._meta = [];
this._meta[this._responseIndex] = this._columns;
this.success([this._rows, this._meta]);
}
} else {
//return promise object as rows that have meta property :
// example for SELECT 1 =>
// [
// {"1": 1},
// meta: [ColumnDefinition]
// ]
Object.defineProperty(this._rows[this._responseIndex], 'meta', {
value: this._columns,
writable: true,
enumerable: this.opts.metaEnumerable
});
if (info.status & ServerStatus.MORE_RESULTS_EXISTS || this.isOutParameter) {
this._responseIndex++;
return (this.onPacketReceive = this.readResponsePacket);
}
this.success(this._responseIndex === 0 ? this._rows[0] : this._rows);
}
}
/**
* Display current SQL with parameters (truncated if too big)
*
* @returns {string}
*/
displaySql() {
if (this.opts && this.initialValues) {
if (this.sql.length > this.opts.debugLen) {
return this.sql.substring(0, this.opts.debugLen) + '...';
}
let sqlMsg = this.sql + ' - parameters:';
return Parser.logParameters(this.opts, sqlMsg, this.initialValues);
}
if (this.sql.length > this.opts.debugLen) {
return this.sql.substring(0, this.opts.debugLen) + '... - parameters:[]';
}
return this.sql + ' - parameters:[]';
}
static logParameters(opts, sqlMsg, values) {
if (opts.namedPlaceholders) {
sqlMsg += '{';
let first = true;
for (let key in values) {
if (first) {
first = false;
} else {
sqlMsg += ',';
}
sqlMsg += "'" + key + "':";
let param = values[key];
sqlMsg = Parser.logParam(sqlMsg, param);
if (sqlMsg.length > opts.debugLen) {
return sqlMsg.substring(0, opts.debugLen) + '...';
}
}
sqlMsg += '}';
} else {
sqlMsg += '[';
if (Array.isArray(values)) {
for (let i = 0; i < values.length; i++) {
if (i !== 0) sqlMsg += ',';
let param = values[i];
sqlMsg = Parser.logParam(sqlMsg, param);
if (sqlMsg.length > opts.debugLen) {
return sqlMsg.substring(0, opts.debugLen) + '...';
}
}
} else {
sqlMsg = Parser.logParam(sqlMsg, values);
if (sqlMsg.length > opts.debugLen) {
return sqlMsg.substring(0, opts.debugLen) + '...';
}
}
sqlMsg += ']';
}
return sqlMsg;
}
parseRowAsArray(packet) {
const row = new Array(this._columnCount);
const nullBitMap = this.binary ? BinaryDecoder.newRow(packet, this._columns) : null;
for (let i = 0; i < this._columnCount; i++) {
row[i] = this._parseFunction[i](packet, this.opts, this.unexpectedError, nullBitMap, i);
}
return row;
}
parseRowNested(packet) {
const row = {};
const nullBitMap = this.binary ? BinaryDecoder.newRow(packet, this._columns) : null;
for (let i = 0; i < this._columnCount; i++) {
if (!row[this.tableHeader[i][0]]) row[this.tableHeader[i][0]] = {};
row[this.tableHeader[i][0]][this.tableHeader[i][1]] = this._parseFunction[i](
packet,
this.opts,
this.unexpectedError,
nullBitMap,
i
);
}
return row;
}
parseRowStdText(packet) {
const row = {};
for (let i = 0; i < this._columnCount; i++) {
row[this.tableHeader[i]] = this._parseFunction[i](packet, this.opts, this.unexpectedError);
}
return row;
}
parseRowStdBinary(packet) {
const nullBitMap = BinaryDecoder.newRow(packet, this._columns);
const row = {};
for (let i = 0; i < this._columnCount; i++) {
row[this.tableHeader[i]] = this._parseFunction[i](packet, this.opts, this.unexpectedError, nullBitMap, i);
}
return row;
}
readCastValue(column, packet, opts, unexpectedError, nullBitmap, index) {
if (this.binary) {
BinaryDecoder.castWrapper(column, packet, opts, nullBitmap, index);
} else {
TextDecoder.castWrapper(column, packet, opts, nullBitmap, index);
}
const dataParser = this.binary ? BinaryDecoder.parser : TextDecoder.parser;
return opts.typeCast(column, dataParser(column, opts).bind(null, packet, opts, unexpectedError, nullBitmap, index));
}
readLocalInfile(packet, out, opts, info) {
packet.skip(1); //skip header
out.startPacket(this);
const fileName = packet.readStringRemaining();
if (!Parse.validateFileName(this.sql, this.initialValues, fileName)) {
out.writeEmptyPacket();
const error = Errors.createError(
"LOCAL INFILE wrong filename. '" +
fileName +
"' doesn't correspond to query " +
this.sql +
'. Query cancelled. Check for malicious server / proxy',
Errors.ER_LOCAL_INFILE_WRONG_FILENAME,
info,
'HY000',
this.sql
);
process.nextTick(this.reject, error);
this.reject = null;
this.resolve = null;
return (this.onPacketReceive = this.readResponsePacket);
}
// this.sequenceNo = 2;
// this.compressSequenceNo = 2;
let stream;
try {
stream = this.opts.infileStreamFactory ? this.opts.infileStreamFactory(fileName) : fs.createReadStream(fileName);
} catch (e) {
out.writeEmptyPacket();
const error = Errors.createError(
`LOCAL INFILE infileStreamFactory failed`,
Errors.ER_LOCAL_INFILE_NOT_READABLE,
info,
'22000',
this.opts.logParam ? this.displaySql() : this.sql
);
error.cause = e;
process.nextTick(this.reject, error);
this.reject = null;
this.resolve = null;
return (this.onPacketReceive = this.readResponsePacket);
}
stream.on(
'error',
function (err) {
out.writeEmptyPacket();
const error = Errors.createError(
`LOCAL INFILE command failed: ${err.message}`,
Errors.ER_LOCAL_INFILE_NOT_READABLE,
info,
'22000',
this.sql
);
process.nextTick(this.reject, error);
this.reject = null;
this.resolve = null;
}.bind(this)
);
stream.on('data', (chunk) => {
out.writeBuffer(chunk, 0, chunk.length);
});
stream.on('end', () => {
if (!out.isEmpty()) {
out.flushBuffer(false);
}
out.writeEmptyPacket();
});
this.onPacketReceive = this.readResponsePacket;
}
static logParam(sqlMsg, param) {
if (param == null) {
sqlMsg += param === undefined ? 'undefined' : 'null';
} else {
switch (param.constructor.name) {
case 'Buffer':
sqlMsg += '0x' + param.toString('hex', 0, Math.min(1024, param.length)) + '';
break;
case 'String':
sqlMsg += "'" + param + "'";
break;
case 'Date':
sqlMsg += getStringDate(param);
break;
case 'Object':
sqlMsg += JSON.stringify(param);
break;
default:
sqlMsg += param.toString();
}
}
return sqlMsg;
}
}
function getStringDate(param) {
return (
"'" +
('00' + (param.getMonth() + 1)).slice(-2) +
'/' +
('00' + param.getDate()).slice(-2) +
'/' +
param.getFullYear() +
' ' +
('00' + param.getHours()).slice(-2) +
':' +
('00' + param.getMinutes()).slice(-2) +
':' +
('00' + param.getSeconds()).slice(-2) +
'.' +
('000' + param.getMilliseconds()).slice(-3) +
"'"
);
}
module.exports = Parser;
+51
View File
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Command = require('./command');
const ServerStatus = require('../const/server-status');
const PING_COMMAND = new Uint8Array([1, 0, 0, 0, 0x0e]);
/**
* send a COM_PING: permits sending a packet containing one byte to check that the connection is active.
* see https://mariadb.com/kb/en/library/com_ping/
*/
class Ping extends Command {
constructor(cmdParam, resolve, reject) {
super(cmdParam, resolve, reject);
}
start(out, opts, info) {
if (opts.logger.query) opts.logger.query('PING');
this.onPacketReceive = this.readPingResponsePacket;
out.fastFlush(this, PING_COMMAND);
this.emit('send_end');
}
/**
* Read ping response packet.
* packet can be :
* - an ERR_Packet
* - an OK_Packet
*
* @param packet query response
* @param out output writer
* @param opts connection options
* @param info connection info
*/
readPingResponsePacket(packet, out, opts, info) {
packet.skip(1); //skip header
packet.skipLengthCodedNumber(); //affected rows
packet.skipLengthCodedNumber(); //insert ids
info.status = packet.readUInt16();
if (info.redirectRequest && (info.status & ServerStatus.STATUS_IN_TRANS) === 0) {
info.redirect(info.redirectRequest, this.successEnd.bind(this, null));
} else {
this.successEnd(null);
}
}
}
module.exports = Ping;
+170
View File
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Parser = require('./parser');
const Parse = require('../misc/parse');
const BinaryEncoder = require('./encoder/binary-encoder');
const PrepareCacheWrapper = require('./class/prepare-cache-wrapper');
const PrepareResult = require('./class/prepare-result-packet');
const ServerStatus = require('../const/server-status');
const Errors = require('../misc/errors');
const ColumnDefinition = require('./column-definition');
/**
* send a COM_STMT_PREPARE: permits sending a prepare packet
* see https://mariadb.com/kb/en/com_stmt_prepare/
*/
class Prepare extends Parser {
constructor(resolve, reject, connOpts, cmdParam, conn) {
super(resolve, reject, connOpts, cmdParam);
this.encoder = new BinaryEncoder(this.opts);
this.binary = true;
this.conn = conn;
this.executeCommand = cmdParam.executeCommand;
}
/**
* Send COM_STMT_PREPARE
*
* @param out output writer
* @param opts connection options
* @param info connection information
*/
start(out, opts, info) {
// check in cache if enabled
if (this.conn.prepareCache) {
let cachedPrepare = this.conn.prepareCache.get(this.sql);
if (cachedPrepare) {
this.emit('send_end');
return this.successEnd(cachedPrepare);
}
}
if (opts.logger.query) opts.logger.query(`PREPARE: ${this.sql}`);
this.onPacketReceive = this.readPrepareResultPacket;
if (this.opts.namedPlaceholders) {
const res = Parse.searchPlaceholder(this.sql);
this.sql = res.sql;
this.placeHolderIndex = res.placeHolderIndex;
}
out.startPacket(this);
out.writeInt8(0x16);
out.writeString(this.sql);
out.flush();
this.emit('send_end');
}
successPrepare(info, opts) {
let prepare = new PrepareResult(
this.statementId,
this.parameterCount,
this._columns,
info.database,
this.sql,
this.placeHolderIndex,
this.conn
);
if (this.conn.prepareCache) {
let cached = new PrepareCacheWrapper(prepare);
this.conn.prepareCache.set(this.sql, cached);
const cachedWrappedPrepared = cached.incrementUse();
if (this.executeCommand) this.executeCommand.prepare = cachedWrappedPrepared;
return this.successEnd(cachedWrappedPrepared);
}
if (this.executeCommand) this.executeCommand.prepare = prepare;
this.successEnd(prepare);
}
/**
* Read COM_STMT_PREPARE response Packet.
* see https://mariadb.com/kb/en/library/com_stmt_prepare/#com_stmt_prepare-response
*
* @param packet COM_STMT_PREPARE_OK packet
* @param opts connection options
* @param info connection information
* @param out output writer
* @returns {*} null or {Result.readResponsePacket} in case of multi-result-set
*/
readPrepareResultPacket(packet, out, opts, info) {
switch (packet.peek()) {
//*********************************************************************************************************
//* PREPARE response
//*********************************************************************************************************
case 0x00:
packet.skip(1); //skip header
this.statementId = packet.readInt32();
this.columnNo = packet.readUInt16();
this.parameterCount = packet.readUInt16();
this._parameterNo = this.parameterCount;
this._columns = [];
if (this._parameterNo > 0) return (this.onPacketReceive = this.skipPrepareParameterPacket);
if (this.columnNo > 0) return (this.onPacketReceive = this.readPrepareColumnsPacket);
return this.successPrepare(info, opts);
//*********************************************************************************************************
//* ERROR response
//*********************************************************************************************************
case 0xff:
const err = packet.readError(info, this.displaySql(), this.stack);
//force in transaction status, since query will have created a transaction if autocommit is off
//goal is to avoid unnecessary COMMIT/ROLLBACK.
info.status |= ServerStatus.STATUS_IN_TRANS;
this.onPacketReceive = this.readResponsePacket;
return this.throwError(err, info);
//*********************************************************************************************************
//* Unexpected response
//*********************************************************************************************************
default:
info.status |= ServerStatus.STATUS_IN_TRANS;
this.onPacketReceive = this.readResponsePacket;
return this.throwError(Errors.ER_UNEXPECTED_PACKET, info);
}
}
readPrepareColumnsPacket(packet, out, opts, info) {
this.columnNo--;
this._columns.push(new ColumnDefinition(packet, info, opts.rowsAsArray));
if (this.columnNo === 0) {
if (info.eofDeprecated) {
return this.successPrepare(info, opts);
}
this.onPacketReceive = this.skipEofPacket;
}
}
skipEofPacket(packet, out, opts, info) {
if (this.columnNo > 0) return (this.onPacketReceive = this.readPrepareColumnsPacket);
this.successPrepare(info, opts);
}
skipPrepareParameterPacket(packet, out, opts, info) {
this._parameterNo--;
if (this._parameterNo === 0) {
if (info.eofDeprecated) {
if (this.columnNo > 0) return (this.onPacketReceive = this.readPrepareColumnsPacket);
return this.successPrepare(info, opts);
}
this.onPacketReceive = this.skipEofPacket;
}
}
/**
* Display current SQL with parameters (truncated if too big)
*
* @returns {string}
*/
displaySql() {
if (this.opts) {
if (this.sql.length > this.opts.debugLen) {
return this.sql.substring(0, this.opts.debugLen) + '...';
}
}
return this.sql;
}
}
module.exports = Prepare;
+392
View File
@@ -0,0 +1,392 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Parser = require('./parser');
const Errors = require('../misc/errors');
const Parse = require('../misc/parse');
const TextEncoder = require('./encoder/text-encoder');
const { Readable } = require('stream');
const QUOTE = 0x27;
/**
* Protocol COM_QUERY
* see : https://mariadb.com/kb/en/library/com_query/
*/
class Query extends Parser {
constructor(resolve, reject, connOpts, cmdParam) {
super(resolve, reject, connOpts, cmdParam);
this.binary = false;
}
/**
* Send COM_QUERY
*
* @param out output writer
* @param opts connection options
* @param info connection information
*/
start(out, opts, info) {
if (opts.logger.query) opts.logger.query(`QUERY: ${opts.logParam ? this.displaySql() : this.sql}`);
this.onPacketReceive = this.readResponsePacket;
if (this.initialValues === undefined) {
//shortcut if no parameters
out.startPacket(this);
out.writeInt8(0x03);
if (!this.handleTimeout(out, info)) return;
out.writeString(this.sql);
out.flush();
this.emit('send_end');
return;
}
this.encodedSql = out.encodeString(this.sql);
if (this.opts.namedPlaceholders) {
try {
const parsed = Parse.splitQueryPlaceholder(
this.encodedSql,
info,
this.initialValues,
this.opts.logParam ? this.displaySql.bind(this) : () => this.sql
);
this.paramPositions = parsed.paramPositions;
this.values = parsed.values;
} catch (err) {
this.emit('send_end');
return this.throwError(err, info);
}
} else {
this.paramPositions = Parse.splitQuery(this.encodedSql);
this.values = Array.isArray(this.initialValues) ? this.initialValues : [this.initialValues];
if (!this.validateParameters(info)) return;
}
out.startPacket(this);
out.writeInt8(0x03);
if (!this.handleTimeout(out, info)) return;
this.paramPos = 0;
this.sqlPos = 0;
//********************************************
// send params
//********************************************
const len = this.paramPositions.length / 2;
for (this.valueIdx = 0; this.valueIdx < len; ) {
out.writeBuffer(this.encodedSql, this.sqlPos, this.paramPositions[this.paramPos++] - this.sqlPos);
this.sqlPos = this.paramPositions[this.paramPos++];
const value = this.values[this.valueIdx++];
if (value == null) {
out.writeStringAscii('NULL');
continue;
}
switch (typeof value) {
case 'boolean':
out.writeStringAscii(value ? 'true' : 'false');
break;
case 'bigint':
case 'number':
out.writeStringAscii(`${value}`);
break;
case 'string':
out.writeStringEscapeQuote(value);
break;
case 'object':
if (typeof value.pipe === 'function' && typeof value.read === 'function') {
this.sending = true;
//********************************************
// param is stream,
// now all params will be written by event
//********************************************
this.paramWritten = this._paramWritten.bind(this, out, info);
out.writeInt8(QUOTE); //'
value.on('data', out.writeBufferEscape.bind(out));
value.on(
'end',
function () {
out.writeInt8(QUOTE); //'
this.paramWritten();
}.bind(this)
);
return;
}
if (Object.prototype.toString.call(value) === '[object Date]') {
out.writeStringAscii(TextEncoder.getLocalDate(value));
} else if (Buffer.isBuffer(value)) {
out.writeStringAscii("_BINARY '");
out.writeBufferEscape(value);
out.writeInt8(QUOTE);
} else if (typeof value.toSqlString === 'function') {
out.writeStringEscapeQuote(String(value.toSqlString()));
} else if (Array.isArray(value)) {
if (opts.arrayParenthesis) {
out.writeStringAscii('(');
}
for (let i = 0; i < value.length; i++) {
if (i !== 0) out.writeStringAscii(',');
if (value[i] == null) {
out.writeStringAscii('NULL');
} else TextEncoder.writeParam(out, value[i], opts, info);
}
if (opts.arrayParenthesis) {
out.writeStringAscii(')');
}
} else {
if (
value.type != null &&
[
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
].includes(value.type)
) {
//GeoJSON format.
let prefix =
(info.isMariaDB() && info.hasMinVersion(10, 1, 4)) || (!info.isMariaDB() && info.hasMinVersion(5, 7, 6))
? 'ST_'
: '';
switch (value.type) {
case 'Point':
out.writeStringAscii(
prefix + "PointFromText('POINT(" + TextEncoder.geoPointToString(value.coordinates) + ")')"
);
break;
case 'LineString':
out.writeStringAscii(
prefix + "LineFromText('LINESTRING(" + TextEncoder.geoArrayPointToString(value.coordinates) + ")')"
);
break;
case 'Polygon':
out.writeStringAscii(
prefix +
"PolygonFromText('POLYGON(" +
TextEncoder.geoMultiArrayPointToString(value.coordinates) +
")')"
);
break;
case 'MultiPoint':
out.writeStringAscii(
prefix +
"MULTIPOINTFROMTEXT('MULTIPOINT(" +
TextEncoder.geoArrayPointToString(value.coordinates) +
")')"
);
break;
case 'MultiLineString':
out.writeStringAscii(
prefix +
"MLineFromText('MULTILINESTRING(" +
TextEncoder.geoMultiArrayPointToString(value.coordinates) +
")')"
);
break;
case 'MultiPolygon':
out.writeStringAscii(
prefix +
"MPolyFromText('MULTIPOLYGON(" +
TextEncoder.geoMultiPolygonToString(value.coordinates) +
")')"
);
break;
case 'GeometryCollection':
out.writeStringAscii(
prefix +
"GeomCollFromText('GEOMETRYCOLLECTION(" +
TextEncoder.geometricCollectionToString(value.geometries) +
")')"
);
break;
}
} else if (String === value.constructor) {
out.writeStringEscapeQuote(value);
break;
} else {
if (opts.permitSetMultiParamEntries) {
let first = true;
for (let key in value) {
const val = value[key];
if (typeof val === 'function') continue;
if (first) {
first = false;
} else {
out.writeStringAscii(',');
}
out.writeString('`' + key + '`');
if (val == null) {
out.writeStringAscii('=NULL');
} else {
out.writeStringAscii('=');
TextEncoder.writeParam(out, val, opts, info);
}
}
if (first) out.writeStringEscapeQuote(JSON.stringify(value));
} else {
out.writeStringEscapeQuote(JSON.stringify(value));
}
}
}
break;
}
}
out.writeBuffer(this.encodedSql, this.sqlPos, this.encodedSql.length - this.sqlPos);
out.flush();
this.emit('send_end');
}
/**
* If timeout is set, prepend query with SET STATEMENT max_statement_time=xx FOR, or throw an error
* @param out buffer
* @param info server information
* @returns {boolean} false if an error has been thrown
*/
handleTimeout(out, info) {
if (this.opts.timeout) {
if (info.isMariaDB()) {
if (info.hasMinVersion(10, 1, 2)) {
out.writeString(`SET STATEMENT max_statement_time=${this.opts.timeout / 1000} FOR `);
return true;
} else {
this.sendCancelled(
`Cannot use timeout for xpand/MariaDB server before 10.1.2. timeout value: ${this.opts.timeout}`,
Errors.ER_TIMEOUT_NOT_SUPPORTED,
info
);
return false;
}
} else {
//not available for MySQL
// max_execution time exist, but only for select, and as hint
this.sendCancelled(
`Cannot use timeout for MySQL server. timeout value: ${this.opts.timeout}`,
Errors.ER_TIMEOUT_NOT_SUPPORTED,
info
);
return false;
}
}
return true;
}
/**
* Validate that parameters exists and are defined.
*
* @param info connection info
* @returns {boolean} return false if any error occur.
*/
validateParameters(info) {
//validate parameter size.
if (this.paramPositions.length / 2 > this.values.length) {
this.sendCancelled(
`Parameter at position ${this.values.length + 1} is not set`,
Errors.ER_MISSING_PARAMETER,
info
);
return false;
}
return true;
}
_paramWritten(out, info) {
while (true) {
if (this.valueIdx === this.paramPositions.length / 2) {
//********************************************
// all parameters are written.
// flush packet
//********************************************
out.writeBuffer(this.encodedSql, this.sqlPos, this.encodedSql.length - this.sqlPos);
out.flush();
this.sending = false;
this.emit('send_end');
return;
} else {
const value = this.values[this.valueIdx++];
out.writeBuffer(this.encodedSql, this.sqlPos, this.paramPositions[this.paramPos++] - this.sqlPos);
this.sqlPos = this.paramPositions[this.paramPos++];
if (value == null) {
out.writeStringAscii('NULL');
continue;
}
if (typeof value === 'object' && typeof value.pipe === 'function' && typeof value.read === 'function') {
//********************************************
// param is stream,
//********************************************
out.writeInt8(QUOTE);
value.once(
'end',
function () {
out.writeInt8(QUOTE);
this._paramWritten(out, info);
}.bind(this)
);
value.on('data', out.writeBufferEscape.bind(out));
return;
}
//********************************************
// param isn't stream. directly write in buffer
//********************************************
TextEncoder.writeParam(out, value, this.opts, info);
}
}
}
_stream(socket, options) {
this.socket = socket;
options = options || {};
options.objectMode = true;
options.read = () => {
this.socket.resume();
};
this.inStream = new Readable(options);
this.on('fields', function (meta) {
this.inStream.emit('fields', meta);
});
this.on('error', function (err) {
this.inStream.emit('error', err);
});
this.on('close', function (err) {
this.inStream.emit('error', err);
});
this.on('end', function (err) {
if (err) this.inStream.emit('error', err);
this.socket.resume();
this.inStream.push(null);
});
this.inStream.close = function () {
this.handleNewRows = () => {};
this.socket.resume();
}.bind(this);
this.handleNewRows = function (row) {
if (!this.inStream.push(row)) {
this.socket.pause();
}
};
return this.inStream;
}
}
module.exports = Query;
+31
View File
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Command = require('./command');
const QUIT_COMMAND = new Uint8Array([1, 0, 0, 0, 0x01]);
/**
* Quit (close connection)
* see https://mariadb.com/kb/en/library/com_quit/
*/
class Quit extends Command {
constructor(cmdParam, resolve, reject) {
super(cmdParam, resolve, reject);
}
start(out, opts, info) {
if (opts.logger.query) opts.logger.query('QUIT');
this.onPacketReceive = this.skipResults;
out.fastFlush(this, QUIT_COMMAND);
this.emit('send_end');
this.successEnd();
}
skipResults(packet, out, opts, info) {
//deliberately empty, if server send answer
}
}
module.exports = Quit;
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Command = require('./command');
const ServerStatus = require('../const/server-status');
const RESET_COMMAND = new Uint8Array([1, 0, 0, 0, 0x1f]);
/**
* send a COM_RESET_CONNECTION: permits to reset a connection without re-authentication.
* see https://mariadb.com/kb/en/library/com_reset_connection/
*/
class Reset extends Command {
constructor(cmdParam, resolve, reject) {
super(cmdParam, resolve, reject);
}
start(out, opts, info) {
if (opts.logger.query) opts.logger.query('RESET');
this.onPacketReceive = this.readResetResponsePacket;
out.fastFlush(this, RESET_COMMAND);
this.emit('send_end');
}
/**
* Read response packet.
* packet can be :
* - an ERR_Packet
* - a OK_Packet
*
* @param packet query response
* @param out output writer
* @param opts connection options
* @param info connection info
*/
readResetResponsePacket(packet, out, opts, info) {
packet.skip(1); //skip header
packet.skipLengthCodedNumber(); //affected rows
packet.skipLengthCodedNumber(); //insert ids
info.status = packet.readUInt16();
if (info.redirectRequest && (info.status & ServerStatus.STATUS_IN_TRANS) === 0) {
info.redirect(info.redirectRequest, this.successEnd.bind(this));
} else {
this.successEnd();
}
}
}
module.exports = Reset;
+60
View File
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Query = require('./query');
const { Readable } = require('stream');
/**
* Protocol COM_QUERY with streaming events.
* see : https://mariadb.com/kb/en/library/com_query/
*/
class Stream extends Query {
constructor(cmdParam, connOpts, socket) {
super(
() => {},
() => {},
connOpts,
cmdParam
);
this.socket = socket;
this.inStream = new Readable({
objectMode: true,
read: () => {
this.socket.resume();
}
});
this.on('fields', function (meta) {
this.inStream.emit('fields', meta);
});
this.on('error', function (err) {
this.inStream.emit('error', err);
});
this.on('close', function (err) {
this.inStream.emit('error', err);
});
this.on('end', function (err) {
if (err) this.inStream.emit('error', err);
this.socket.resume();
this.inStream.push(null);
});
this.inStream.close = function () {
this.handleNewRows = () => {};
this.socket.resume();
}.bind(this);
}
handleNewRows(row) {
if (!this.inStream.push(row)) {
this.socket.pause();
}
}
}
module.exports = Stream;
+23
View File
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
class ClusterOptions {
constructor(opts) {
if (opts) {
this.canRetry = opts.canRetry === undefined ? true : Boolean(opts.canRetry);
this.removeNodeErrorCount =
opts.removeNodeErrorCount === undefined ? Number.POSITIVE_INFINITY : Number(opts.removeNodeErrorCount);
this.restoreNodeTimeout = opts.restoreNodeTimeout === undefined ? 1000 : Number(opts.restoreNodeTimeout);
this.defaultSelector = opts.defaultSelector || 'RR';
} else {
this.canRetry = true;
this.removeNodeErrorCount = Number.POSITIVE_INFINITY;
this.restoreNodeTimeout = 1000;
this.defaultSelector = 'RR';
}
}
}
module.exports = ClusterOptions;
@@ -0,0 +1,322 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Collations = require('../const/collations.js');
const urlFormat = /mariadb:\/\/(([^/@:]+)?(:([^/]+))?@)?(([^/:]+)(:([0-9]+))?)\/([^?]+)(\?(.*))?$/;
/**
* Default option similar to mysql driver.
* known differences
* - no queryFormat option. Permitting client to parse is a security risk. Best is to give SQL + parameters
* Only possible Objects are :
* - Buffer
* - Date
* - Object that implement toSqlString function
* - JSON object
* + rowsAsArray (in mysql2) permit to have rows by index, not by name. Avoiding to parsing metadata string => faster
*/
class ConnectionOptions {
constructor(opts) {
if (typeof opts === 'string') {
opts = ConnectionOptions.parse(opts);
}
if (!opts) opts = {};
this.host = opts.host || 'localhost';
this.port = opts.port ? Number(opts.port) : 3306;
this.keepEof = Boolean(opts.keepEof) || false;
this.user = opts.user || process.env.USERNAME;
this.password = opts.password;
this.database = opts.database;
this.stream = opts.stream;
this.fullResult = opts.fullResult;
// log
this.debug = Boolean(opts.debug) || false;
this.debugCompress = Boolean(opts.debugCompress) || false;
this.debugLen = opts.debugLen ? Number(opts.debugLen) : 256;
this.logParam = opts.logParam === undefined ? true : Boolean(opts.logParam);
if (opts.logger) {
if (typeof opts.logger === 'function') {
this.logger = {
network: opts.logger,
query: opts.logger,
error: opts.logger,
warning: opts.logger
};
} else {
this.logger = {
network: opts.logger.network,
query: opts.logger.query,
error: opts.logger.error,
warning: opts.logger.warning || console.log
};
if (opts.logger.logParam !== undefined) this.logParam = Boolean(opts.logger.logParam);
}
} else {
this.logger = {
network: this.debug || this.debugCompress ? console.log : null,
query: null,
error: null,
warning: console.log
};
}
this.debug = !!this.logger.network;
if (opts.charset && typeof opts.charset === 'string') {
this.collation = Collations.fromCharset(opts.charset.toLowerCase());
if (this.collation === undefined) {
this.collation = Collations.fromName(opts.charset.toUpperCase());
if (this.collation !== undefined) {
this.logger.warning(
"warning: please use option 'collation' " +
"in replacement of 'charset' when using a collation name ('" +
opts.charset +
"')\n" +
"(collation looks like 'UTF8MB4_UNICODE_CI', charset like 'utf8')."
);
} else {
this.charset = opts.charset;
}
}
} else if (opts.collation && typeof opts.collation === 'string') {
this.collation = Collations.fromName(opts.collation.toUpperCase());
if (this.collation === undefined) throw new RangeError("Unknown collation '" + opts.collation + "'");
} else {
this.collation = opts.charsetNumber ? Collations.fromIndex(Number(opts.charsetNumber)) : undefined;
}
// connection options
this.initSql = opts.initSql;
this.connectTimeout = opts.connectTimeout === undefined ? 1000 : Number(opts.connectTimeout);
this.connectAttributes = opts.connectAttributes || false;
this.compress = Boolean(opts.compress) || false;
this.rsaPublicKey = opts.rsaPublicKey;
this.cachingRsaPublicKey = opts.cachingRsaPublicKey;
this.allowPublicKeyRetrieval = Boolean(opts.allowPublicKeyRetrieval) || false;
this.forceVersionCheck = Boolean(opts.forceVersionCheck) || false;
this.maxAllowedPacket = opts.maxAllowedPacket ? Number(opts.maxAllowedPacket) : undefined;
this.permitConnectionWhenExpired = Boolean(opts.permitConnectionWhenExpired) || false;
this.pipelining = opts.pipelining;
this.timezone = opts.timezone || 'local';
this.socketPath = opts.socketPath;
this.sessionVariables = opts.sessionVariables;
this.infileStreamFactory = opts.infileStreamFactory;
this.ssl = opts.ssl;
if (opts.ssl) {
if (typeof opts.ssl !== 'boolean' && typeof opts.ssl !== 'string') {
this.ssl.rejectUnauthorized = opts.ssl.rejectUnauthorized !== false;
}
}
this.permitRedirect =
opts.permitRedirect === undefined
? !!this.ssl && this.ssl.rejectUnauthorized !== false
: Boolean(opts.permitRedirect);
// socket
this.queryTimeout = isNaN(opts.queryTimeout) || Number(opts.queryTimeout) < 0 ? 0 : Number(opts.queryTimeout);
this.socketTimeout = isNaN(opts.socketTimeout) || Number(opts.socketTimeout) < 0 ? 0 : Number(opts.socketTimeout);
this.keepAliveDelay = opts.keepAliveDelay === undefined ? 0 : Number(opts.keepAliveDelay);
if (!opts.keepAliveDelay) {
// for mysql2 compatibility, check keepAliveInitialDelay/enableKeepAlive options.
if (opts.enableKeepAlive === true && opts.keepAliveInitialDelay !== undefined) {
this.keepAliveDelay = Number(opts.keepAliveInitialDelay);
}
}
this.trace = Boolean(opts.trace) || false;
// result-set
this.checkDuplicate = opts.checkDuplicate === undefined ? true : Boolean(opts.checkDuplicate);
this.dateStrings = Boolean(opts.dateStrings) || false;
this.foundRows = opts.foundRows === undefined || Boolean(opts.foundRows);
this.metaAsArray = Boolean(opts.metaAsArray) || false;
this.metaEnumerable = Boolean(opts.metaEnumerable) || false;
this.multipleStatements = Boolean(opts.multipleStatements) || false;
this.namedPlaceholders = Boolean(opts.namedPlaceholders) || false;
this.nestTables = opts.nestTables;
this.autoJsonMap = opts.autoJsonMap === undefined ? true : Boolean(opts.autoJsonMap);
this.jsonStrings = Boolean(opts.jsonStrings) || false;
if (opts.jsonStrings !== undefined) {
this.autoJsonMap = !this.jsonStrings;
}
this.bitOneIsBoolean = opts.bitOneIsBoolean === undefined ? true : Boolean(opts.bitOneIsBoolean);
this.arrayParenthesis = Boolean(opts.arrayParenthesis) || false;
this.permitSetMultiParamEntries = Boolean(opts.permitSetMultiParamEntries) || false;
this.rowsAsArray = Boolean(opts.rowsAsArray) || false;
this.typeCast = opts.typeCast;
if (this.typeCast !== undefined && typeof this.typeCast !== 'function') {
this.typeCast = undefined;
}
this.bulk = opts.bulk === undefined || Boolean(opts.bulk);
this.checkNumberRange = Boolean(opts.checkNumberRange) || false;
// coherence check
if (opts.pipelining === undefined) {
this.permitLocalInfile = Boolean(opts.permitLocalInfile) || false;
this.pipelining = !this.permitLocalInfile;
} else {
this.pipelining = Boolean(opts.pipelining);
if (opts.permitLocalInfile === true && this.pipelining) {
throw new Error(
'enabling options `permitLocalInfile` and `pipelining` is not possible, options are incompatible.'
);
}
this.permitLocalInfile = this.pipelining ? false : Boolean(opts.permitLocalInfile) || false;
}
this.prepareCacheLength = opts.prepareCacheLength === undefined ? 256 : Number(opts.prepareCacheLength);
this.restrictedAuth = opts.restrictedAuth;
if (this.restrictedAuth != null) {
if (!Array.isArray(this.restrictedAuth)) {
this.restrictedAuth = this.restrictedAuth.split(',');
}
}
// for compatibility with 2.x version and mysql/mysql2
this.bigIntAsNumber = Boolean(opts.bigIntAsNumber) || false;
this.insertIdAsNumber = Boolean(opts.insertIdAsNumber) || false;
this.decimalAsNumber = Boolean(opts.decimalAsNumber) || false;
this.supportBigNumbers = Boolean(opts.supportBigNumbers) || false;
this.bigNumberStrings = Boolean(opts.bigNumberStrings) || false;
if (opts.maxAllowedPacket && isNaN(this.maxAllowedPacket)) {
throw new RangeError(`maxAllowedPacket must be an integer. was '${opts.maxAllowedPacket}'`);
}
}
/**
* When parsing from String, correcting type.
*
* @param {object} opts - options
* @return {object} options with corrected data types
*/
static parseOptionDataType(opts) {
// Convert boolean strings to boolean values
const booleanOptions = [
'bulk',
'allowPublicKeyRetrieval',
'insertIdAsNumber',
'decimalAsNumber',
'bigIntAsNumber',
'permitRedirect',
'logParam',
'compress',
'dateStrings',
'debug',
'autoJsonMap',
'arrayParenthesis',
'checkDuplicate',
'debugCompress',
'foundRows',
'metaAsArray',
'metaEnumerable',
'multipleStatements',
'namedPlaceholders',
'nestTables',
'permitSetMultiParamEntries',
'pipelining',
'forceVersionCheck',
'rowsAsArray',
'trace',
'bitOneIsBoolean',
'jsonStrings',
'enableKeepAlive',
'supportBigNumbers',
'bigNumberStrings',
'keepEof',
'permitLocalInfile',
'permitConnectionWhenExpired'
];
booleanOptions.forEach((option) => {
if (opts[option] !== undefined && typeof opts[option] === 'string') {
opts[option] = opts[option] === 'true';
}
});
// Convert numeric strings to numbers
const numericOptions = [
'charsetNumber',
'connectTimeout',
'keepAliveDelay',
'socketTimeout',
'debugLen',
'prepareCacheLength',
'queryTimeout',
'maxAllowedPacket',
'keepAliveInitialDelay',
'port'
];
numericOptions.forEach((option) => {
if (opts[option] !== undefined && typeof opts[option] === 'string') {
const parsedValue = parseInt(opts[option], 10);
if (!isNaN(parsedValue)) {
opts[option] = parsedValue;
}
}
});
// Handle special case for SSL
if (opts.ssl !== undefined && typeof opts.ssl === 'string') {
opts.ssl = opts.ssl === 'true';
}
// Handle special case for connectAttributes (JSON parsing)
if (opts.connectAttributes !== undefined && typeof opts.connectAttributes === 'string') {
try {
opts.connectAttributes = JSON.parse(opts.connectAttributes);
} catch (e) {
throw new Error(`Failed to parse connectAttributes as JSON: ${e.message}`);
}
}
// Handle special case for sessionVariables (JSON parsing if it's a string and looks like JSON)
if (opts.sessionVariables !== undefined && typeof opts.sessionVariables === 'string') {
if (opts.sessionVariables.trim().startsWith('{')) {
try {
opts.sessionVariables = JSON.parse(opts.sessionVariables);
} catch (e) {
// If it fails to parse, keep it as a string
}
}
}
return opts;
}
static parse(opts) {
const matchResults = opts.match(urlFormat);
if (!matchResults) {
throw new Error(
`error parsing connection string '${opts}'. format must be 'mariadb://[<user>[:<password>]@]<host>[:<port>]/[<db>[?<opt1>=<value1>[&<opt2>=<value2>]]]'`
);
}
const options = {
user: matchResults[2] ? decodeURIComponent(matchResults[2]) : undefined,
password: matchResults[4] ? decodeURIComponent(matchResults[4]) : undefined,
host: matchResults[6] ? decodeURIComponent(matchResults[6]) : matchResults[6],
port: matchResults[8] ? parseInt(matchResults[8]) : undefined,
database: matchResults[9] ? decodeURIComponent(matchResults[9]) : matchResults[9]
};
const variousOptsString = matchResults[11];
if (variousOptsString) {
const keyValues = variousOptsString.split('&');
keyValues.forEach(function (keyVal) {
const equalIdx = keyVal.indexOf('=');
if (equalIdx !== 1) {
let val = keyVal.substring(equalIdx + 1);
val = val ? decodeURIComponent(val) : undefined;
options[keyVal.substring(0, equalIdx)] = val;
}
});
}
return this.parseOptionDataType(options);
}
}
module.exports = ConnectionOptions;
+55
View File
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
let ConnOptions = require('./connection-options');
class PoolOptions {
constructor(opts) {
if (typeof opts === 'string') {
opts = ConnOptions.parse(opts);
// Set data type
// These conversions will be replaced with explicit type casting in the main assignment below
if (opts.acquireTimeout) opts.acquireTimeout = parseInt(opts.acquireTimeout);
if (opts.connectionLimit) opts.connectionLimit = parseInt(opts.connectionLimit);
if (opts.idleTimeout) opts.idleTimeout = parseInt(opts.idleTimeout);
if (opts.leakDetectionTimeout) opts.leakDetectionTimeout = parseInt(opts.leakDetectionTimeout);
if (opts.initializationTimeout) opts.initializationTimeout = parseInt(opts.initializationTimeout);
if (opts.minDelayValidation) opts.minDelayValidation = parseInt(opts.minDelayValidation);
if (opts.minimumIdle) opts.minimumIdle = parseInt(opts.minimumIdle);
if (opts.noControlAfterUse) opts.noControlAfterUse = opts.noControlAfterUse === 'true';
if (opts.resetAfterUse) opts.resetAfterUse = opts.resetAfterUse === 'true';
if (opts.pingTimeout) opts.pingTimeout = parseInt(opts.pingTimeout);
}
// Apply explicit type conversion for all numeric options
this.acquireTimeout = opts.acquireTimeout === undefined ? 10000 : Number(opts.acquireTimeout);
this.connectionLimit = opts.connectionLimit === undefined ? 10 : Number(opts.connectionLimit);
this.idleTimeout = opts.idleTimeout === undefined ? 1800 : Number(opts.idleTimeout);
this.leakDetectionTimeout = Number(opts.leakDetectionTimeout) || 0;
this.initializationTimeout =
opts.initializationTimeout === undefined
? Math.max(100, this.acquireTimeout - 100)
: Number(opts.initializationTimeout);
this.minDelayValidation = opts.minDelayValidation === undefined ? 500 : Number(opts.minDelayValidation);
this.minimumIdle =
opts.minimumIdle === undefined ? this.connectionLimit : Math.min(Number(opts.minimumIdle), this.connectionLimit);
// Apply explicit type conversion for boolean options
this.noControlAfterUse = Boolean(opts.noControlAfterUse) || false;
this.resetAfterUse = Boolean(opts.resetAfterUse) || false;
this.pingTimeout = Number(opts.pingTimeout) || 250;
// Create connection options
this.connOptions = new ConnOptions(opts);
// Adjust connectTimeout if acquireTimeout is smaller
if (this.acquireTimeout > 0 && this.connOptions.connectTimeout > this.acquireTimeout) {
this.connOptions.connectTimeout = this.acquireTimeout;
}
}
}
module.exports = PoolOptions;
+532
View File
@@ -0,0 +1,532 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Errors = require('./misc/errors');
const { Status } = require('./const/connection_status');
const Query = require('./cmd/query');
class ConnectionCallback {
#conn;
constructor(conn) {
this.#conn = conn;
}
get threadId() {
return this.#conn.info ? this.#conn.info.threadId : null;
}
get info() {
return this.#conn.info;
}
#noop = () => {};
release = (cb) => {
this.#conn.release(() => {
if (cb) cb();
});
};
/**
* Permit changing user during connection.
* All user variables will be reset, Prepare commands will be released.
* !!! mysql has a bug when CONNECT_ATTRS capability is set, that is default !!!!
*
* @param options connection options
* @param callback callback function
*/
changeUser(options, callback) {
let _options, _cb;
if (typeof options === 'function') {
_cb = options;
_options = undefined;
} else {
_options = options;
_cb = callback;
}
const cmdParam = {
opts: _options,
callback: _cb
};
if (this.#conn.opts.trace) Error.captureStackTrace(cmdParam);
new Promise(this.#conn.changeUser.bind(this.#conn, cmdParam))
.then(() => {
if (cmdParam.callback) cmdParam.callback(null, null, null);
})
.catch(cmdParam.callback || this.#noop);
}
/**
* Start transaction
*
* @param callback callback function
*/
beginTransaction(callback) {
this.query('START TRANSACTION', null, callback);
}
/**
* Commit a transaction.
*
* @param callback callback function
*/
commit(callback) {
this.#conn.changeTransaction(
{ sql: 'COMMIT' },
() => {
if (callback) callback(null, null, null);
},
callback || this.#noop
);
}
/**
* Roll back a transaction.
*
* @param callback callback function
*/
rollback(callback) {
this.#conn.changeTransaction(
{ sql: 'ROLLBACK' },
() => {
if (callback) callback(null, null, null);
},
callback || this.#noop
);
}
/**
* Execute query using text protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @param callback callback function
*/
query(sql, values, callback) {
const cmdParam = ConnectionCallback._PARAM(this.#conn.opts, sql, values, callback);
return ConnectionCallback._QUERY_CMD(this.#conn, cmdParam);
}
/**
* Execute a query returning a Readable Object that will emit columns/data/end/error events
* to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede the default option.
* Object must then have `sql` property.
* @param values object / array of placeholder values (not mandatory)
* @returns {Readable}
*/
queryStream(sql, values) {
const cmdParam = ConnectionCallback._PARAM(this.#conn.opts, sql, values);
const cmd = ConnectionCallback._QUERY_CMD(this.#conn, cmdParam);
return cmd.stream();
}
static _QUERY_CMD(conn, cmdParam) {
let cmd;
if (cmdParam.callback) {
cmdParam.opts = cmdParam.opts ? Object.assign(cmdParam.opts, { metaAsArray: true }) : { metaAsArray: true };
cmd = new Query(
([rows, meta]) => {
cmdParam.callback(null, rows, meta);
},
cmdParam.callback,
conn.opts,
cmdParam
);
} else {
cmd = new Query(
() => {},
() => {},
conn.opts,
cmdParam
);
}
cmd.handleNewRows = (row) => {
cmd._rows[cmd._responseIndex].push(row);
cmd.emit('data', row);
};
conn.addCommand(cmd, true);
cmd.stream = (opt) => cmd._stream(conn.socket, opt);
return cmd;
}
execute(sql, values, callback) {
const cmdParam = ConnectionCallback._PARAM(this.#conn.opts, sql, values, callback);
cmdParam.opts = cmdParam.opts ? Object.assign(cmdParam.opts, { metaAsArray: true }) : { metaAsArray: true };
this.#conn.prepareExecute(
cmdParam,
([rows, meta]) => {
if (cmdParam.callback) {
cmdParam.callback(null, rows, meta);
}
},
(err) => {
if (cmdParam.callback) {
cmdParam.callback(err);
}
}
);
}
static _PARAM(options, sql, values, callback) {
let _cmdOpt,
_sql,
_values = values,
_cb = callback;
if (typeof values === 'function') {
_cb = values;
_values = undefined;
}
if (typeof sql === 'object') {
_cmdOpt = sql;
_sql = _cmdOpt.sql;
if (_cmdOpt.values) _values = _cmdOpt.values;
} else {
_sql = sql;
}
const cmdParam = {
sql: _sql,
values: _values,
opts: _cmdOpt,
callback: _cb
};
if (options.trace) Error.captureStackTrace(cmdParam, ConnectionCallback._PARAM);
return cmdParam;
}
static _EXECUTE_CMD(conn, cmdParam) {
new Promise(conn.prepare.bind(conn, cmdParam))
.then((prepare) => {
const opts = cmdParam.opts ? Object.assign(cmdParam.opts, { metaAsArray: true }) : { metaAsArray: true };
return prepare
.execute(cmdParam.values, opts, null, cmdParam.stack)
.then(([rows, meta]) => {
if (cmdParam.callback) {
cmdParam.callback(null, rows, meta);
}
})
.finally(() => prepare.close());
})
.catch((err) => {
if (conn.opts.logger.error) conn.opts.logger.error(err);
if (cmdParam.callback) cmdParam.callback(err);
});
}
prepare(sql, callback) {
let _cmdOpt, _sql;
if (typeof sql === 'object') {
_cmdOpt = sql;
_sql = _cmdOpt.sql;
} else {
_sql = sql;
}
const cmdParam = {
sql: _sql,
opts: _cmdOpt,
callback: callback
};
if (this.#conn.opts.trace) Error.captureStackTrace(cmdParam);
return new Promise(this.#conn.prepare.bind(this.#conn, cmdParam))
.then((prepare) => {
if (callback) callback(null, prepare, null);
})
.catch(callback || this.#noop);
}
/**
* Execute a batch
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede the default options.
* Object must then have `sql` property.
* @param values object / array of placeholder values (not mandatory)
* @param callback callback
*/
batch(sql, values, callback) {
const cmdParam = ConnectionCallback._PARAM(this.#conn.opts, sql, values, callback);
this.#conn.batch(
cmdParam,
(res) => {
if (cmdParam.callback) cmdParam.callback(null, res);
},
(err) => {
if (cmdParam.callback) cmdParam.callback(err);
}
);
}
/**
* Import sql file.
*
* @param opts JSON array with 2 possible fields: file and database
* @param cb callback
*/
importFile(opts, cb) {
if (!opts || !opts.file) {
if (cb)
cb(
Errors.createError(
'SQL file parameter is mandatory',
Errors.ER_MISSING_SQL_PARAMETER,
this.#conn.info,
'HY000',
null,
false,
null
)
);
return;
}
new Promise(this.#conn.importFile.bind(this.#conn, { file: opts.file, database: opts.database }))
.then(() => {
if (cb) cb();
})
.catch((err) => {
if (cb) cb(err);
});
}
/**
* Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
* @param timeout (optional) timeout value in ms. If reached, throw error and close connection
* @param callback callback
*/
ping(timeout, callback) {
let _cmdOpt = {},
_cb;
if (typeof timeout === 'function') {
_cb = timeout;
} else {
_cmdOpt.timeout = timeout;
_cb = callback;
}
const cmdParam = {
opts: _cmdOpt,
callback: _cb
};
if (this.#conn.opts.trace) Error.captureStackTrace(cmdParam);
new Promise(this.#conn.ping.bind(this.#conn, cmdParam)).then(_cb || this.#noop).catch(_cb || this.#noop);
}
/**
* Send a reset command that will
* - rollback any open transaction
* - reset transaction isolation level
* - reset session variables
* - delete user variables
* - remove temporary tables
* - remove all PREPARE statement
*
* @param callback callback
*/
reset(callback) {
const cmdParam = {};
if (this.#conn.opts.trace) Error.captureStackTrace(cmdParam);
return new Promise(this.#conn.reset.bind(this.#conn, cmdParam))
.then(callback || this.#noop)
.catch(callback || this.#noop);
}
/**
* Indicates the state of the connection as the driver knows it
* @returns {boolean}
*/
isValid() {
return this.#conn.isValid();
}
/**
* Terminate connection gracefully.
*
* @param callback callback
*/
end(callback) {
const cmdParam = {};
if (this.#conn.opts.trace) Error.captureStackTrace(cmdParam);
new Promise(this.#conn.end.bind(this.#conn, cmdParam))
.then(() => {
if (callback) callback();
})
.catch(callback || this.#noop);
}
/**
* Alias for destroy.
*/
close() {
this.destroy();
}
/**
* Force connection termination by closing the underlying socket and killing server process if any.
*/
destroy() {
this.#conn.destroy();
}
pause() {
this.#conn.pause();
}
resume() {
this.#conn.resume();
}
format(sql, values) {
this.#conn.format(sql, values);
}
/**
* return current connected server version information.
*
* @returns {*}
*/
serverVersion() {
return this.#conn.serverVersion();
}
/**
* Change option "debug" during connection.
* @param val debug value
*/
debug(val) {
return this.#conn.debug(val);
}
debugCompress(val) {
return this.#conn.debugCompress(val);
}
escape(val) {
return this.#conn.escape(val);
}
escapeId(val) {
return this.#conn.escapeId(val);
}
//*****************************************************************
// internal public testing methods
//*****************************************************************
get __tests() {
return this.#conn.__tests;
}
connect(callback) {
if (!callback) {
throw new Errors.createError(
'missing mandatory callback parameter',
Errors.ER_MISSING_PARAMETER,
this.#conn.info
);
}
switch (this.#conn.status) {
case Status.NOT_CONNECTED:
case Status.CONNECTING:
case Status.AUTHENTICATING:
case Status.INIT_CMD:
this.once('connect', callback);
break;
case Status.CONNECTED:
callback.call(this);
break;
case Status.CLOSING:
case Status.CLOSED:
callback.call(
this,
Errors.createError(
'Connection closed',
Errors.ER_CONNECTION_ALREADY_CLOSED,
this.#conn.info,
'08S01',
null,
true
)
);
break;
}
}
//*****************************************************************
// EventEmitter proxy methods
//*****************************************************************
on(eventName, listener) {
this.#conn.on.call(this.#conn, eventName, listener);
return this;
}
off(eventName, listener) {
this.#conn.off.call(this.#conn, eventName, listener);
return this;
}
once(eventName, listener) {
this.#conn.once.call(this.#conn, eventName, listener);
return this;
}
listeners(eventName) {
return this.#conn.listeners.call(this.#conn, eventName);
}
addListener(eventName, listener) {
this.#conn.addListener.call(this.#conn, eventName, listener);
return this;
}
eventNames() {
return this.#conn.eventNames.call(this.#conn);
}
getMaxListeners() {
return this.#conn.getMaxListeners.call(this.#conn);
}
listenerCount(eventName, listener) {
return this.#conn.listenerCount.call(this.#conn, eventName, listener);
}
prependListener(eventName, listener) {
this.#conn.prependListener.call(this.#conn, eventName, listener);
return this;
}
prependOnceListener(eventName, listener) {
this.#conn.prependOnceListener.call(this.#conn, eventName, listener);
return this;
}
removeAllListeners(eventName, listener) {
this.#conn.removeAllListeners.call(this.#conn, eventName, listener);
return this;
}
removeListener(eventName, listener) {
this.#conn.removeListener.call(this.#conn, eventName, listener);
return this;
}
setMaxListeners(n) {
this.#conn.setMaxListeners.call(this.#conn, n);
return this;
}
rawListeners(eventName) {
return this.#conn.rawListeners.call(this.#conn, eventName);
}
}
module.exports = ConnectionCallback;
+372
View File
@@ -0,0 +1,372 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Stream = require('./cmd/stream');
const Errors = require('./misc/errors');
/**
* New Connection instance.
*
* @param options connection options
* @returns Connection instance
* @constructor
* @fires Connection#connect
* @fires Connection#end
* @fires Connection#error
*
*/
class ConnectionPromise {
#conn;
#capture;
constructor(conn) {
this.#conn = conn;
this.#capture = conn.opts.trace ? Error.captureStackTrace : () => {};
}
get threadId() {
return this.#conn.threadId;
}
get info() {
return this.#conn.info;
}
get prepareCache() {
return this.#conn.prepareCache;
}
/**
* Permit to change user during connection.
* All user variables will be reset, Prepare commands will be released.
* !!! mysql has a bug when CONNECT_ATTRS capability is set, that is default !!!!
*
* @param options connection options
* @returns {Promise} promise
*/
changeUser(options) {
const param = { opts: options };
this.#capture(param);
return new Promise(this.#conn.changeUser.bind(this.#conn, param));
}
/**
* Start transaction
*
* @returns {Promise} promise
*/
beginTransaction() {
const param = { sql: 'START TRANSACTION' };
this.#capture(param);
return new Promise(this.#conn.query.bind(this.#conn, param));
}
/**
* Commit a transaction.
*
* @returns {Promise} command if commit was needed only
*/
commit() {
const param = { sql: 'COMMIT' };
this.#capture(param);
return new Promise(this.#conn.changeTransaction.bind(this.#conn, param));
}
/**
* Roll back a transaction.
*
* @returns {Promise} promise
*/
rollback() {
const param = { sql: 'ROLLBACK' };
this.#capture(param);
return new Promise(this.#conn.changeTransaction.bind(this.#conn, param));
}
/**
* Execute query using text protocol.
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @returns {Promise} promise
*/
query(sql, values) {
const cmdParam = paramSetter(sql, values);
this.#capture(cmdParam);
return new Promise(this.#conn.query.bind(this.#conn, cmdParam));
}
/**
* Execute a query returning a Readable Object that will emit columns/data/end/error events
* to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede the default option.
* Object must then have `sql` property.
* @param values object / array of placeholder values (not mandatory)
* @returns {Readable}
*/
queryStream(sql, values) {
const cmdParam = paramSetter(sql, values);
this.#capture(cmdParam);
const cmd = new Stream(cmdParam, this.#conn.opts, this.#conn.socket);
if (this.#conn.opts.logger.error) cmd.on('error', this.#conn.opts.logger.error);
this.#conn.addCommand(cmd, true);
return cmd.inStream;
}
static _PARAM_DEF(sql, values) {
if (typeof sql === 'object') {
return { sql: sql.sql, values: sql.values ? sql.values : values, opts: sql };
} else return { sql: sql, values: values };
}
execute(sql, values) {
const cmdParam = paramSetter(sql, values);
this.#capture(cmdParam);
return new Promise(this.#conn.prepareExecute.bind(this.#conn, cmdParam));
}
static _EXECUTE_CMD(conn, cmdParam) {
return conn.prepareExecute(cmdParam);
}
prepare(sql) {
let param;
if (typeof sql === 'object') {
param = { sql: sql.sql, opts: sql };
} else {
param = { sql: sql };
}
this.#capture(param);
return new Promise(this.#conn.prepare.bind(this.#conn, param));
}
/**
* Execute batch using text protocol.
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values
* @returns {Promise} promise
*/
batch(sql, values) {
const cmdParam = paramSetter(sql, values);
this.#capture(cmdParam);
return new Promise(this.#conn.batch.bind(this.#conn, cmdParam));
}
/**
* Import sql file.
*
* @param opts JSON array with 2 possible fields: file and database
*/
importFile(opts) {
if (!opts || !opts.file) {
return Promise.reject(
Errors.createError(
'SQL file parameter is mandatory',
Errors.ER_MISSING_SQL_PARAMETER,
this.#conn.info,
'HY000',
null,
false,
null
)
);
}
return new Promise(this.#conn.importFile.bind(this.#conn, { file: opts.file, database: opts.database }));
}
/**
* Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
* @param timeout (optional) timeout value in ms. If reached, throw error and close connection
* @returns {Promise} promise
*/
ping(timeout) {
const cmdParam = {
opts: { timeout: timeout }
};
this.#capture(cmdParam);
return new Promise(this.#conn.ping.bind(this.#conn, cmdParam));
}
/**
* Send a reset command that will
* - rollback any open transaction
* - reset transaction isolation level
* - reset session variables
* - delete user variables
* - remove temporary tables
* - remove all PREPARE statement
*
* @returns {Promise} promise
*/
reset() {
const cmdParam = {};
this.#capture(cmdParam);
return new Promise(this.#conn.reset.bind(this.#conn, cmdParam));
}
/**
* Indicates the state of the connection as the driver knows it
* @returns {boolean}
*/
isValid() {
return this.#conn.isValid();
}
/**
* Terminate connection gracefully.
*
* @returns {Promise} promise
*/
end() {
const cmdParam = {};
this.#capture(cmdParam);
return new Promise(this.#conn.end.bind(this.#conn, cmdParam));
}
/**
* Alias for destroy.
*/
close() {
this.destroy();
}
/**
* Force connection termination by closing the underlying socket and killing server process if any.
*/
destroy() {
this.#conn.destroy();
}
pause() {
this.#conn.pause();
}
resume() {
this.#conn.resume();
}
format(sql, values) {
this.#conn.format(sql, values);
}
/**
* return current connected server version information.
*
* @returns {*}
*/
serverVersion() {
return this.#conn.serverVersion();
}
/**
* Change option "debug" during connection.
* @param val debug value
*/
debug(val) {
return this.#conn.debug(val);
}
debugCompress(val) {
return this.#conn.debugCompress(val);
}
escape(val) {
return this.#conn.escape(val);
}
escapeId(val) {
return this.#conn.escapeId(val);
}
//*****************************************************************
// EventEmitter proxy methods
//*****************************************************************
on(eventName, listener) {
this.#conn.on.call(this.#conn, eventName, listener);
return this;
}
off(eventName, listener) {
this.#conn.off.call(this.#conn, eventName, listener);
return this;
}
once(eventName, listener) {
this.#conn.once.call(this.#conn, eventName, listener);
return this;
}
listeners(eventName) {
return this.#conn.listeners.call(this.#conn, eventName);
}
addListener(eventName, listener) {
this.#conn.addListener.call(this.#conn, eventName, listener);
return this;
}
eventNames() {
return this.#conn.eventNames.call(this.#conn);
}
getMaxListeners() {
return this.#conn.getMaxListeners.call(this.#conn);
}
listenerCount(eventName, listener) {
return this.#conn.listenerCount.call(this.#conn, eventName, listener);
}
prependListener(eventName, listener) {
this.#conn.prependListener.call(this.#conn, eventName, listener);
return this;
}
prependOnceListener(eventName, listener) {
this.#conn.prependOnceListener.call(this.#conn, eventName, listener);
return this;
}
removeAllListeners(eventName, listener) {
this.#conn.removeAllListeners.call(this.#conn, eventName, listener);
return this;
}
removeListener(eventName, listener) {
this.#conn.removeListener.call(this.#conn, eventName, listener);
return this;
}
setMaxListeners(n) {
this.#conn.setMaxListeners.call(this.#conn, n);
return this;
}
rawListeners(eventName) {
return this.#conn.rawListeners.call(this.#conn, eventName);
}
//*****************************************************************
// internal public testing methods
//*****************************************************************
get __tests() {
return this.#conn.__tests;
}
}
const paramSetter = function (sql, values) {
if (typeof sql === 'object') {
return { sql: sql.sql, values: sql.values ? sql.values : values, opts: sql };
} else return { sql: sql, values: values };
};
module.exports = ConnectionPromise;
module.exports.paramSetter = paramSetter;
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
/**
* Capabilities list ( with 'CLIENT_' removed)
* see : https://mariadb.com/kb/en/library/1-connecting-connecting/#capabilities
*/
/* mysql/old mariadb server/client */
module.exports.MYSQL = 1n;
/* Found instead of affected rows */
module.exports.FOUND_ROWS = 2n;
/* get all column flags */
module.exports.LONG_FLAG = 4n;
/* one can specify db on connect */
module.exports.CONNECT_WITH_DB = 8n;
/* don't allow database.table.column */
module.exports.NO_SCHEMA = 1n << 4n;
/* can use compression protocol */
module.exports.COMPRESS = 1n << 5n;
/* odbc client */
module.exports.ODBC = 1n << 6n;
/* can use LOAD DATA LOCAL */
module.exports.LOCAL_FILES = 1n << 7n;
/* ignore spaces before '' */
module.exports.IGNORE_SPACE = 1n << 8n;
/* new 4.1 protocol */
module.exports.PROTOCOL_41 = 1n << 9n;
/* this is an interactive client */
module.exports.INTERACTIVE = 1n << 10n;
/* switch to ssl after handshake */
module.exports.SSL = 1n << 11n;
/* IGNORE sigpipes */
module.exports.IGNORE_SIGPIPE = 1n << 12n;
/* client knows about transactions */
module.exports.TRANSACTIONS = 1n << 13n;
/* old flag for 4.1 protocol */
module.exports.RESERVED = 1n << 14n;
/* new 4.1 authentication */
module.exports.SECURE_CONNECTION = 1n << 15n;
/* enable/disable multi-stmt support */
module.exports.MULTI_STATEMENTS = 1n << 16n;
/* enable/disable multi-results */
module.exports.MULTI_RESULTS = 1n << 17n;
/* multi-results in ps-protocol */
module.exports.PS_MULTI_RESULTS = 1n << 18n;
/* client supports plugin authentication */
module.exports.PLUGIN_AUTH = 1n << 19n;
/* permits connection attributes */
module.exports.CONNECT_ATTRS = 1n << 20n;
/* Enable authentication response packet to be larger than 255 bytes. */
module.exports.PLUGIN_AUTH_LENENC_CLIENT_DATA = 1n << 21n;
/* Don't close the connection for a connection with expired password. */
module.exports.CAN_HANDLE_EXPIRED_PASSWORDS = 1n << 22n;
/* Capable of handling server state change information. It's a hint to the
server to include the state change information in Ok packet. */
module.exports.SESSION_TRACK = 1n << 23n;
/* Client no longer needs EOF packet */
module.exports.DEPRECATE_EOF = 1n << 24n;
module.exports.SSL_VERIFY_SERVER_CERT = 1n << 30n;
/* MariaDB extended capabilities */
/* Permit bulk insert*/
module.exports.MARIADB_CLIENT_STMT_BULK_OPERATIONS = 1n << 34n;
/* Clients supporting extended metadata */
module.exports.MARIADB_CLIENT_EXTENDED_METADATA = 1n << 35n;
/* permit metadata caching */
module.exports.MARIADB_CLIENT_CACHE_METADATA = 1n << 36n;
/* permit returning all bulk individual results */
module.exports.BULK_UNIT_RESULTS = 1n << 37n;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Status = {
NOT_CONNECTED: 1,
CONNECTING: 2,
AUTHENTICATING: 3,
INIT_CMD: 4,
CONNECTED: 5,
CLOSING: 6,
CLOSED: 7
};
module.exports.Status = Status;
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
/**
* Column definition packet "Field detail" flag value
* see : https://mariadb.com/kb/en/library/resultset/#field-detail-flag
*/
// field cannot be null
module.exports.NOT_NULL = 1;
// field is a primary key
module.exports.PRIMARY_KEY = 2;
//field is unique
module.exports.UNIQUE_KEY = 4;
//field is in a multiple key
module.exports.MULTIPLE_KEY = 8;
//is this field a Blob
module.exports.BLOB = 1 << 4;
// is this field unsigned
module.exports.UNSIGNED = 1 << 5;
//is this field a zerofill
module.exports.ZEROFILL_FLAG = 1 << 6;
//whether this field has a binary collation
module.exports.BINARY_COLLATION = 1 << 7;
//Field is an enumeration
module.exports.ENUM = 1 << 8;
//field auto-increment
module.exports.AUTO_INCREMENT = 1 << 9;
//field is a timestamp value
module.exports.TIMESTAMP = 1 << 10;
//field is a SET
module.exports.SET = 1 << 11;
//field doesn't have default value
module.exports.NO_DEFAULT_VALUE_FLAG = 1 << 12;
//field is set to NOW on UPDATE
module.exports.ON_UPDATE_NOW_FLAG = 1 << 13;
//field is num
module.exports.NUM_FLAG = 1 << 14;
+74
View File
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
/**
* Field types
* see https://mariadb.com/kb/en/library/resultset/#field-types
*/
module.exports.DECIMAL = 0;
module.exports.TINY = 1;
module.exports.SHORT = 2;
module.exports.INT = 3;
module.exports.FLOAT = 4;
module.exports.DOUBLE = 5;
module.exports.NULL = 6;
module.exports.TIMESTAMP = 7;
module.exports.BIGINT = 8;
module.exports.INT24 = 9;
module.exports.DATE = 10;
module.exports.TIME = 11;
module.exports.DATETIME = 12;
module.exports.YEAR = 13;
module.exports.NEWDATE = 14;
module.exports.VARCHAR = 15;
module.exports.BIT = 16;
module.exports.TIMESTAMP2 = 17;
module.exports.DATETIME2 = 18;
module.exports.TIME2 = 19;
module.exports.JSON = 245; //only for MySQL
module.exports.NEWDECIMAL = 246;
module.exports.ENUM = 247;
module.exports.SET = 248;
module.exports.TINY_BLOB = 249;
module.exports.MEDIUM_BLOB = 250;
module.exports.LONG_BLOB = 251;
module.exports.BLOB = 252;
module.exports.VAR_STRING = 253;
module.exports.STRING = 254;
module.exports.GEOMETRY = 255;
const typeNames = [];
typeNames[0] = 'DECIMAL';
typeNames[1] = 'TINY';
typeNames[2] = 'SHORT';
typeNames[3] = 'INT';
typeNames[4] = 'FLOAT';
typeNames[5] = 'DOUBLE';
typeNames[6] = 'NULL';
typeNames[7] = 'TIMESTAMP';
typeNames[8] = 'BIGINT';
typeNames[9] = 'INT24';
typeNames[10] = 'DATE';
typeNames[11] = 'TIME';
typeNames[12] = 'DATETIME';
typeNames[13] = 'YEAR';
typeNames[14] = 'NEWDATE';
typeNames[15] = 'VARCHAR';
typeNames[16] = 'BIT';
typeNames[17] = 'TIMESTAMP2';
typeNames[18] = 'DATETIME2';
typeNames[19] = 'TIME2';
typeNames[245] = 'JSON';
typeNames[246] = 'NEWDECIMAL';
typeNames[247] = 'ENUM';
typeNames[248] = 'SET';
typeNames[249] = 'TINY_BLOB';
typeNames[250] = 'MEDIUM_BLOB';
typeNames[251] = 'LONG_BLOB';
typeNames[252] = 'BLOB';
typeNames[253] = 'VAR_STRING';
typeNames[254] = 'STRING';
typeNames[255] = 'GEOMETRY';
module.exports.TYPES = typeNames;
+33
View File
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
/**
* possible server status flag value
* see https://mariadb.com/kb/en/library/ok_packet/#server-status-flag
* @type {number}
*/
//A transaction is currently active
module.exports.STATUS_IN_TRANS = 1;
//Autocommit mode is set
module.exports.STATUS_AUTOCOMMIT = 2;
//more results exists (more packet follow)
module.exports.MORE_RESULTS_EXISTS = 8;
module.exports.QUERY_NO_GOOD_INDEX_USED = 16;
module.exports.QUERY_NO_INDEX_USED = 32;
//when using COM_STMT_FETCH, indicate that current cursor still has result (deprecated)
module.exports.STATUS_CURSOR_EXISTS = 64;
//when using COM_STMT_FETCH, indicate that current cursor has finished to send results (deprecated)
module.exports.STATUS_LAST_ROW_SENT = 128;
//database has been dropped
module.exports.STATUS_DB_DROPPED = 1 << 8;
//current escape mode is "no backslash escape"
module.exports.STATUS_NO_BACKSLASH_ESCAPES = 1 << 9;
//A DDL change did have an impact on an existing PREPARE (an automatic re-prepare has been executed)
module.exports.STATUS_METADATA_CHANGED = 1 << 10;
module.exports.QUERY_WAS_SLOW = 1 << 11;
//this result-set contain stored procedure output parameter
module.exports.PS_OUT_PARAMS = 1 << 12;
//current transaction is a read-only transaction
module.exports.STATUS_IN_TRANS_READONLY = 1 << 13;
//session state change. see Session change type for more information
module.exports.SESSION_STATE_CHANGED = 1 << 14;
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
/**
* Session change type.
* see : https://mariadb.com/kb/en/library/ok_packet/#session-change-type
* @type {number}
*/
module.exports.SESSION_TRACK_SYSTEM_VARIABLES = 0;
module.exports.SESSION_TRACK_SCHEMA = 1;
module.exports.SESSION_TRACK_STATE_CHANGE = 2;
module.exports.SESSION_TRACK_GTIDS = 3;
module.exports.SESSION_TRACK_TRANSACTION_CHARACTERISTICS = 4;
module.exports.SESSION_TRACK_TRANSACTION_STATE = 5;
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
/**
* Similar to pool cluster with a pre-set pattern and selector.
* Additional method query
*
* @param poolCluster cluster
* @param patternArg pre-set pattern
* @param selectorArg pre-set selector
* @constructor
*/
class FilteredClusterCallback {
#cluster;
#pattern;
#selector;
constructor(poolCluster, patternArg, selectorArg) {
this.#cluster = poolCluster;
this.#pattern = patternArg;
this.#selector = selectorArg;
}
/**
* Get a connection according to a previously indicated pattern and selector.
*/
getConnection(callback) {
const cal = callback ? callback : (err, conn) => {};
return this.#cluster.getConnection(this.#pattern, this.#selector, cal);
}
/**
* Execute a text query on one connection from an available pools matching pattern
* in cluster.
*
* @param sql sql command
* @param value parameter value of SQL command (not mandatory)
* @param callback callback parameters
* @return {Promise}
*/
query(sql, value, callback) {
let sq = sql,
val = value,
cal = callback;
if (typeof value === 'function') {
val = null;
cal = value;
}
const endingFct = cal ? cal : () => {};
this.getConnection((err, conn) => {
if (err) {
endingFct(err);
} else {
conn.query(sq, val, (err, res, meta) => {
conn.release(() => {});
if (err) {
endingFct(err);
} else {
endingFct(null, res, meta);
}
});
}
});
}
/**
* Execute a binary query on one connection from an available pools matching pattern
* in cluster.
*
* @param sql sql command
* @param value parameter value of SQL command (not mandatory)
* @param callback callback function
*/
execute(sql, value, callback) {
let sq = sql,
val = value,
cal = callback;
if (typeof value === 'function') {
val = null;
cal = value;
}
const endingFct = cal ? cal : () => {};
this.getConnection((err, conn) => {
if (err) {
endingFct(err);
} else {
conn.execute(sq, val, (err, res, meta) => {
conn.release(() => {});
if (err) {
endingFct(err);
} else {
endingFct(null, res, meta);
}
});
}
});
}
/**
* Execute a batch on one connection from an available pools matching pattern
* in cluster.
*
* @param sql sql command
* @param value parameter value of SQL command
* @param callback callback function
*/
batch(sql, value, callback) {
let sq = sql,
val = value,
cal = callback;
if (typeof value === 'function') {
val = null;
cal = value;
}
const endingFct = cal ? cal : () => {};
this.getConnection((err, conn) => {
if (err) {
endingFct(err);
} else {
conn.batch(sq, val, (err, res, meta) => {
conn.release(() => {});
if (err) {
endingFct(err);
} else {
endingFct(null, res, meta);
}
});
}
});
}
}
module.exports = FilteredClusterCallback;
+118
View File
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
/**
* Similar to pool cluster with pre-set pattern and selector.
* Additional method query
*
* @param poolCluster cluster
* @param patternArg pre-set pattern
* @param selectorArg pre-set selector
* @constructor
*/
class FilteredCluster {
#cluster;
#pattern;
#selector;
constructor(poolCluster, patternArg, selectorArg) {
this.#cluster = poolCluster;
this.#pattern = patternArg;
this.#selector = selectorArg;
}
/**
* Get a connection according to a previously indicated pattern and selector.
*
* @return {Promise}
*/
getConnection() {
return this.#cluster.getConnection(this.#pattern, this.#selector);
}
/**
* Execute a text query on one connection from an available pools matching pattern
* in cluster.
*
* @param sql sql command
* @param value parameter value of sql command (not mandatory)
* @return {Promise}
*/
query(sql, value) {
return this.#cluster
.getConnection(this.#pattern, this.#selector)
.then((conn) => {
return conn
.query(sql, value)
.then((res) => {
conn.release();
return res;
})
.catch((err) => {
conn.release();
return Promise.reject(err);
});
})
.catch((err) => {
return Promise.reject(err);
});
}
/**
* Execute a binary query on one connection from available pools matching pattern
* in cluster.
*
* @param sql sql command
* @param value parameter value of sql command (not mandatory)
* @return {Promise}
*/
execute(sql, value) {
return this.#cluster
.getConnection(this.#pattern, this.#selector)
.then((conn) => {
return conn
.execute(sql, value)
.then((res) => {
conn.release();
return res;
})
.catch((err) => {
conn.release();
return Promise.reject(err);
});
})
.catch((err) => {
return Promise.reject(err);
});
}
/**
* Execute a batch on one connection from available pools matching pattern
* in cluster.
*
* @param sql sql command
* @param value parameter value of sql command
* @return {Promise}
*/
batch(sql, value) {
return this.#cluster
.getConnection(this.#pattern, this.#selector)
.then((conn) => {
return conn
.batch(sql, value)
.then((res) => {
conn.release();
return res;
})
.catch((err) => {
conn.release();
return Promise.reject(err);
});
})
.catch((err) => {
return Promise.reject(err);
});
}
}
module.exports = FilteredCluster;
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const ZLib = require('zlib');
const Utils = require('../misc/utils');
/**
* MySQL packet parser
* see : https://mariadb.com/kb/en/library/0-packet/
*/
class CompressionInputStream {
constructor(reader, receiveQueue, opts, info) {
this.reader = reader;
this.receiveQueue = receiveQueue;
this.info = info;
this.opts = opts;
this.header = Buffer.allocUnsafe(7);
this.headerLen = 0;
this.compressPacketLen = null;
this.packetLen = null;
this.remainingLen = null;
this.parts = null;
this.partsTotalLen = 0;
}
receivePacket(chunk) {
let cmd = this.currentCmd();
if (this.opts.debugCompress) {
this.opts.logger.network(
`<== conn:${this.info.threadId ? this.info.threadId : -1} ${
cmd
? cmd.onPacketReceive
? cmd.constructor.name + '.' + cmd.onPacketReceive.name
: cmd.constructor.name
: 'no command'
} (compress)\n${Utils.log(this.opts, chunk, 0, chunk.length, this.header)}`
);
}
if (cmd) cmd.compressSequenceNo = this.header[3];
const unCompressLen = this.header[4] | (this.header[5] << 8) | (this.header[6] << 16);
if (unCompressLen === 0) {
this.reader.onData(chunk);
} else {
//use synchronous inflating, to ensure FIFO packet order
const unCompressChunk = ZLib.inflateSync(chunk);
this.reader.onData(unCompressChunk);
}
}
currentCmd() {
let cmd;
while ((cmd = this.receiveQueue.peek())) {
if (cmd.onPacketReceive) return cmd;
this.receiveQueue.shift();
}
return null;
}
resetHeader() {
this.remainingLen = null;
this.headerLen = 0;
}
onData(chunk) {
let pos = 0;
let length;
const chunkLen = chunk.length;
do {
if (this.remainingLen) {
length = this.remainingLen;
} else if (this.headerLen === 0 && chunkLen - pos >= 7) {
this.header[0] = chunk[pos];
this.header[1] = chunk[pos + 1];
this.header[2] = chunk[pos + 2];
this.header[3] = chunk[pos + 3];
this.header[4] = chunk[pos + 4];
this.header[5] = chunk[pos + 5];
this.header[6] = chunk[pos + 6];
this.headerLen = 7;
pos += 7;
this.compressPacketLen = this.header[0] + (this.header[1] << 8) + (this.header[2] << 16);
this.packetLen = this.header[4] | (this.header[5] << 8) | (this.header[6] << 16);
if (this.packetLen === 0) this.packetLen = this.compressPacketLen;
length = this.compressPacketLen;
} else {
length = null;
while (chunkLen - pos > 0) {
this.header[this.headerLen++] = chunk[pos++];
if (this.headerLen === 7) {
this.compressPacketLen = this.header[0] + (this.header[1] << 8) + (this.header[2] << 16);
this.packetLen = this.header[4] | (this.header[5] << 8) | (this.header[6] << 16);
if (this.packetLen === 0) this.packetLen = this.compressPacketLen;
length = this.compressPacketLen;
break;
}
}
}
if (length) {
if (chunkLen - pos >= length) {
const buf = chunk.subarray(pos, pos + length);
pos += length;
if (this.parts) {
this.parts.push(buf);
this.partsTotalLen += length;
if (this.compressPacketLen < 0xffffff) {
let buf = Buffer.concat(this.parts, this.partsTotalLen);
this.parts = null;
this.receivePacket(buf);
}
} else {
if (this.compressPacketLen < 0xffffff) {
this.receivePacket(buf);
} else {
this.parts = [buf];
this.partsTotalLen = length;
}
}
this.resetHeader();
} else {
const buf = chunk.subarray(pos, chunkLen);
if (!this.parts) {
this.parts = [buf];
this.partsTotalLen = chunkLen - pos;
} else {
this.parts.push(buf);
this.partsTotalLen += chunkLen - pos;
}
this.remainingLen = length - (chunkLen - pos);
return;
}
}
} while (pos < chunkLen);
}
}
module.exports = CompressionInputStream;
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Utils = require('../misc/utils');
const ZLib = require('zlib');
//increase by level to avoid buffer copy.
const SMALL_BUFFER_SIZE = 2048;
const MEDIUM_BUFFER_SIZE = 131072; //128k
const LARGE_BUFFER_SIZE = 1048576; //1M
const MAX_BUFFER_SIZE = 16777222; //16M + 7
/**
/**
* MySQL compression filter.
* see https://mariadb.com/kb/en/library/0-packet/#compressed-packet
*/
class CompressionOutputStream {
/**
* Constructor
*
* @param socket current socket
* @param opts current connection options
* @param info current connection information
* @constructor
*/
constructor(socket, opts, info) {
this.info = info;
this.opts = opts;
this.pos = 7;
this.header = Buffer.allocUnsafe(7);
this.buf = Buffer.allocUnsafe(SMALL_BUFFER_SIZE);
this.writer = (buffer) => {
socket.write(buffer);
};
}
growBuffer(len) {
let newCapacity;
if (len + this.pos < MEDIUM_BUFFER_SIZE) {
newCapacity = MEDIUM_BUFFER_SIZE;
} else if (len + this.pos < LARGE_BUFFER_SIZE) {
newCapacity = LARGE_BUFFER_SIZE;
} else newCapacity = MAX_BUFFER_SIZE;
let newBuf = Buffer.allocUnsafe(newCapacity);
this.buf.copy(newBuf, 0, 0, this.pos);
this.buf = newBuf;
}
writeBuf(arr, cmd) {
let off = 0,
len = arr.length;
if (arr instanceof Uint8Array) {
arr = Buffer.from(arr);
}
if (len > this.buf.length - this.pos) {
if (this.buf.length !== MAX_BUFFER_SIZE) {
this.growBuffer(len);
}
//max buffer size
if (len > this.buf.length - this.pos) {
//not enough space in buffer, will stream :
// fill buffer and flush until all data are snd
let remainingLen = len;
while (true) {
//filling buffer
let lenToFillBuffer = Math.min(MAX_BUFFER_SIZE - this.pos, remainingLen);
arr.copy(this.buf, this.pos, off, off + lenToFillBuffer);
remainingLen -= lenToFillBuffer;
off += lenToFillBuffer;
this.pos += lenToFillBuffer;
if (remainingLen === 0) return;
this.flush(false, cmd, remainingLen);
}
}
}
arr.copy(this.buf, this.pos, off, off + len);
this.pos += len;
}
/**
* Flush the internal buffer.
*/
flush(cmdEnd, cmd, remainingLen) {
if (this.pos < 1536) {
//*******************************************************************************
// small packet, no compression
//*******************************************************************************
this.buf[0] = this.pos - 7;
this.buf[1] = (this.pos - 7) >>> 8;
this.buf[2] = (this.pos - 7) >>> 16;
this.buf[3] = ++cmd.compressSequenceNo;
this.buf[4] = 0;
this.buf[5] = 0;
this.buf[6] = 0;
if (this.opts.debugCompress) {
this.opts.logger.network(
`==> conn:${this.info.threadId ? this.info.threadId : -1} ${
cmd ? cmd.constructor.name + '(0,' + this.pos + ')' : 'unknown'
} (compress)\n${Utils.log(this.opts, this.buf, 0, this.pos)}`
);
}
this.writer(this.buf.subarray(0, this.pos));
} else {
//*******************************************************************************
// compressing packet
//*******************************************************************************
//use synchronous inflating, to ensure FIFO packet order
const compressChunk = ZLib.deflateSync(this.buf.subarray(7, this.pos));
const compressChunkLen = compressChunk.length;
this.header[0] = compressChunkLen;
this.header[1] = compressChunkLen >>> 8;
this.header[2] = compressChunkLen >>> 16;
this.header[3] = ++cmd.compressSequenceNo;
this.header[4] = this.pos - 7;
this.header[5] = (this.pos - 7) >>> 8;
this.header[6] = (this.pos - 7) >>> 16;
if (this.opts.debugCompress) {
this.opts.logger.network(
`==> conn:${this.info.threadId ? this.info.threadId : -1} ${
cmd ? cmd.constructor.name + '(0,' + this.pos + '=>' + compressChunkLen + ')' : 'unknown'
} (compress)\n${Utils.log(this.opts, compressChunk, 0, compressChunkLen, this.header)}`
);
}
this.writer(this.header);
this.writer(compressChunk);
if (cmdEnd && compressChunkLen === MAX_BUFFER_SIZE) this.writeEmptyPacket(cmd);
this.header = Buffer.allocUnsafe(7);
}
this.buf = remainingLen
? CompressionOutputStream.allocateBuffer(remainingLen)
: Buffer.allocUnsafe(SMALL_BUFFER_SIZE);
this.pos = 7;
}
static allocateBuffer(len) {
if (len + 4 < SMALL_BUFFER_SIZE) {
return Buffer.allocUnsafe(SMALL_BUFFER_SIZE);
} else if (len + 4 < MEDIUM_BUFFER_SIZE) {
return Buffer.allocUnsafe(MEDIUM_BUFFER_SIZE);
} else if (len + 4 < LARGE_BUFFER_SIZE) {
return Buffer.allocUnsafe(LARGE_BUFFER_SIZE);
}
return Buffer.allocUnsafe(MAX_BUFFER_SIZE);
}
writeEmptyPacket(cmd) {
const emptyBuf = Buffer.from([0x00, 0x00, 0x00, cmd.compressSequenceNo, 0x00, 0x00, 0x00]);
if (this.opts.debugCompress) {
this.opts.logger.network(
`==> conn:${this.info.threadId ? this.info.threadId : -1} ${
cmd ? cmd.constructor.name + '(0,' + this.pos + ')' : 'unknown'
} (compress)\n${Utils.log(this.opts, emptyBuf, 0, 7)}`
);
}
this.writer(emptyBuf);
}
}
module.exports = CompressionOutputStream;
+204
View File
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const PacketNodeEncoded = require('./packet-node-encoded');
const PacketIconvEncoded = require('./packet-node-iconv');
const Collations = require('../const/collations');
const Utils = require('../misc/utils');
/**
* MySQL packet parser
* see : https://mariadb.com/kb/en/library/0-packet/
*/
class PacketInputStream {
constructor(unexpectedPacket, receiveQueue, out, opts, info) {
this.unexpectedPacket = unexpectedPacket;
this.opts = opts;
this.receiveQueue = receiveQueue;
this.info = info;
this.out = out;
//in case packet is not complete
this.header = Buffer.allocUnsafe(4);
this.headerLen = 0;
this.packetLen = null;
this.remainingLen = null;
this.parts = null;
this.partsTotalLen = 0;
this.changeEncoding(this.opts.collation ? this.opts.collation : Collations.fromIndex(224));
this.changeDebug(this.opts.debug);
this.opts.on('collation', this.changeEncoding.bind(this));
this.opts.on('debug', this.changeDebug.bind(this));
}
changeEncoding(collation) {
this.encoding = collation.charset;
this.packet = Buffer.isEncoding(this.encoding)
? new PacketNodeEncoded(this.encoding)
: new PacketIconvEncoded(this.encoding);
}
changeDebug(debug) {
this.receivePacket = debug ? this.receivePacketDebug : this.receivePacketBasic;
}
receivePacketDebug(packet) {
let cmd = this.currentCmd();
this.header[0] = this.packetLen;
this.header[1] = this.packetLen >> 8;
this.header[2] = this.packetLen >> 16;
this.header[3] = this.sequenceNo;
if (packet) {
this.opts.logger.network(
`<== conn:${this.info.threadId ? this.info.threadId : -1} ${
cmd
? cmd.onPacketReceive
? cmd.constructor.name + '.' + cmd.onPacketReceive.name
: cmd.constructor.name
: 'no command'
} (${packet.pos},${packet.end})\n${Utils.log(this.opts, packet.buf, packet.pos, packet.end, this.header)}`
);
}
if (!cmd) {
this.unexpectedPacket(packet);
return;
}
cmd.sequenceNo = this.sequenceNo;
cmd.onPacketReceive(packet, this.out, this.opts, this.info);
if (!cmd.onPacketReceive) {
this.receiveQueue.shift();
}
}
receivePacketBasic(packet) {
let cmd = this.currentCmd();
if (!cmd) {
this.unexpectedPacket(packet);
return;
}
cmd.sequenceNo = this.sequenceNo;
cmd.onPacketReceive(packet, this.out, this.opts, this.info);
if (!cmd.onPacketReceive) this.receiveQueue.shift();
}
resetHeader() {
this.remainingLen = null;
this.headerLen = 0;
}
currentCmd() {
let cmd;
while ((cmd = this.receiveQueue.peek())) {
if (cmd.onPacketReceive) return cmd;
this.receiveQueue.shift();
}
return null;
}
onData(chunk) {
let pos = 0;
let length;
const chunkLen = chunk.length;
do {
//read header
if (this.remainingLen) {
length = this.remainingLen;
} else if (this.headerLen === 0 && chunkLen - pos >= 4) {
this.packetLen = chunk[pos] + (chunk[pos + 1] << 8) + (chunk[pos + 2] << 16);
this.sequenceNo = chunk[pos + 3];
pos += 4;
length = this.packetLen;
} else {
length = null;
while (chunkLen - pos > 0) {
this.header[this.headerLen++] = chunk[pos++];
if (this.headerLen === 4) {
this.packetLen = this.header[0] + (this.header[1] << 8) + (this.header[2] << 16);
this.sequenceNo = this.header[3];
length = this.packetLen;
break;
}
}
}
if (length) {
if (chunkLen - pos >= length) {
pos += length;
if (!this.parts) {
if (this.packetLen < 0xffffff) {
this.receivePacket(this.packet.update(chunk, pos - length, pos));
// fast path, knowing there is no parts
// loop can be simplified until reaching the end of the packet.
while (pos + 4 < chunkLen) {
this.packetLen = chunk[pos] + (chunk[pos + 1] << 8) + (chunk[pos + 2] << 16);
this.sequenceNo = chunk[pos + 3];
pos += 4;
if (chunkLen - pos >= this.packetLen) {
pos += this.packetLen;
if (this.packetLen < 0xffffff) {
this.receivePacket(this.packet.update(chunk, pos - this.packetLen, pos));
} else {
this.parts = [chunk.subarray(pos - this.packetLen, pos)];
this.partsTotalLen = this.packetLen;
break;
}
} else {
const buf = chunk.subarray(pos, chunkLen);
if (!this.parts) {
this.parts = [buf];
this.partsTotalLen = chunkLen - pos;
} else {
this.parts.push(buf);
this.partsTotalLen += chunkLen - pos;
}
this.remainingLen = this.packetLen - (chunkLen - pos);
return;
}
}
} else {
this.parts = [chunk.subarray(pos - length, pos)];
this.partsTotalLen = length;
}
} else {
this.parts.push(chunk.subarray(pos - length, pos));
this.partsTotalLen += length;
if (this.packetLen < 0xffffff) {
let buf = Buffer.concat(this.parts, this.partsTotalLen);
this.parts = null;
this.receivePacket(this.packet.update(buf, 0, this.partsTotalLen));
}
}
this.resetHeader();
} else {
const buf = chunk.subarray(pos, chunkLen);
if (!this.parts) {
this.parts = [buf];
this.partsTotalLen = chunkLen - pos;
} else {
this.parts.push(buf);
this.partsTotalLen += chunkLen - pos;
}
this.remainingLen = length - (chunkLen - pos);
return;
}
} else if (length === 0 && this.parts) {
// ending empty packet
this.parts.push(chunk.subarray(pos - length, pos));
this.partsTotalLen += length;
let buf = Buffer.concat(this.parts, this.partsTotalLen);
this.parts = null;
this.receivePacket(this.packet.update(buf, 0, this.partsTotalLen));
this.resetHeader();
}
} while (pos < chunkLen);
}
}
module.exports = PacketInputStream;
+40
View File
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Packet = require('./packet');
class PacketNodeEncoded extends Packet {
constructor(encoding) {
super();
// using undefined for utf8 permit to avoid node.js searching
// for charset, using directly utf8 default one.
this.encoding = encoding === 'utf8' ? undefined : encoding;
}
readStringLengthEncoded() {
const len = this.readUnsignedLength();
if (len === null) return null;
this.pos += len;
return this.buf.toString(this.encoding, this.pos - len, this.pos);
}
static readString(encoding, buf, beg, len) {
return buf.toString(encoding, beg, beg + len);
}
subPacketLengthEncoded(len) {
this.skip(len);
return new PacketNodeEncoded(this.encoding).update(this.buf, this.pos - len, this.pos);
}
readStringRemaining() {
const str = this.buf.toString(this.encoding, this.pos, this.end);
this.pos = this.end;
return str;
}
}
module.exports = PacketNodeEncoded;
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Packet = require('./packet');
const Iconv = require('iconv-lite');
class PacketIconvEncoded extends Packet {
constructor(encoding) {
super();
this.encoding = encoding;
}
readStringLengthEncoded() {
const len = this.readUnsignedLength();
if (len === null) return null;
this.pos += len;
return Iconv.decode(this.buf.subarray(this.pos - len, this.pos), this.encoding);
}
static readString(encoding, buf, beg, len) {
return Iconv.decode(buf.subarray(beg, beg + len), encoding);
}
subPacketLengthEncoded(len) {
this.skip(len);
return new PacketIconvEncoded(this.encoding).update(this.buf, this.pos - len, this.pos);
}
readStringRemaining() {
const str = Iconv.decode(this.buf.subarray(this.pos, this.end), this.encoding);
this.pos = this.end;
return str;
}
}
module.exports = PacketIconvEncoded;
+770
View File
@@ -0,0 +1,770 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const Iconv = require('iconv-lite');
const Utils = require('../misc/utils');
const Errors = require('../misc/errors');
const Collations = require('../const/collations');
const QUOTE = 0x27;
const DBL_QUOTE = 0x22;
const ZERO_BYTE = 0x00;
const SLASH = 0x5c;
//increase by level to avoid buffer copy.
const SMALL_BUFFER_SIZE = 256;
const MEDIUM_BUFFER_SIZE = 16384; //16k
const LARGE_BUFFER_SIZE = 131072; //128k
const BIG_BUFFER_SIZE = 1048576; //1M
const MAX_BUFFER_SIZE = 16777219; //16M + 4
const CHARS_GLOBAL_REGEXP = /[\000\032"'\\\n\r\t]/g;
/**
* MySQL packet builder.
*
* @param opts options
* @param info connection info
* @constructor
*/
class PacketOutputStream {
constructor(opts, info) {
this.opts = opts;
this.info = info;
this.pos = 4;
this.markPos = -1;
this.bufContainDataAfterMark = false;
this.cmdLength = 0;
this.buf = Buffer.allocUnsafe(SMALL_BUFFER_SIZE);
this.maxAllowedPacket = opts.maxAllowedPacket || 16777216;
this.maxPacketLength = Math.min(MAX_BUFFER_SIZE, this.maxAllowedPacket + 4);
this.changeEncoding(this.opts.collation ? this.opts.collation : Collations.fromIndex(224));
this.changeDebug(this.opts.debug);
this.opts.on('collation', this.changeEncoding.bind(this));
this.opts.on('debug', this.changeDebug.bind(this));
}
changeEncoding(collation) {
this.encoding = collation.charset;
if (this.encoding === 'utf8') {
this.writeString = this.writeDefaultBufferString;
this.encodeString = this.encodeNodeString;
this.writeLengthEncodedString = this.writeDefaultBufferLengthEncodedString;
this.writeStringEscapeQuote = this.writeUtf8StringEscapeQuote;
} else if (Buffer.isEncoding(this.encoding)) {
this.writeString = this.writeDefaultBufferString;
this.encodeString = this.encodeNodeString;
this.writeLengthEncodedString = this.writeDefaultBufferLengthEncodedString;
this.writeStringEscapeQuote = this.writeDefaultStringEscapeQuote;
} else {
this.writeString = this.writeDefaultIconvString;
this.encodeString = this.encodeIconvString;
this.writeLengthEncodedString = this.writeDefaultIconvLengthEncodedString;
this.writeStringEscapeQuote = this.writeDefaultStringEscapeQuote;
}
}
changeDebug(debug) {
this.debug = debug;
this.flushBuffer = debug ? this.flushBufferDebug : this.flushBufferBasic;
this.fastFlush = debug ? this.fastFlushDebug : this.fastFlushBasic;
}
setStream(stream) {
this.stream = stream;
}
growBuffer(len) {
let newCapacity;
if (len + this.pos < MEDIUM_BUFFER_SIZE) {
newCapacity = MEDIUM_BUFFER_SIZE;
} else if (len + this.pos < LARGE_BUFFER_SIZE) {
newCapacity = LARGE_BUFFER_SIZE;
} else if (len + this.pos < BIG_BUFFER_SIZE) {
newCapacity = BIG_BUFFER_SIZE;
} else if (this.bufContainDataAfterMark) {
// special case, for bulk, when a bunch of parameter doesn't fit in 16Mb packet
// this save bunch of encoded parameter, sending parameter until mark, then resending data after mark
newCapacity = len + this.pos;
} else {
newCapacity = MAX_BUFFER_SIZE;
}
if (len + this.pos > newCapacity) {
if (this.markPos !== -1) {
// buf is > 16M with mark.
// flush until mark, reset pos at beginning
this.flushBufferStopAtMark();
if (len + this.pos <= this.buf.length) {
return;
}
return this.growBuffer(len);
}
}
let newBuf = Buffer.allocUnsafe(newCapacity);
this.buf.copy(newBuf, 0, 0, this.pos);
this.buf = newBuf;
}
mark() {
this.markPos = this.pos;
}
isMarked() {
return this.markPos !== -1;
}
hasFlushed() {
return this.cmd.sequenceNo !== -1;
}
hasDataAfterMark() {
return this.bufContainDataAfterMark;
}
bufIsAfterMaxPacketLength() {
return this.pos > this.maxPacketLength;
}
/**
* Reset mark flag and send bytes after mark flag.
*
* @return buffer after mark flag
*/
resetMark() {
this.pos = this.markPos;
this.markPos = -1;
if (this.bufContainDataAfterMark) {
const data = Buffer.allocUnsafe(this.pos - 4);
this.buf.copy(data, 0, 4, this.pos);
this.cmd.sequenceNo = -1;
this.cmd.compressSequenceNo = -1;
this.bufContainDataAfterMark = false;
return data;
}
return null;
}
/**
* Send packet to socket.
*
* @throws IOException if socket error occur.
*/
flush() {
this.flushBuffer(true, 0);
this.buf = Buffer.allocUnsafe(SMALL_BUFFER_SIZE);
this.cmd.sequenceNo = -1;
this.cmd.compressSequenceNo = -1;
this.cmdLength = 0;
this.markPos = -1;
}
flushPacket() {
this.flushBuffer(false, 0);
this.buf = Buffer.allocUnsafe(SMALL_BUFFER_SIZE);
this.cmdLength = 0;
this.markPos = -1;
}
startPacket(cmd) {
this.cmd = cmd;
this.pos = 4;
}
writeInt8(value) {
if (this.pos + 1 >= this.buf.length) {
let b = Buffer.allocUnsafe(1);
b[0] = value;
this.writeBuffer(b, 0, 1);
return;
}
this.buf[this.pos++] = value;
}
writeInt16(value) {
if (this.pos + 2 >= this.buf.length) {
let b = Buffer.allocUnsafe(2);
b[0] = value;
b[1] = value >>> 8;
this.writeBuffer(b, 0, 2);
return;
}
this.buf[this.pos] = value;
this.buf[this.pos + 1] = value >> 8;
this.pos += 2;
}
writeInt16AtPos(initPos) {
this.buf[initPos] = this.pos - initPos - 2;
this.buf[initPos + 1] = (this.pos - initPos - 2) >> 8;
}
writeInt24(value) {
if (this.pos + 3 >= this.buf.length) {
//not enough space remaining
let arr = Buffer.allocUnsafe(3);
arr[0] = value;
arr[1] = value >> 8;
arr[2] = value >> 16;
this.writeBuffer(arr, 0, 3);
return;
}
this.buf[this.pos] = value;
this.buf[this.pos + 1] = value >> 8;
this.buf[this.pos + 2] = value >> 16;
this.pos += 3;
}
writeInt32(value) {
if (this.pos + 4 >= this.buf.length) {
//not enough space remaining
let arr = Buffer.allocUnsafe(4);
arr.writeInt32LE(value, 0);
this.writeBuffer(arr, 0, 4);
return;
}
this.buf[this.pos] = value;
this.buf[this.pos + 1] = value >> 8;
this.buf[this.pos + 2] = value >> 16;
this.buf[this.pos + 3] = value >> 24;
this.pos += 4;
}
writeBigInt(value) {
if (this.pos + 8 >= this.buf.length) {
//not enough space remaining
let arr = Buffer.allocUnsafe(8);
arr.writeBigInt64LE(value, 0);
this.writeBuffer(arr, 0, 8);
return;
}
this.buf.writeBigInt64LE(value, this.pos);
this.pos += 8;
}
writeDouble(value) {
if (this.pos + 8 >= this.buf.length) {
//not enough space remaining
let arr = Buffer.allocUnsafe(8);
arr.writeDoubleLE(value, 0);
this.writeBuffer(arr, 0, 8);
return;
}
this.buf.writeDoubleLE(value, this.pos);
this.pos += 8;
}
writeLengthCoded(len) {
if (len < 0xfb) {
this.writeInt8(len);
return;
}
if (len < 65536) {
//max length is len < 0xffff
this.writeInt8(0xfc);
this.writeInt16(len);
} else if (len < 16777216) {
this.writeInt8(0xfd);
this.writeInt24(len);
} else {
this.writeInt8(0xfe);
this.writeBigInt(BigInt(len));
}
}
writeBuffer(arr, off, len) {
if (len > this.buf.length - this.pos) {
if (this.buf.length !== MAX_BUFFER_SIZE) {
this.growBuffer(len);
}
//max buffer size
if (len > this.buf.length - this.pos) {
if (this.markPos !== -1) {
this.growBuffer(len);
if (this.markPos !== -1) {
this.flushBufferStopAtMark();
}
}
if (len > this.buf.length - this.pos) {
//not enough space in buffer, will stream :
// fill buffer and flush until all data are snd
let remainingLen = len;
while (true) {
//filling buffer
let lenToFillBuffer = Math.min(MAX_BUFFER_SIZE - this.pos, remainingLen);
arr.copy(this.buf, this.pos, off, off + lenToFillBuffer);
remainingLen -= lenToFillBuffer;
off += lenToFillBuffer;
this.pos += lenToFillBuffer;
if (remainingLen === 0) return;
this.flushBuffer(false, remainingLen);
}
}
}
}
// node.js copy is fast only when copying big buffer.
// quick array copy is multiple time faster for small copy
if (len > 50) {
arr.copy(this.buf, this.pos, off, off + len);
this.pos += len;
} else {
for (let i = 0; i < len; ) {
this.buf[this.pos++] = arr[off + i++];
}
}
}
/**
* Write ascii string to socket (no escaping)
*
* @param str string
*/
writeStringAscii(str) {
let len = str.length;
//not enough space remaining
if (len >= this.buf.length - this.pos) {
let strBuf = Buffer.from(str, 'ascii');
this.writeBuffer(strBuf, 0, strBuf.length);
return;
}
for (let off = 0; off < len; ) {
this.buf[this.pos++] = str.charCodeAt(off++);
}
}
writeLengthEncodedBuffer(buffer) {
const len = buffer.length;
this.writeLengthCoded(len);
this.writeBuffer(buffer, 0, len);
}
writeUtf8StringEscapeQuote(str) {
const charsLength = str.length;
//not enough space remaining
if (charsLength * 3 + 2 >= this.buf.length - this.pos) {
const arr = Buffer.from(str, 'utf8');
this.writeInt8(QUOTE);
this.writeBufferEscape(arr);
this.writeInt8(QUOTE);
return;
}
//create UTF-8 byte array
//since javascript char are internally using UTF-16 using surrogate's pattern, 4 bytes unicode characters will
//represent 2 characters : example "\uD83C\uDFA4" = 🎤 unicode 8 "no microphones"
//so max size is 3 * charLength
//(escape characters are 1 byte encoded, so length might only be 2 when escaped)
// + 2 for the quotes for text protocol
let charsOffset = 0;
let currChar;
this.buf[this.pos++] = QUOTE;
//quick loop if only ASCII chars for faster escape
for (; charsOffset < charsLength && (currChar = str.charCodeAt(charsOffset)) < 0x80; charsOffset++) {
if (currChar === SLASH || currChar === QUOTE || currChar === ZERO_BYTE || currChar === DBL_QUOTE) {
this.buf[this.pos++] = SLASH;
}
this.buf[this.pos++] = currChar;
}
//if quick loop not finished
while (charsOffset < charsLength) {
currChar = str.charCodeAt(charsOffset++);
if (currChar < 0x80) {
if (currChar === SLASH || currChar === QUOTE || currChar === ZERO_BYTE || currChar === DBL_QUOTE) {
this.buf[this.pos++] = SLASH;
}
this.buf[this.pos++] = currChar;
} else if (currChar < 0x800) {
this.buf[this.pos++] = 0xc0 | (currChar >> 6);
this.buf[this.pos++] = 0x80 | (currChar & 0x3f);
} else if (currChar >= 0xd800 && currChar < 0xe000) {
//reserved for surrogate - see https://en.wikipedia.org/wiki/UTF-16
if (currChar < 0xdc00) {
//is high surrogate
if (charsOffset + 1 > charsLength) {
this.buf[this.pos++] = 0x3f;
} else {
const nextChar = str.charCodeAt(charsOffset);
if (nextChar >= 0xdc00 && nextChar < 0xe000) {
//is low surrogate
const surrogatePairs = (currChar << 10) + nextChar + (0x010000 - (0xd800 << 10) - 0xdc00);
this.buf[this.pos++] = 0xf0 | (surrogatePairs >> 18);
this.buf[this.pos++] = 0x80 | ((surrogatePairs >> 12) & 0x3f);
this.buf[this.pos++] = 0x80 | ((surrogatePairs >> 6) & 0x3f);
this.buf[this.pos++] = 0x80 | (surrogatePairs & 0x3f);
charsOffset++;
} else {
//must have low surrogate
this.buf[this.pos++] = 0x3f;
}
}
} else {
//low surrogate without high surrogate before
this.buf[this.pos++] = 0x3f;
}
} else {
this.buf[this.pos++] = 0xe0 | (currChar >> 12);
this.buf[this.pos++] = 0x80 | ((currChar >> 6) & 0x3f);
this.buf[this.pos++] = 0x80 | (currChar & 0x3f);
}
}
this.buf[this.pos++] = QUOTE;
}
encodeIconvString(str) {
return Iconv.encode(str, this.encoding);
}
encodeNodeString(str) {
return Buffer.from(str, this.encoding);
}
writeDefaultBufferString(str) {
//javascript use UCS-2 or UTF-16 string internal representation
//that means that string to byte will be a maximum of * 3
// (4 bytes utf-8 are represented on 2 UTF-16 characters)
if (str.length * 3 < this.buf.length - this.pos) {
this.pos += this.buf.write(str, this.pos, this.encoding);
return;
}
//checking real length
let byteLength = Buffer.byteLength(str, this.encoding);
if (byteLength > this.buf.length - this.pos) {
if (this.buf.length < MAX_BUFFER_SIZE) {
this.growBuffer(byteLength);
}
if (byteLength > this.buf.length - this.pos) {
//not enough space in buffer, will stream :
let strBuf = Buffer.from(str, this.encoding);
this.writeBuffer(strBuf, 0, strBuf.length);
return;
}
}
this.pos += this.buf.write(str, this.pos, this.encoding);
}
writeDefaultBufferLengthEncodedString(str) {
//javascript use UCS-2 or UTF-16 string internal representation
//that means that string to byte will be a maximum of * 3
// (4 bytes utf-8 are represented on 2 UTF-16 characters)
//checking real length
let byteLength = Buffer.byteLength(str, this.encoding);
this.writeLengthCoded(byteLength);
if (byteLength > this.buf.length - this.pos) {
if (this.buf.length < MAX_BUFFER_SIZE) {
this.growBuffer(byteLength);
}
if (byteLength > this.buf.length - this.pos) {
//not enough space in buffer, will stream :
let strBuf = Buffer.from(str, this.encoding);
this.writeBuffer(strBuf, 0, strBuf.length);
return;
}
}
this.pos += this.buf.write(str, this.pos, this.encoding);
}
writeDefaultIconvString(str) {
let buf = Iconv.encode(str, this.encoding);
this.writeBuffer(buf, 0, buf.length);
}
writeDefaultIconvLengthEncodedString(str) {
let buf = Iconv.encode(str, this.encoding);
this.writeLengthCoded(buf.length);
this.writeBuffer(buf, 0, buf.length);
}
/**
* Parameters need to be properly escaped :
* following characters are to be escaped by "\" :
* - \0
* - \\
* - \'
* - \"
* - \032
* regex split part of string writing part, and escaping special char.
* Those chars are <= 7f meaning that this will work even with multibyte encoding
*
* @param str string to escape.
*/
writeDefaultStringEscapeQuote(str) {
this.writeInt8(QUOTE);
let match;
let lastIndex = 0;
while ((match = CHARS_GLOBAL_REGEXP.exec(str)) !== null) {
this.writeString(str.slice(lastIndex, match.index));
this.writeInt8(SLASH);
this.writeInt8(match[0].charCodeAt(0));
lastIndex = CHARS_GLOBAL_REGEXP.lastIndex;
}
if (lastIndex === 0) {
// Nothing was escaped
this.writeString(str);
this.writeInt8(QUOTE);
return;
}
if (lastIndex < str.length) {
this.writeString(str.slice(lastIndex));
}
this.writeInt8(QUOTE);
}
writeBinaryDate(date) {
const year = date.getFullYear();
const mon = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const min = date.getMinutes();
const sec = date.getSeconds();
const ms = date.getMilliseconds();
let len = ms === 0 ? 7 : 11;
//not enough space remaining
if (len + 1 > this.buf.length - this.pos) {
let tmpBuf = Buffer.allocUnsafe(len + 1);
tmpBuf[0] = len;
tmpBuf[1] = year;
tmpBuf[2] = year >>> 8;
tmpBuf[3] = mon;
tmpBuf[4] = day;
tmpBuf[5] = hour;
tmpBuf[6] = min;
tmpBuf[7] = sec;
if (ms !== 0) {
const micro = ms * 1000;
tmpBuf[8] = micro;
tmpBuf[9] = micro >>> 8;
tmpBuf[10] = micro >>> 16;
tmpBuf[11] = micro >>> 24;
}
this.writeBuffer(tmpBuf, 0, len + 1);
return;
}
this.buf[this.pos] = len;
this.buf[this.pos + 1] = year;
this.buf[this.pos + 2] = year >>> 8;
this.buf[this.pos + 3] = mon;
this.buf[this.pos + 4] = day;
this.buf[this.pos + 5] = hour;
this.buf[this.pos + 6] = min;
this.buf[this.pos + 7] = sec;
if (ms !== 0) {
const micro = ms * 1000;
this.buf[this.pos + 8] = micro;
this.buf[this.pos + 9] = micro >>> 8;
this.buf[this.pos + 10] = micro >>> 16;
this.buf[this.pos + 11] = micro >>> 24;
}
this.pos += len + 1;
}
writeBufferEscape(val) {
let valLen = val.length;
if (valLen * 2 > this.buf.length - this.pos) {
//makes buffer bigger (up to 16M)
if (this.buf.length !== MAX_BUFFER_SIZE) this.growBuffer(valLen * 2);
//data may still be bigger than buffer.
//must flush buffer when full (and reset position to 4)
if (valLen * 2 > this.buf.length - this.pos) {
//not enough space in buffer, will fill buffer
for (let i = 0; i < valLen; i++) {
switch (val[i]) {
case QUOTE:
case SLASH:
case DBL_QUOTE:
case ZERO_BYTE:
if (this.pos >= this.buf.length) this.flushBuffer(false, (valLen - i) * 2);
this.buf[this.pos++] = SLASH; //add escape slash
}
if (this.pos >= this.buf.length) this.flushBuffer(false, (valLen - i) * 2);
this.buf[this.pos++] = val[i];
}
return;
}
}
//sure to have enough place to use buffer directly
for (let i = 0; i < valLen; i++) {
switch (val[i]) {
case QUOTE:
case SLASH:
case DBL_QUOTE:
case ZERO_BYTE:
this.buf[this.pos++] = SLASH; //add escape slash
}
this.buf[this.pos++] = val[i];
}
}
/**
* Count query size. If query size is greater than max_allowed_packet and nothing has been already
* send, throw an exception to avoid having the connection closed.
*
* @param length additional length to query size
* @param info current connection information
* @throws Error if query has not to be sent.
*/
checkMaxAllowedLength(length, info) {
if (this.opts.maxAllowedPacket && this.cmdLength + length >= this.maxAllowedPacket) {
// launch exception only if no packet has been sent.
return Errors.createError(
`query size (${this.cmdLength + length}) is >= to max_allowed_packet (${this.maxAllowedPacket})`,
Errors.ER_MAX_ALLOWED_PACKET,
info
);
}
return null;
}
/**
* Indicate if buffer contain any data.
* @returns {boolean}
*/
isEmpty() {
return this.pos <= 4;
}
/**
* Flush the internal buffer.
*/
flushBufferDebug(commandEnd, remainingLen) {
if (this.pos > 4) {
this.buf[0] = this.pos - 4;
this.buf[1] = (this.pos - 4) >>> 8;
this.buf[2] = (this.pos - 4) >>> 16;
this.buf[3] = ++this.cmd.sequenceNo;
this.stream.writeBuf(this.buf.subarray(0, this.pos), this.cmd);
this.stream.flush(true, this.cmd);
this.cmdLength += this.pos - 4;
this.opts.logger.network(
`==> conn:${this.info.threadId ? this.info.threadId : -1} ${
this.cmd.constructor.name + '(0,' + this.pos + ')'
}\n${Utils.log(this.opts, this.buf, 0, this.pos)}`
);
if (commandEnd && this.pos === MAX_BUFFER_SIZE) {
//if last packet fill the max size, must send an empty com to indicate that command end.
this.writeEmptyPacket();
}
this.buf = this.createBufferWithMinSize(remainingLen);
this.pos = 4;
}
}
/**
* Flush to last mark.
*/
flushBufferStopAtMark() {
const end = this.pos;
this.pos = this.markPos;
const tmpBuf = Buffer.allocUnsafe(Math.max(SMALL_BUFFER_SIZE, end + 4 - this.pos));
this.buf.copy(tmpBuf, 4, this.markPos, end);
this.flushBuffer(true, end - this.pos);
this.cmdLength = 0;
this.buf = tmpBuf;
this.pos = 4 + end - this.markPos;
this.markPos = -1;
this.bufContainDataAfterMark = true;
}
flushBufferBasic(commandEnd, remainingLen) {
this.buf[0] = this.pos - 4;
this.buf[1] = (this.pos - 4) >>> 8;
this.buf[2] = (this.pos - 4) >>> 16;
this.buf[3] = ++this.cmd.sequenceNo;
this.stream.writeBuf(this.buf.subarray(0, this.pos), this.cmd);
this.stream.flush(true, this.cmd);
this.cmdLength += this.pos - 4;
if (commandEnd && this.pos === MAX_BUFFER_SIZE) {
//if last packet fill the max size, must send an empty com to indicate that command end.
this.writeEmptyPacket();
}
this.buf = this.createBufferWithMinSize(remainingLen);
this.pos = 4;
}
createBufferWithMinSize(remainingLen) {
let newCapacity;
if (remainingLen + 4 < SMALL_BUFFER_SIZE) {
newCapacity = SMALL_BUFFER_SIZE;
} else if (remainingLen + 4 < MEDIUM_BUFFER_SIZE) {
newCapacity = MEDIUM_BUFFER_SIZE;
} else if (remainingLen + 4 < LARGE_BUFFER_SIZE) {
newCapacity = LARGE_BUFFER_SIZE;
} else if (remainingLen + 4 < BIG_BUFFER_SIZE) {
newCapacity = BIG_BUFFER_SIZE;
} else {
newCapacity = MAX_BUFFER_SIZE;
}
return Buffer.allocUnsafe(newCapacity);
}
fastFlushDebug(cmd, packet) {
this.stream.writeBuf(packet, cmd);
this.stream.flush(true, cmd);
this.cmdLength += packet.length;
this.opts.logger.network(
`==> conn:${this.info.threadId ? this.info.threadId : -1} ${
cmd.constructor.name + '(0,' + packet.length + ')'
}\n${Utils.log(this.opts, packet, 0, packet.length)}`
);
this.cmdLength = 0;
this.markPos = -1;
}
fastFlushBasic(cmd, packet) {
this.stream.writeBuf(packet, cmd);
this.stream.flush(true, cmd);
this.cmdLength = 0;
this.markPos = -1;
}
writeEmptyPacket() {
const emptyBuf = Buffer.from([0x00, 0x00, 0x00, ++this.cmd.sequenceNo]);
if (this.debug) {
this.opts.logger.network(
`==> conn:${this.info.threadId ? this.info.threadId : -1} ${this.cmd.constructor.name}(0,4)\n${Utils.log(
this.opts,
emptyBuf,
0,
4
)}`
);
}
this.stream.writeBuf(emptyBuf, this.cmd);
this.stream.flush(true, this.cmd);
this.cmdLength = 0;
}
}
module.exports = PacketOutputStream;
+600
View File
@@ -0,0 +1,600 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const Errors = require('../misc/errors');
/**
* Object to easily parse buffer.
* Packet are MUTABLE (buffer are changed, to avoid massive packet object creation).
* Use clone() in case immutability is required
*
*/
class Packet {
update(buf, pos, end) {
this.buf = buf;
this.pos = pos;
this.end = end;
return this;
}
skip(n) {
this.pos += n;
}
readGeometry(defaultVal) {
const geoBuf = this.readBufferLengthEncoded();
if (geoBuf === null || geoBuf.length === 0) {
return defaultVal;
}
let geoPos = 4;
return readGeometryObject(false);
function parseCoordinates(byteOrder) {
geoPos += 16;
const x = byteOrder ? geoBuf.readDoubleLE(geoPos - 16) : geoBuf.readDoubleBE(geoPos - 16);
const y = byteOrder ? geoBuf.readDoubleLE(geoPos - 8) : geoBuf.readDoubleBE(geoPos - 8);
return [x, y];
}
function readGeometryObject(inner) {
const byteOrder = geoBuf[geoPos++];
const wkbType = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
geoPos += 4;
switch (wkbType) {
case 1: //wkbPoint
const coords = parseCoordinates(byteOrder);
if (inner) return coords;
return {
type: 'Point',
coordinates: coords
};
case 2: //wkbLineString
const pointNumber = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
geoPos += 4;
let coordinates = [];
for (let i = 0; i < pointNumber; i++) {
coordinates.push(parseCoordinates(byteOrder));
}
if (inner) return coordinates;
return {
type: 'LineString',
coordinates: coordinates
};
case 3: //wkbPolygon
let polygonCoordinates = [];
const numRings = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
geoPos += 4;
for (let ring = 0; ring < numRings; ring++) {
const pointNumber = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
geoPos += 4;
let linesCoordinates = [];
for (let i = 0; i < pointNumber; i++) {
linesCoordinates.push(parseCoordinates(byteOrder));
}
polygonCoordinates.push(linesCoordinates);
}
if (inner) return polygonCoordinates;
return {
type: 'Polygon',
coordinates: polygonCoordinates
};
case 4: //wkbMultiPoint
return {
type: 'MultiPoint',
coordinates: parseGeomArray(byteOrder, true)
};
case 5: //wkbMultiLineString
return {
type: 'MultiLineString',
coordinates: parseGeomArray(byteOrder, true)
};
case 6: //wkbMultiPolygon
return {
type: 'MultiPolygon',
coordinates: parseGeomArray(byteOrder, true)
};
case 7: //wkbGeometryCollection
return {
type: 'GeometryCollection',
geometries: parseGeomArray(byteOrder, false)
};
}
return null;
}
function parseGeomArray(byteOrder, inner) {
let coordinates = [];
const number = byteOrder ? geoBuf.readInt32LE(geoPos) : geoBuf.readInt32BE(geoPos);
geoPos += 4;
for (let i = 0; i < number; i++) {
coordinates.push(readGeometryObject(inner));
}
return coordinates;
}
}
peek() {
return this.buf[this.pos];
}
remaining() {
return this.end - this.pos > 0;
}
readInt8() {
const val = this.buf[this.pos++];
return val | ((val & (2 ** 7)) * 0x1fffffe);
}
readUInt8() {
return this.buf[this.pos++];
}
readInt16() {
this.pos += 2;
const first = this.buf[this.pos - 2];
const last = this.buf[this.pos - 1];
const val = first + last * 2 ** 8;
return val | ((val & (2 ** 15)) * 0x1fffe);
}
readUInt16() {
this.pos += 2;
return this.buf[this.pos - 2] + this.buf[this.pos - 1] * 2 ** 8;
}
readInt24() {
const first = this.buf[this.pos];
const last = this.buf[this.pos + 2];
const val = first + this.buf[this.pos + 1] * 2 ** 8 + last * 2 ** 16;
this.pos += 3;
return val | ((val & (2 ** 23)) * 0x1fe);
}
readUInt24() {
this.pos += 3;
return this.buf[this.pos - 3] + this.buf[this.pos - 2] * 2 ** 8 + this.buf[this.pos - 1] * 2 ** 16;
}
readUInt32() {
this.pos += 4;
return (
this.buf[this.pos - 4] +
this.buf[this.pos - 3] * 2 ** 8 +
this.buf[this.pos - 2] * 2 ** 16 +
this.buf[this.pos - 1] * 2 ** 24
);
}
readInt32() {
this.pos += 4;
return (
this.buf[this.pos - 4] +
this.buf[this.pos - 3] * 2 ** 8 +
this.buf[this.pos - 2] * 2 ** 16 +
(this.buf[this.pos - 1] << 24)
);
}
readBigInt64() {
const val = this.buf.readBigInt64LE(this.pos);
this.pos += 8;
return val;
}
readBigUInt64() {
const val = this.buf.readBigUInt64LE(this.pos);
this.pos += 8;
return val;
}
/**
* Metadata are length encoded, but cannot have length > 256, so simplified readUnsignedLength
* @returns {number}
*/
readMetadataLength() {
const type = this.buf[this.pos++];
if (type < 0xfb) return type;
return this.readUInt16();
}
readUnsignedLength() {
const type = this.buf[this.pos++];
if (type < 0xfb) return type;
switch (type) {
case 0xfb:
return null;
case 0xfc:
//readUInt16();
this.pos += 2;
return this.buf[this.pos - 2] + this.buf[this.pos - 1] * 2 ** 8;
case 0xfd:
//readUInt24();
this.pos += 3;
return this.buf[this.pos - 3] + this.buf[this.pos - 2] * 2 ** 8 + this.buf[this.pos - 1] * 2 ** 16;
case 0xfe:
// limitation to BigInt signed value
return Number(this.readBigInt64());
}
}
readBuffer(len) {
this.pos += len;
return this.buf.subarray(this.pos - len, this.pos);
}
readBufferRemaining() {
let b = this.buf.subarray(this.pos, this.end);
this.pos = this.end;
return b;
}
readBufferLengthEncoded() {
const len = this.readUnsignedLength();
if (len === null) return null;
this.pos += len;
return this.buf.subarray(this.pos - len, this.pos);
}
readStringNullEnded() {
let initialPosition = this.pos;
let cnt = 0;
while (this.remaining() > 0 && this.buf[this.pos++] !== 0) {
cnt++;
}
return this.buf.toString(undefined, initialPosition, initialPosition + cnt);
}
/**
* Return unsigned Bigint.
*
* Could be used for reading other kinds of value than InsertId, if reading possible null value
* @returns {bigint}
*/
readInsertId() {
const type = this.buf[this.pos++];
if (type < 0xfb) return BigInt(type);
switch (type) {
case 0xfc:
this.pos += 2;
return BigInt(this.buf[this.pos - 2] + this.buf[this.pos - 1] * 2 ** 8);
case 0xfd:
this.pos += 3;
return BigInt(this.buf[this.pos - 3] + this.buf[this.pos - 2] * 2 ** 8 + this.buf[this.pos - 1] * 2 ** 16);
case 0xfe:
return this.readBigInt64();
}
}
readAsciiStringLengthEncoded() {
const len = this.readUnsignedLength();
if (len === null) return null;
this.pos += len;
return this.buf.toString('ascii', this.pos - len, this.pos);
}
readStringLengthEncoded() {
throw new Error('code is normally superseded by Node encoder or Iconv depending on charset used');
}
readBigIntLengthEncoded() {
const len = this.buf[this.pos++];
// fast-path: if length encoded is < to 16, value is in safe integer range, using atoi
if (len < 16) {
return BigInt(this._atoi(len));
}
if (len === 0xfb) return null;
return this.readBigIntFromLen(len);
}
readBigIntFromLen(len) {
// atoll
let result = 0n;
let negate = false;
let begin = this.pos;
if (len > 0 && this.buf[begin] === 45) {
//minus sign
negate = true;
begin++;
}
for (; begin < this.pos + len; begin++) {
result = result * 10n + BigInt(this.buf[begin] - 48);
}
this.pos += len;
return negate ? -1n * result : result;
}
readDecimalLengthEncoded() {
const len = this.buf[this.pos++];
if (len === 0xfb) return null;
this.pos += len;
return this.buf.toString('ascii', this.pos - len, this.pos);
}
readDate() {
const len = this.buf[this.pos++];
if (len === 0xfb) return null;
let res = [];
let value = 0;
let initPos = this.pos;
this.pos += len;
while (initPos < this.pos) {
const char = this.buf[initPos++];
if (char === 45) {
//minus separator
res.push(value);
value = 0;
} else {
value = value * 10 + char - 48;
}
}
res.push(value);
//handle zero-date as null
if (res[0] === 0 && res[1] === 0 && res[2] === 0) return null;
return new Date(res[0], res[1] - 1, res[2]);
}
readBinaryDate(opts) {
const len = this.buf[this.pos++];
let year = 0;
let month = 0;
let day = 0;
if (len > 0) {
year = this.readInt16();
if (len > 2) {
month = this.readUInt8() - 1;
if (len > 3) {
day = this.readUInt8();
}
}
}
if (year === 0 && month === 0 && day === 0) return opts.dateStrings ? '0000-00-00' : null;
if (opts.dateStrings) {
return `${appendZero(year, 4)}-${appendZero(month + 1, 2)}-${appendZero(day, 2)}`;
}
//handle zero-date as null
return new Date(year, month, day);
}
readDateTime() {
const len = this.buf[this.pos++];
if (len === 0xfb) return null;
this.pos += len;
const str = this.buf.toString('ascii', this.pos - len, this.pos);
if (str.startsWith('0000-00-00 00:00:00')) return null;
return new Date(str);
}
readBinaryDateTime() {
const len = this.buf[this.pos++];
let year = 0;
let month = 0;
let day = 0;
let hour = 0;
let min = 0;
let sec = 0;
let microSec = 0;
if (len > 0) {
year = this.readInt16();
if (len > 2) {
month = this.readUInt8();
if (len > 3) {
day = this.readUInt8();
if (len > 4) {
hour = this.readUInt8();
min = this.readUInt8();
sec = this.readUInt8();
if (len > 7) {
microSec = this.readUInt32();
}
}
}
}
}
//handle zero-date as null
if (year === 0 && month === 0 && day === 0 && hour === 0 && min === 0 && sec === 0 && microSec === 0) return null;
return new Date(year, month - 1, day, hour, min, sec, microSec / 1000);
}
readBinaryDateTimeAsString(scale) {
const len = this.buf[this.pos++];
let year = 0;
let month = 0;
let day = 0;
let hour = 0;
let min = 0;
let sec = 0;
let microSec = 0;
if (len > 0) {
year = this.readInt16();
if (len > 2) {
month = this.readUInt8();
if (len > 3) {
day = this.readUInt8();
if (len > 4) {
hour = this.readUInt8();
min = this.readUInt8();
sec = this.readUInt8();
if (len > 7) {
microSec = this.readUInt32();
}
}
}
}
}
//handle zero-date as null
if (year === 0 && month === 0 && day === 0 && hour === 0 && min === 0 && sec === 0 && microSec === 0)
return '0000-00-00 00:00:00' + (scale > 0 ? '.000000'.substring(0, scale + 1) : '');
return (
appendZero(year, 4) +
'-' +
appendZero(month, 2) +
'-' +
appendZero(day, 2) +
' ' +
appendZero(hour, 2) +
':' +
appendZero(min, 2) +
':' +
appendZero(sec, 2) +
(microSec > 0
? scale > 0
? '.' + appendZero(microSec, 6).substring(0, scale)
: '.' + appendZero(microSec, 6)
: scale > 0
? '.' + appendZero(microSec, 6).substring(0, scale)
: '')
);
}
readBinaryTime() {
const len = this.buf[this.pos++];
let negate = false;
let hour = 0;
let min = 0;
let sec = 0;
let microSec = 0;
if (len > 0) {
negate = this.buf[this.pos++] === 1;
hour = this.readUInt32() * 24 + this.readUInt8();
min = this.readUInt8();
sec = this.readUInt8();
if (len > 8) {
microSec = this.readUInt32();
}
}
let val = appendZero(hour, 2) + ':' + appendZero(min, 2) + ':' + appendZero(sec, 2);
if (microSec > 0) {
val += '.' + appendZero(microSec, 6);
}
if (negate) return '-' + val;
return val;
}
readFloat() {
const val = this.buf.readFloatLE(this.pos);
this.pos += 4;
return val;
}
readDouble() {
const val = this.buf.readDoubleLE(this.pos);
this.pos += 8;
return val;
}
readIntLengthEncoded() {
const len = this.buf[this.pos++];
if (len === 0xfb) return null;
return this._atoi(len);
}
_atoi(len) {
let result = 0;
let negate = false;
let begin = this.pos;
if (len > 0 && this.buf[begin] === 45) {
//minus sign
negate = true;
begin++;
}
for (; begin < this.pos + len; begin++) {
result = result * 10 + (this.buf[begin] - 48);
}
this.pos += len;
return negate ? -1 * result : result;
}
readFloatLengthCoded() {
const len = this.readUnsignedLength();
if (len === null) return null;
this.pos += len;
return +this.buf.toString('ascii', this.pos - len, this.pos);
}
skipLengthCodedNumber() {
const type = this.buf[this.pos++];
switch (type) {
case 251:
return;
case 252:
this.pos += 2 + (0xffff & (this.buf[this.pos] + (this.buf[this.pos + 1] << 8)));
return;
case 253:
this.pos +=
3 + (0xffffff & (this.buf[this.pos] + (this.buf[this.pos + 1] << 8) + (this.buf[this.pos + 2] << 16)));
return;
case 254:
this.pos += 8 + Number(this.buf.readBigUInt64LE(this.pos));
return;
default:
this.pos += type;
return;
}
}
length() {
return this.end - this.pos;
}
subPacketLengthEncoded(len) {}
/**
* Parse ERR_Packet : https://mariadb.com/kb/en/library/err_packet/
*
* @param info current connection info
* @param sql command sql
* @param stack additional stack trace
* @returns {Error}
*/
readError(info, sql, stack) {
this.skip(1);
let errno = this.readUInt16();
let sqlState;
let msg;
// check '#'
if (this.peek() === 0x23) {
// skip '#'
this.skip(6);
sqlState = this.buf.toString(undefined, this.pos - 5, this.pos);
msg = this.readStringNullEnded();
} else {
// pre 4.1 format
sqlState = 'HY000';
msg = this.buf.toString(undefined, this.pos, this.end);
}
let fatal = sqlState.startsWith('08') || sqlState === '70100';
return Errors.createError(msg, errno, info, sqlState, sql, fatal, stack);
}
}
const appendZero = (val, len) => {
let st = val.toString();
while (st.length < len) {
st = '0' + st;
}
return st;
};
module.exports = Packet;
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const LRU = require('lru-cache');
/**
* LRU prepare cache for storing prepared SQL statements
*
* This class provides caching functionality for prepared statements
* using a Least Recently Used (LRU) cache strategy.
*/
class LruPrepareCache {
#lruCache;
#info;
/**
* Creates a new LRU prepare cache
*
* @param {Object} info - Database connection information
* @param {number} prepareCacheLength - Maximum number of prepared statements to cache
*/
constructor(info, prepareCacheLength) {
if (!Number.isInteger(prepareCacheLength) || prepareCacheLength <= 0) {
throw new TypeError('prepareCacheLength must be a positive integer');
}
this.#info = info;
this.#lruCache = new LRU.LRUCache({
max: prepareCacheLength,
dispose: (value, key) => value.unCache()
});
}
/**
* Gets a cached prepared statement
*
* @param {string} sql - SQL statement to retrieve
* @returns {Object|null} Cached prepared statement or null if not found
*/
get(sql) {
const key = this.#info.database + '|' + sql;
const cachedItem = this.#lruCache.get(key);
if (cachedItem) {
return cachedItem.incrementUse();
}
return null;
}
/**
* Adds a prepared statement to the cache
*
* @param {string} sql - SQL statement
* @param {Object} cache - Prepared statement object
* @returns {void}
*/
set(sql, cache) {
const key = this.#info.database + '|' + sql;
this.#lruCache.set(key, cache);
}
/**
* Provides a string representation of the cache contents
*
* @returns {string} String representation of cache
*/
toString() {
const keys = [...this.#lruCache.keys()];
const keyStr = keys.length ? keys.map((key) => `[${key}]`).join(',') : '';
return `info{cache:${keyStr}}`;
}
/**
* Clears all cached prepared statements
*
* @returns {void}
*/
reset() {
this.#lruCache.clear();
}
}
module.exports = LruPrepareCache;
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
class ConnectionInformation {
#redirectFct;
constructor(opts, redirectFct) {
this.threadId = -1;
this.status = null;
this.serverVersion = null;
this.serverCapabilities = null;
this.database = opts.database;
this.port = opts.port;
this.#redirectFct = redirectFct;
this.redirectRequest = null;
}
hasMinVersion(major, minor, patch) {
if (!this.serverVersion) throw new Error('cannot know if server version until connection is established');
if (!major) throw new Error('a major version must be set');
if (!minor) minor = 0;
if (!patch) patch = 0;
let ver = this.serverVersion;
return (
ver.major > major ||
(ver.major === major && ver.minor > minor) ||
(ver.major === major && ver.minor === minor && ver.patch >= patch)
);
}
redirect(value, resolve) {
return this.#redirectFct(value, resolve);
}
isMariaDB() {
if (!this.serverVersion) throw new Error('cannot know if server is MariaDB until connection is established');
return this.serverVersion.mariaDb;
}
/**
* Parse raw info to set server major/minor/patch values
* @param info
*/
static parseVersionString(info) {
let car;
let offset = 0;
let type = 0;
let val = 0;
for (; offset < info.serverVersion.raw.length; offset++) {
car = info.serverVersion.raw.charCodeAt(offset);
if (car < 48 || car > 57) {
switch (type) {
case 0:
info.serverVersion.major = val;
break;
case 1:
info.serverVersion.minor = val;
break;
case 2:
info.serverVersion.patch = val;
return;
}
type++;
val = 0;
} else {
val = val * 10 + car - 48;
}
}
//serverVersion finished by number like "5.5.57", assign patchVersion
if (type === 2) info.serverVersion.patch = val;
}
}
module.exports = ConnectionInformation;
+166
View File
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const ErrorCodes = require('../const/error-code');
class SqlError extends Error {
constructor(msg, sql, fatal, info, sqlState, errno, additionalStack, addHeader = undefined, cause) {
super(
(addHeader !== false
? `(conn:${info && info.threadId ? info.threadId : -1}, no: ${errno ? errno : -1}, SQLState: ${sqlState}) `
: '') +
msg +
(sql ? '\nsql: ' + sql : ''),
cause
);
this.name = 'SqlError';
this.sqlMessage = msg;
this.sql = sql;
this.fatal = fatal;
this.errno = errno;
this.sqlState = sqlState;
if (errno > 45000 && errno < 46000) {
//driver error
this.code = errByNo[errno] || 'UNKNOWN';
} else {
this.code = ErrorCodes.codes[this.errno] || 'UNKNOWN';
}
if (additionalStack) {
//adding caller stack, removing initial "Error:\n"
this.stack += '\n From event:\n' + additionalStack.substring(additionalStack.indexOf('\n') + 1);
}
}
get text() {
return this.sqlMessage;
}
}
/**
* Error factory, so error get connection information.
*
* @param msg current error message
* @param errno error number
* @param info connection information
* @param sqlState sql state
* @param sql sql command
* @param fatal is error fatal
* @param additionalStack additional stack trace to see
* @param addHeader add connection information
* @param cause add cause
* @returns {Error} the error
*/
module.exports.createError = function (
msg,
errno,
info = null,
sqlState = 'HY000',
sql = null,
fatal = false,
additionalStack = undefined,
addHeader = undefined,
cause = undefined
) {
if (cause) return new SqlError(msg, sql, fatal, info, sqlState, errno, additionalStack, addHeader, { cause: cause });
return new SqlError(msg, sql, fatal, info, sqlState, errno, additionalStack, addHeader, cause);
};
/**
* Fatal error factory, so error get connection information.
*
* @param msg current error message
* @param errno error number
* @param info connection information
* @param sqlState sql state
* @param sql sql command
* @param additionalStack additional stack trace to see
* @param addHeader add connection information
* @returns {Error} the error
*/
module.exports.createFatalError = function (
msg,
errno,
info = null,
sqlState = '08S01',
sql = null,
additionalStack = undefined,
addHeader = undefined
) {
return new SqlError(msg, sql, true, info, sqlState, errno, additionalStack, addHeader);
};
/********************************************************************************
* Driver specific errors
********************************************************************************/
module.exports.ER_CONNECTION_ALREADY_CLOSED = 45001;
module.exports.ER_MYSQL_CHANGE_USER_BUG = 45003;
module.exports.ER_CMD_NOT_EXECUTED_DESTROYED = 45004;
module.exports.ER_NULL_CHAR_ESCAPEID = 45005;
module.exports.ER_NULL_ESCAPEID = 45006;
module.exports.ER_NOT_IMPLEMENTED_FORMAT = 45007;
module.exports.ER_NODE_NOT_SUPPORTED_TLS = 45008;
module.exports.ER_SOCKET_UNEXPECTED_CLOSE = 45009;
module.exports.ER_UNEXPECTED_PACKET = 45011;
module.exports.ER_CONNECTION_TIMEOUT = 45012;
module.exports.ER_CMD_CONNECTION_CLOSED = 45013;
module.exports.ER_CHANGE_USER_BAD_PACKET = 45014;
module.exports.ER_PING_BAD_PACKET = 45015;
module.exports.ER_MISSING_PARAMETER = 45016;
module.exports.ER_PARAMETER_UNDEFINED = 45017;
module.exports.ER_PLACEHOLDER_UNDEFINED = 45018;
module.exports.ER_SOCKET = 45019;
module.exports.ER_EOF_EXPECTED = 45020;
module.exports.ER_LOCAL_INFILE_DISABLED = 45021;
module.exports.ER_LOCAL_INFILE_NOT_READABLE = 45022;
module.exports.ER_SERVER_SSL_DISABLED = 45023;
module.exports.ER_AUTHENTICATION_BAD_PACKET = 45024;
module.exports.ER_AUTHENTICATION_PLUGIN_NOT_SUPPORTED = 45025;
module.exports.ER_SOCKET_TIMEOUT = 45026;
module.exports.ER_POOL_ALREADY_CLOSED = 45027;
module.exports.ER_GET_CONNECTION_TIMEOUT = 45028;
module.exports.ER_SETTING_SESSION_ERROR = 45029;
module.exports.ER_INITIAL_SQL_ERROR = 45030;
module.exports.ER_BATCH_WITH_NO_VALUES = 45031;
module.exports.ER_RESET_BAD_PACKET = 45032;
module.exports.ER_WRONG_IANA_TIMEZONE = 45033;
module.exports.ER_LOCAL_INFILE_WRONG_FILENAME = 45034;
module.exports.ER_ADD_CONNECTION_CLOSED_POOL = 45035;
module.exports.ER_WRONG_AUTO_TIMEZONE = 45036;
module.exports.ER_CLOSING_POOL = 45037;
module.exports.ER_TIMEOUT_NOT_SUPPORTED = 45038;
module.exports.ER_INITIAL_TIMEOUT_ERROR = 45039;
module.exports.ER_DUPLICATE_FIELD = 45040;
module.exports.ER_PING_TIMEOUT = 45042;
module.exports.ER_BAD_PARAMETER_VALUE = 45043;
module.exports.ER_CANNOT_RETRIEVE_RSA_KEY = 45044;
module.exports.ER_MINIMUM_NODE_VERSION_REQUIRED = 45045;
module.exports.ER_MAX_ALLOWED_PACKET = 45046;
module.exports.ER_NOT_SUPPORTED_AUTH_PLUGIN = 45047;
module.exports.ER_COMPRESSION_NOT_SUPPORTED = 45048;
module.exports.ER_UNDEFINED_SQL = 45049;
module.exports.ER_PARSING_PRECISION = 45050;
module.exports.ER_PREPARE_CLOSED = 45051;
module.exports.ER_MISSING_SQL_PARAMETER = 45052;
module.exports.ER_MISSING_SQL_FILE = 45053;
module.exports.ER_SQL_FILE_ERROR = 45054;
module.exports.ER_MISSING_DATABASE_PARAMETER = 45055;
module.exports.ER_SELF_SIGNED = 45056;
module.exports.ER_SELF_SIGNED_NO_PWD = 45057;
module.exports.ER_PRIVATE_FIELDS_USE = 45058;
module.exports.ER_TLS_IDENTITY_ERROR = 45059;
module.exports.ER_POOL_NOT_INITIALIZED = 45060;
module.exports.ER_POOL_NO_CONNECTION = 45061;
module.exports.ER_SELF_SIGNED_BAD_PLUGIN = 45062;
const keys = Object.keys(module.exports);
const errByNo = {};
for (let i = 0; i < keys.length; i++) {
const keyName = keys[i];
if (keyName !== 'createError') {
errByNo[module.exports[keyName]] = keyName;
}
}
module.exports.SqlError = SqlError;
+531
View File
@@ -0,0 +1,531 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
const Errors = require('../misc/errors');
const State = {
Normal: 1 /* inside query */,
String: 2 /* inside string */,
SlashStarComment: 3 /* inside slash-star comment */,
Escape: 4 /* found backslash */,
EOLComment: 5 /* # comment, or // comment, or -- comment */,
Backtick: 6 /* found backtick */,
Placeholder: 7 /* found placeholder */
};
const SLASH_BYTE = '/'.charCodeAt(0);
const STAR_BYTE = '*'.charCodeAt(0);
const BACKSLASH_BYTE = '\\'.charCodeAt(0);
const HASH_BYTE = '#'.charCodeAt(0);
const MINUS_BYTE = '-'.charCodeAt(0);
const LINE_FEED_BYTE = '\n'.charCodeAt(0);
const DBL_QUOTE_BYTE = '"'.charCodeAt(0);
const QUOTE_BYTE = "'".charCodeAt(0);
const RADICAL_BYTE = '`'.charCodeAt(0);
const QUESTION_MARK_BYTE = '?'.charCodeAt(0);
const COLON_BYTE = ':'.charCodeAt(0);
const SEMICOLON_BYTE = ';'.charCodeAt(0);
/**
* Set question mark position (question mark).
* Question mark in comment are not taken in account
*
* @returns {Array} question mark position
*/
module.exports.splitQuery = function (query) {
let paramPositions = [];
let state = State.Normal;
let lastChar = 0x00;
let singleQuotes = false;
let currentChar;
const len = query.length;
for (let i = 0; i < len; i++) {
currentChar = query[i];
if (
state === State.Escape &&
!((currentChar === QUOTE_BYTE && singleQuotes) || (currentChar === DBL_QUOTE_BYTE && !singleQuotes))
) {
state = State.String;
lastChar = currentChar;
continue;
}
switch (currentChar) {
case STAR_BYTE:
if (state === State.Normal && lastChar === SLASH_BYTE) {
state = State.SlashStarComment;
}
break;
case SLASH_BYTE:
if (state === State.SlashStarComment && lastChar === STAR_BYTE) {
state = State.Normal;
} else if (state === State.Normal && lastChar === SLASH_BYTE) {
state = State.EOLComment;
}
break;
case HASH_BYTE:
if (state === State.Normal) {
state = State.EOLComment;
}
break;
case MINUS_BYTE:
if (state === State.Normal && lastChar === MINUS_BYTE) {
state = State.EOLComment;
}
break;
case LINE_FEED_BYTE:
if (state === State.EOLComment) {
state = State.Normal;
}
break;
case DBL_QUOTE_BYTE:
if (state === State.Normal) {
state = State.String;
singleQuotes = false;
} else if (state === State.String && !singleQuotes) {
state = State.Normal;
} else if (state === State.Escape) {
state = State.String;
}
break;
case QUOTE_BYTE:
if (state === State.Normal) {
state = State.String;
singleQuotes = true;
} else if (state === State.String && singleQuotes) {
state = State.Normal;
} else if (state === State.Escape) {
state = State.String;
}
break;
case BACKSLASH_BYTE:
if (state === State.String) {
state = State.Escape;
}
break;
case QUESTION_MARK_BYTE:
if (state === State.Normal) {
paramPositions.push(i, ++i);
}
break;
case RADICAL_BYTE:
if (state === State.Backtick) {
state = State.Normal;
} else if (state === State.Normal) {
state = State.Backtick;
}
break;
}
lastChar = currentChar;
}
return paramPositions;
};
/**
* Split query according to parameters using placeholder.
*
* @param query query bytes
* @param info connection information
* @param initialValues placeholder object
* @param displaySql display sql function
* @returns {{paramPositions: Array, values: Array}}
*/
module.exports.splitQueryPlaceholder = function (query, info, initialValues, displaySql) {
let placeholderValues = Object.assign({}, initialValues);
let paramPositions = [];
let values = [];
let state = State.Normal;
let lastChar = 0x00;
let singleQuotes = false;
let car;
const len = query.length;
for (let i = 0; i < len; i++) {
car = query[i];
if (
state === State.Escape &&
!((car === QUOTE_BYTE && singleQuotes) || (car === DBL_QUOTE_BYTE && !singleQuotes))
) {
state = State.String;
lastChar = car;
continue;
}
switch (car) {
case STAR_BYTE:
if (state === State.Normal && lastChar === SLASH_BYTE) {
state = State.SlashStarComment;
}
break;
case SLASH_BYTE:
if (state === State.SlashStarComment && lastChar === STAR_BYTE) {
state = State.Normal;
} else if (state === State.Normal && lastChar === SLASH_BYTE) {
state = State.EOLComment;
}
break;
case HASH_BYTE:
if (state === State.Normal) {
state = State.EOLComment;
}
break;
case MINUS_BYTE:
if (state === State.Normal && lastChar === MINUS_BYTE) {
state = State.EOLComment;
}
break;
case LINE_FEED_BYTE:
if (state === State.EOLComment) {
state = State.Normal;
}
break;
case DBL_QUOTE_BYTE:
if (state === State.Normal) {
state = State.String;
singleQuotes = false;
} else if (state === State.String && !singleQuotes) {
state = State.Normal;
} else if (state === State.Escape) {
state = State.String;
}
break;
case QUOTE_BYTE:
if (state === State.Normal) {
state = State.String;
singleQuotes = true;
} else if (state === State.String && singleQuotes) {
state = State.Normal;
} else if (state === State.Escape) {
state = State.String;
}
break;
case BACKSLASH_BYTE:
if (state === State.String) {
state = State.Escape;
}
break;
case QUESTION_MARK_BYTE:
if (state === State.Normal) {
const key = Object.keys(placeholderValues)[0];
values.push(placeholderValues[key]);
delete placeholderValues[key];
paramPositions.push(i);
paramPositions.push(++i);
}
break;
case COLON_BYTE:
if (state === State.Normal) {
let j = 1;
while (
(i + j < len && query[i + j] >= '0'.charCodeAt(0) && query[i + j] <= '9'.charCodeAt(0)) ||
(query[i + j] >= 'A'.charCodeAt(0) && query[i + j] <= 'Z'.charCodeAt(0)) ||
(query[i + j] >= 'a'.charCodeAt(0) && query[i + j] <= 'z'.charCodeAt(0)) ||
query[i + j] === '-'.charCodeAt(0) ||
query[i + j] === '_'.charCodeAt(0)
) {
j++;
}
paramPositions.push(i, i + j);
const placeholderName = query.toString('utf8', i + 1, i + j);
i += j;
let val;
if (placeholderName in placeholderValues) {
val = placeholderValues[placeholderName];
delete placeholderValues[placeholderName];
} else {
// value is already used
val = initialValues[placeholderName];
}
if (val === undefined) {
throw Errors.createError(
`Placeholder '${placeholderName}' is not defined`,
Errors.ER_PLACEHOLDER_UNDEFINED,
info,
'HY000',
displaySql.call()
);
}
values.push(val);
}
break;
case RADICAL_BYTE:
if (state === State.Backtick) {
state = State.Normal;
} else if (state === State.Normal) {
state = State.Backtick;
}
break;
}
lastChar = car;
}
return { paramPositions: paramPositions, values: values };
};
module.exports.searchPlaceholder = function (sql) {
let sqlPlaceHolder = '';
let placeHolderIndex = [];
let state = State.Normal;
let lastChar = '\0';
let singleQuotes = false;
let lastParameterPosition = 0;
let idx = 0;
let car = sql.charAt(idx++);
let placeholderName;
while (car !== '') {
if (state === State.Escape && !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))) {
state = State.String;
lastChar = car;
car = sql.charAt(idx++);
continue;
}
switch (car) {
case '*':
if (state === State.Normal && lastChar === '/') state = State.SlashStarComment;
break;
case '/':
if (state === State.SlashStarComment && lastChar === '*') state = State.Normal;
break;
case '#':
if (state === State.Normal) state = State.EOLComment;
break;
case '-':
if (state === State.Normal && lastChar === '-') {
state = State.EOLComment;
}
break;
case '\n':
if (state === State.EOLComment) {
state = State.Normal;
}
break;
case '"':
if (state === State.Normal) {
state = State.String;
singleQuotes = false;
} else if (state === State.String && !singleQuotes) {
state = State.Normal;
} else if (state === State.Escape && !singleQuotes) {
state = State.String;
}
break;
case "'":
if (state === State.Normal) {
state = State.String;
singleQuotes = true;
} else if (state === State.String && singleQuotes) {
state = State.Normal;
singleQuotes = false;
} else if (state === State.Escape && singleQuotes) {
state = State.String;
}
break;
case '\\':
if (state === State.String) state = State.Escape;
break;
case ':':
if (state === State.Normal) {
sqlPlaceHolder += sql.substring(lastParameterPosition, idx - 1) + '?';
placeholderName = '';
while (
((car = sql.charAt(idx++)) !== '' && car >= '0' && car <= '9') ||
(car >= 'A' && car <= 'Z') ||
(car >= 'a' && car <= 'z') ||
car === '-' ||
car === '_'
) {
placeholderName += car;
}
idx--;
placeHolderIndex.push(placeholderName);
lastParameterPosition = idx;
}
break;
case '`':
if (state === State.Backtick) {
state = State.Normal;
} else if (state === State.Normal) {
state = State.Backtick;
}
}
lastChar = car;
car = sql.charAt(idx++);
}
if (lastParameterPosition === 0) {
sqlPlaceHolder = sql;
} else {
sqlPlaceHolder += sql.substring(lastParameterPosition);
}
return { sql: sqlPlaceHolder, placeHolderIndex: placeHolderIndex };
};
/**
* Ensure that filename requested by server corresponds to query
* protocol : https://mariadb.com/kb/en/library/local_infile-packet/
*
* @param sql query
* @param parameters parameters if any
* @param fileName server requested file
* @returns {boolean} is filename corresponding to query
*/
module.exports.validateFileName = function (sql, parameters, fileName) {
// in case of windows, file name in query are escaped
// so for example LOAD DATA LOCAL INFILE 'C:\\Temp\\myFile.txt' ...
// but server return 'C:\Temp\myFile.txt'
// so with regex escaped, must test LOAD DATA LOCAL INFILE 'C:\\\\Temp\\\\myFile.txt'
let queryValidator = new RegExp(
"^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+DATA\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+'" +
fileName.replace(/\\/g, '\\\\\\\\').replace('.', '\\.') +
"'",
'i'
);
if (queryValidator.test(sql)) return true;
if (parameters != null) {
queryValidator = new RegExp(
'^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+DATA\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+\\?',
'i'
);
if (queryValidator.test(sql) && parameters.length > 0) {
if (Array.isArray(parameters)) {
return parameters[0].toLowerCase() === fileName.toLowerCase();
}
return parameters.toLowerCase() === fileName.toLowerCase();
}
}
return false;
};
/**
* Parse commands from buffer, returns queries separated by ';'
* (last one is not parsed)
*
* @param bufState buffer
* @returns {*[]} array of queries contained in buffer
*/
module.exports.parseQueries = function (bufState) {
let state = State.Normal;
let lastChar = 0x00;
let currByte;
let queries = [];
let singleQuotes = false;
for (let i = bufState.offset; i < bufState.end; i++) {
currByte = bufState.buffer[i];
if (
state === State.Escape &&
!((currByte === QUOTE_BYTE && singleQuotes) || (currByte === DBL_QUOTE_BYTE && !singleQuotes))
) {
state = State.String;
lastChar = currByte;
continue;
}
switch (currByte) {
case STAR_BYTE:
if (state === State.Normal && lastChar === SLASH_BYTE) {
state = State.SlashStarComment;
}
break;
case SLASH_BYTE:
if (state === State.SlashStarComment && lastChar === STAR_BYTE) {
state = State.Normal;
} else if (state === State.Normal && lastChar === SLASH_BYTE) {
state = State.EOLComment;
}
break;
case HASH_BYTE:
if (state === State.Normal) {
state = State.EOLComment;
}
break;
case MINUS_BYTE:
if (state === State.Normal && lastChar === MINUS_BYTE) {
state = State.EOLComment;
}
break;
case LINE_FEED_BYTE:
if (state === State.EOLComment) {
state = State.Normal;
}
break;
case DBL_QUOTE_BYTE:
if (state === State.Normal) {
state = State.String;
singleQuotes = false;
} else if (state === State.String && !singleQuotes) {
state = State.Normal;
} else if (state === State.Escape) {
state = State.String;
}
break;
case QUOTE_BYTE:
if (state === State.Normal) {
state = State.String;
singleQuotes = true;
} else if (state === State.String && singleQuotes) {
state = State.Normal;
} else if (state === State.Escape) {
state = State.String;
}
break;
case BACKSLASH_BYTE:
if (state === State.String) {
state = State.Escape;
}
break;
case SEMICOLON_BYTE:
if (state === State.Normal) {
queries.push(bufState.buffer.toString('utf8', bufState.offset, i));
bufState.offset = i + 1;
}
break;
case RADICAL_BYTE:
if (state === State.Backtick) {
state = State.Normal;
} else if (state === State.Normal) {
state = State.Backtick;
}
break;
}
lastChar = currByte;
}
return queries;
};
+270
View File
@@ -0,0 +1,270 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
const hexArray = '0123456789ABCDEF'.split('');
const Errors = require('../misc/errors');
const Iconv = require('iconv-lite');
const TextEncoder = require('../cmd/encoder/text-encoder');
/**
* Write bytes/hexadecimal value of a byte array to a string.
* String output example :
* 38 00 00 00 03 63 72 65 61 74 65 20 74 61 62 6C 8....create tabl
* 65 20 42 6C 6F 62 54 65 73 74 63 6C 6F 62 74 65 e BlobTestclobte
* 73 74 32 20 28 73 74 72 6D 20 74 65 78 74 29 20 st2 (strm text)
* 43 48 41 52 53 45 54 20 75 74 66 38 CHARSET utf8
*/
module.exports.log = function (opts, buf, off, end, header) {
let out = [];
if (!buf) return '';
if (off === undefined || off === null) off = 0;
if (end === undefined || end === null) end = buf.length;
let asciiValue = new Array(16);
asciiValue[8] = ' ';
let useHeader = header !== undefined;
let offset = off || 0;
const maxLgh = Math.min(useHeader ? opts.debugLen - header.length : opts.debugLen, end - offset);
const isLimited = end - offset > maxLgh;
let byteValue;
let posHexa = 0;
let pos = 0;
out.push(
'+--------------------------------------------------+\n' +
'| 0 1 2 3 4 5 6 7 8 9 a b c d e f |\n' +
'+--------------------------------------------------+------------------+\n'
);
if (useHeader) {
while (pos < header.length) {
if (posHexa === 0) out.push('| ');
byteValue = header[pos++] & 0xff;
out.push(hexArray[byteValue >>> 4], hexArray[byteValue & 0x0f], ' ');
asciiValue[posHexa++] = byteValue > 31 && byteValue < 127 ? String.fromCharCode(byteValue) : '.';
if (posHexa === 8) out.push(' ');
}
}
pos = offset;
while (pos < maxLgh + offset) {
if (posHexa === 0) out.push('| ');
byteValue = buf[pos] & 0xff;
out.push(hexArray[byteValue >>> 4], hexArray[byteValue & 0x0f], ' ');
asciiValue[posHexa++] = byteValue > 31 && byteValue < 127 ? String.fromCharCode(byteValue) : '.';
if (posHexa === 8) out.push(' ');
if (posHexa === 16) {
out.push('| ', asciiValue.join(''), ' |\n');
posHexa = 0;
}
pos++;
}
let remaining = posHexa;
if (remaining > 0) {
if (remaining < 8) {
for (; remaining < 8; remaining++) {
out.push(' ');
asciiValue[posHexa++] = ' ';
}
out.push(' ');
}
for (; remaining < 16; remaining++) {
out.push(' ');
asciiValue[posHexa++] = ' ';
}
out.push('| ', asciiValue.join(''), isLimited ? ' |...\n' : ' |\n');
} else if (isLimited) {
out[out.length - 1] = ' |...\n';
}
out.push('+--------------------------------------------------+------------------+\n');
return out.join('');
};
module.exports.toHexString = (bytes) => {
return Array.from(bytes, (byte) => {
return ('0' + (byte & 0xff).toString(16)).slice(-2);
}).join('');
};
module.exports.escapeId = (opts, info, value) => {
if (!value || value === '') {
throw Errors.createError('Cannot escape empty ID value', Errors.ER_NULL_ESCAPEID, info, '0A000');
}
if (value.includes('\u0000')) {
throw Errors.createError(
'Cannot escape ID with null character (u0000)',
Errors.ER_NULL_CHAR_ESCAPEID,
info,
'0A000'
);
}
// always return escaped value, even when there is no special characters
// to permit working with reserved words
return '`' + value.replace(/`/g, '``') + '`';
};
const escapeParameters = (opts, info, value) => {
if (value == null) return 'NULL';
switch (typeof value) {
case 'boolean':
return value ? 'true' : 'false';
case 'bigint':
case 'number':
return `${value}`;
case 'object':
if (Object.prototype.toString.call(value) === '[object Date]') {
return TextEncoder.getFixedFormatDate(value);
} else if (Buffer.isBuffer(value)) {
let stValue;
if (Buffer.isEncoding(info.collation.charset)) {
stValue = value.toString(info.collation.charset, 0, value.length);
} else {
stValue = Iconv.decode(value, info.collation.charset);
}
return "_binary'" + escapeString(stValue) + "'";
} else if (typeof value.toSqlString === 'function') {
return "'" + escapeString(String(value.toSqlString())) + "'";
} else if (Array.isArray(value)) {
let out = opts.arrayParenthesis ? '(' : '';
for (let i = 0; i < value.length; i++) {
if (i !== 0) out += ',';
out += escapeParameters(opts, info, value[i]);
}
if (opts.arrayParenthesis) out += ')';
return out;
} else {
if (
value.type != null &&
[
'Point',
'LineString',
'Polygon',
'MultiPoint',
'MultiLineString',
'MultiPolygon',
'GeometryCollection'
].includes(value.type)
) {
//GeoJSON format.
let prefix =
info &&
((info.isMariaDB() && info.hasMinVersion(10, 1, 4)) || (!info.isMariaDB() && info.hasMinVersion(5, 7, 6)))
? 'ST_'
: '';
switch (value.type) {
case 'Point':
return prefix + "PointFromText('POINT(" + TextEncoder.geoPointToString(value.coordinates) + ")')";
case 'LineString':
return (
prefix + "LineFromText('LINESTRING(" + TextEncoder.geoArrayPointToString(value.coordinates) + ")')"
);
case 'Polygon':
return (
prefix + "PolygonFromText('POLYGON(" + TextEncoder.geoMultiArrayPointToString(value.coordinates) + ")')"
);
case 'MultiPoint':
return (
prefix +
"MULTIPOINTFROMTEXT('MULTIPOINT(" +
TextEncoder.geoArrayPointToString(value.coordinates) +
")')"
);
case 'MultiLineString':
return (
prefix +
"MLineFromText('MULTILINESTRING(" +
TextEncoder.geoMultiArrayPointToString(value.coordinates) +
")')"
);
case 'MultiPolygon':
return (
prefix + "MPolyFromText('MULTIPOLYGON(" + TextEncoder.geoMultiPolygonToString(value.coordinates) + ")')"
);
case 'GeometryCollection':
return (
prefix +
"GeomCollFromText('GEOMETRYCOLLECTION(" +
TextEncoder.geometricCollectionToString(value.geometries) +
")')"
);
}
} else {
if (opts.permitSetMultiParamEntries) {
let out = '';
let first = true;
for (let key in value) {
const val = value[key];
if (typeof val === 'function') continue;
if (first) {
first = false;
} else {
out += ',';
}
out += '`' + key + '`=';
out += this.escape(opts, info, val);
}
if (out === '') return "'" + escapeString(JSON.stringify(value)) + "'";
return out;
} else {
return "'" + escapeString(JSON.stringify(value)) + "'";
}
}
}
default:
return "'" + escapeString(value) + "'";
}
};
// see https://mariadb.com/kb/en/library/string-literals/
const LITTERAL_ESCAPE = {
'\u0000': '\\0',
"'": "\\'",
'"': '\\"',
'\b': '\\b',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\u001A': '\\Z',
'\\': '\\\\'
};
const CHARS_GLOBAL_REGEXP = /[\000\032"'\\\b\n\r\t]/g;
const escapeString = (val) => {
let offset = 0;
let escaped = '';
let match;
while ((match = CHARS_GLOBAL_REGEXP.exec(val))) {
escaped += val.substring(offset, match.index);
escaped += LITTERAL_ESCAPE[match[0]];
offset = CHARS_GLOBAL_REGEXP.lastIndex;
}
if (offset === 0) {
return val;
}
if (offset < val.length) {
escaped += val.substring(offset);
}
return escaped;
};
module.exports.escape = escapeParameters;
+241
View File
@@ -0,0 +1,241 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const { EventEmitter } = require('events');
const Pool = require('./pool');
const Errors = require('./misc/errors');
const ConnectionCallback = require('./connection-callback');
class PoolCallback extends EventEmitter {
#pool;
constructor(options) {
super();
this.#pool = new Pool(options);
this.#pool.on('acquire', this.emit.bind(this, 'acquire'));
this.#pool.on('connection', this.emit.bind(this, 'connection'));
this.#pool.on('enqueue', this.emit.bind(this, 'enqueue'));
this.#pool.on('release', this.emit.bind(this, 'release'));
this.#pool.on('error', this.emit.bind(this, 'error'));
}
#noop = () => {};
get closed() {
return this.#pool.closed;
}
/**
* Get current total connection number.
* @return {number}
*/
totalConnections() {
return this.#pool.totalConnections();
}
/**
* Get current active connections.
* @return {number}
*/
activeConnections() {
return this.#pool.activeConnections();
}
/**
* Get current idle connection number.
* @return {number}
*/
idleConnections() {
return this.#pool.idleConnections();
}
/**
* Get current stacked connection request.
* @return {number}
*/
taskQueueSize() {
return this.#pool.taskQueueSize();
}
escape(value) {
return this.#pool.escape(value);
}
escapeId(value) {
return this.#pool.escapeId(value);
}
/**
* Ends pool
*
* @param callback
*/
end(callback) {
this.#pool
.end()
.then(() => {
if (callback) callback(null);
})
.catch(callback || this.#noop);
}
/**
* Retrieve a connection from the pool.
* Create a new one if the limit is not reached.
* wait until acquireTimeout.
*
* @param cb callback
*/
getConnection(cb) {
if (!cb) {
throw new Errors.createError('missing mandatory callback parameter', Errors.ER_MISSING_PARAMETER);
}
const cmdParam = {};
if (this.#pool.opts.connOptions.trace) Error.captureStackTrace(cmdParam);
this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
cb(err);
} else {
const cc = new ConnectionCallback(baseConn);
cc.end = (cb) => cc.release(cb);
cc.close = (cb) => cc.release(cb);
cb(null, cc);
}
});
}
/**
* Execute query using text protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @param cb callback
*/
query(sql, values, cb) {
const cmdParam = ConnectionCallback._PARAM(this.#pool.opts.connOptions, sql, values, cb);
this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
if (cmdParam.callback) cmdParam.callback(err);
} else {
const _cb = cmdParam.callback;
cmdParam.callback = (err, rows, meta) => {
this.#pool.release(baseConn);
if (_cb) _cb(err, rows, meta);
};
ConnectionCallback._QUERY_CMD(baseConn, cmdParam);
}
});
}
/**
* Execute query using binary protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
* @param cb callback
*/
execute(sql, values, cb) {
const cmdParam = ConnectionCallback._PARAM(this.#pool.opts.connOptions, sql, values, cb);
this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
if (cmdParam.callback) cmdParam.callback(err);
} else {
const _cb = cmdParam.callback;
baseConn.prepareExecute(
cmdParam,
(res) => {
this.#pool.release(baseConn);
if (_cb) _cb(null, res, res.meta);
},
(err) => {
this.#pool.release(baseConn);
if (_cb) _cb(err);
}
);
}
});
}
/**
* execute a batch
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values array of placeholder values
* @param cb callback
*/
batch(sql, values, cb) {
const cmdParam = ConnectionCallback._PARAM(this.#pool.opts.connOptions, sql, values, cb);
this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
if (cmdParam.callback) cmdParam.callback(err);
} else {
const _cb = cmdParam.callback;
baseConn.batch(
cmdParam,
(res) => {
this.#pool.release(baseConn);
if (_cb) _cb(null, res);
},
(err) => {
this.#pool.release(baseConn);
if (_cb) _cb(err);
}
);
}
});
}
/**
* Import sql file.
*
* @param opts JSON array with 2 possible fields: file and database
* @param cb callback
*/
importFile(opts, cb) {
if (!opts) {
if (cb)
cb(
Errors.createError(
'SQL file parameter is mandatory',
Errors.ER_MISSING_SQL_PARAMETER,
null,
'HY000',
null,
false,
null
)
);
return;
}
this.#pool.getConnection({}, (err, baseConn) => {
if (err) {
if (cb) cb(err);
} else {
baseConn.importFile(
{ file: opts.file, database: opts.database },
(res) => {
this.#pool.release(baseConn);
if (cb) cb(null, res);
},
(err) => {
this.#pool.release(baseConn);
if (cb) cb(err);
}
);
}
});
}
toString() {
return 'poolCallback(' + this.#pool.toString() + ')';
}
}
module.exports = PoolCallback;
+242
View File
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const { EventEmitter } = require('events');
const Pool = require('./pool');
const ConnectionPromise = require('./connection-promise');
const Errors = require('./misc/errors');
class PoolPromise extends EventEmitter {
#pool;
constructor(options) {
super();
this.#pool = new Pool(options);
this.#pool.on('acquire', this.emit.bind(this, 'acquire'));
this.#pool.on('connection', this.emit.bind(this, 'connection'));
this.#pool.on('enqueue', this.emit.bind(this, 'enqueue'));
this.#pool.on('release', this.emit.bind(this, 'release'));
this.#pool.on('error', this.emit.bind(this, 'error'));
}
get closed() {
return this.#pool.closed;
}
/**
* Get current total connection number.
* @return {number}
*/
totalConnections() {
return this.#pool.totalConnections();
}
/**
* Get current active connections.
* @return {number}
*/
activeConnections() {
return this.#pool.activeConnections();
}
/**
* Get current idle connection number.
* @return {number}
*/
idleConnections() {
return this.#pool.idleConnections();
}
/**
* Get current stacked connection request.
* @return {number}
*/
taskQueueSize() {
return this.#pool.taskQueueSize();
}
escape(value) {
return this.#pool.escape(value);
}
escapeId(value) {
return this.#pool.escapeId(value);
}
/**
* Ends pool
*
* @return Promise
**/
end() {
return this.#pool.end();
}
/**
* Retrieve a connection from pool.
* Create a new one, if limit is not reached.
* wait until acquireTimeout.
*
*/
async getConnection() {
const cmdParam = {};
if (this.#pool.opts.connOptions.trace) Error.captureStackTrace(cmdParam);
return new Promise((resolve, reject) => {
this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
reject(err);
} else {
const conn = new ConnectionPromise(baseConn);
conn.release = () => new Promise(baseConn.release);
conn.end = conn.release;
conn.close = conn.release;
resolve(conn);
}
});
});
}
/**
* Execute query using text protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
*/
query(sql, values) {
const cmdParam = ConnectionPromise.paramSetter(sql, values);
if (this.#pool.opts.connOptions.trace) Error.captureStackTrace(cmdParam);
return new Promise((resolve, reject) => {
return this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
reject(err);
} else {
baseConn.query(
cmdParam,
(res) => {
this.#pool.release(baseConn);
resolve(res);
},
(err) => {
this.#pool.release(baseConn);
reject(err);
}
);
}
});
});
}
/**
* Execute query using binary protocol with callback emit columns/data/end/error
* events to permit streaming big result-set
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values object / array of placeholder values (not mandatory)
*/
execute(sql, values) {
const cmdParam = ConnectionPromise.paramSetter(sql, values);
if (this.#pool.opts.connOptions.trace) Error.captureStackTrace(cmdParam);
return new Promise((resolve, reject) => {
return this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
reject(err);
} else {
baseConn.prepareExecute(
cmdParam,
(res) => {
this.#pool.release(baseConn);
resolve(res);
},
(err) => {
this.#pool.release(baseConn);
reject(err);
}
);
}
});
});
}
/**
* execute a batch
*
* @param sql sql parameter Object can be used to supersede default option.
* Object must then have sql property.
* @param values array of placeholder values
*/
batch(sql, values) {
const cmdParam = ConnectionPromise.paramSetter(sql, values);
if (this.#pool.opts.connOptions.trace) Error.captureStackTrace(cmdParam);
return new Promise((resolve, reject) => {
return this.#pool.getConnection(cmdParam, (err, baseConn) => {
if (err) {
reject(err);
} else {
baseConn.batch(
cmdParam,
(res) => {
this.#pool.release(baseConn);
resolve(res);
},
(err) => {
this.#pool.release(baseConn);
reject(err);
}
);
}
});
});
}
/**
* Import sql file.
*
* @param opts JSON array with 2 possible fields: file and database
*/
importFile(opts) {
if (!opts) {
return Promise.reject(
Errors.createError(
'SQL file parameter is mandatory',
Errors.ER_MISSING_SQL_PARAMETER,
null,
'HY000',
null,
false,
null
)
);
}
return new Promise((resolve, reject) => {
return this.#pool.getConnection({}, (err, baseConn) => {
if (err) {
reject(err);
} else {
baseConn.importFile(
{ file: opts.file, database: opts.database },
(res) => {
this.#pool.release(baseConn);
resolve(res);
},
(err) => {
this.#pool.release(baseConn);
reject(err);
}
);
}
});
});
}
toString() {
return 'poolPromise(' + this.#pool.toString() + ')';
}
}
module.exports = PoolPromise;
+951
View File
@@ -0,0 +1,951 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const { EventEmitter } = require('events');
const Queue = require('denque');
const Errors = require('./misc/errors');
const Utils = require('./misc/utils');
const Connection = require('./connection');
class Pool extends EventEmitter {
opts;
#closed = false;
#connectionInCreation = false;
#errorCreatingConnection = null;
#idleConnections;
#activeConnections = {};
#requests = new Queue();
#unusedConnectionRemoverId;
#requestTimeoutId;
#connErrorNumber = 0;
#initialized = false;
_managePoolSizeTask;
_connectionCreationTask;
constructor(options) {
super();
this.opts = options;
this.#idleConnections = new Queue(null, { capacity: this.opts.connectionLimit });
this.on('_idle', this._processNextPendingRequest);
this.on('validateSize', this._managePoolSize);
this._managePoolSize();
}
//*****************************************************************
// pool automatic handlers
//*****************************************************************
/**
* Manages pool size by creating new connections when needed
*/
_managePoolSize() {
// Only create new connections if conditions are met and no creation is in progress
if (!this._shouldCreateMoreConnections() || this._managePoolSizeTask) {
return;
}
this.#connectionInCreation = true;
const timeoutEnd = Date.now() + this.opts.initializationTimeout;
this._initiateConnectionCreation(timeoutEnd);
}
/**
* Initiates connection creation with proper error handling
* @param {number} timeoutEnd - When the connection attempt should time out
*/
_initiateConnectionCreation(timeoutEnd) {
this._createPoolConnection(
// Success callback
() => this._onConnectionCreationSuccess(),
// Error callback
(err) => this._onConnectionCreationError(err, timeoutEnd),
timeoutEnd
);
}
/**
* Handles successful connection creation
*/
_onConnectionCreationSuccess() {
this.#initialized = true;
this.#errorCreatingConnection = null;
this.#connErrorNumber = 0;
this._connectionCreationTask = null;
// Check if we need more connections
if (this._shouldCreateMoreConnections()) {
this.emit('validateSize');
}
this._startConnectionReaping();
}
/**
* Handles errors during connection creation
* @param {Error} err - The error that occurred
* @param {number} timeoutEnd - When the connection attempt should time out
*/
_onConnectionCreationError(err, timeoutEnd) {
this.#connectionInCreation = false;
if (this.#closed) {
return;
}
if (this.#errorCreatingConnection) err = this.#errorCreatingConnection;
// Format error message based on pool state
let error;
if (!this.#initialized) {
error = Errors.createError(
`Error during pool initialization`,
Errors.ER_POOL_NOT_INITIALIZED,
null,
null,
null,
false,
null,
null,
err
);
} else {
error = Errors.createError(
`Pool fails to create connection`,
Errors.ER_POOL_NO_CONNECTION,
null,
null,
null,
false,
null,
null,
err
);
}
// Schedule next attempt with exponential backoff
const backoffTime = Math.min(++this.#connErrorNumber * 200, 10000);
this._scheduleRetryWithBackoff(backoffTime);
this.emit('error', error);
}
/**
* Schedules the next connection creation attempt with backoff
* @param {number} delay - Time to wait before next attempt
*/
_scheduleRetryWithBackoff(delay) {
if (this.#closed) {
return;
}
this._managePoolSizeTask = setTimeout(() => {
this._managePoolSizeTask = null;
if (!this.#requests.isEmpty()) {
this._managePoolSize();
}
}, delay);
}
/**
* Creates a new connection for the pool with proper error handling
* @param {Function} onSuccess - Success callback
* @param {Function} onError - Error callback
* @param {number} timeoutEnd - Timestamp when connection attempt should time out
*/
_createPoolConnection(onSuccess, onError, timeoutEnd) {
const minTimeout = timeoutEnd - Date.now();
const connectionOpts = Object.assign({}, this.opts.connOptions, {
connectTimeout: Math.max(1, Math.min(minTimeout, this.opts.connOptions.connectTimeout || Number.MAX_SAFE_INTEGER))
});
const conn = new Connection(connectionOpts);
this._connectionCreationTask = null;
// Use direct callback approach instead of Promise
conn
.connect()
.then((conn) => this._prepareNewConnection(conn, onSuccess, onError))
.catch((err) => this._handleConnectionCreationError(err, onSuccess, onError, timeoutEnd));
}
/**
* Sets up a newly created connection for use in the pool
* @param {Connection} conn - The new connection
* @param {Function} onSuccess - Success callback
* @param {Function} onError - Error callback
*/
_prepareNewConnection(conn, onSuccess, onError) {
// Handle pool closed during connection creation
if (this.#closed) {
this._cleanupConnection(conn, 'pool_closed');
onError(
new Errors.createFatalError(
'Cannot create new connection to pool, pool closed',
Errors.ER_ADD_CONNECTION_CLOSED_POOL
)
);
return;
}
// Initialize connection for pool use
conn.lastUse = Date.now();
// Setup connection for pool use
conn.forceEnd = conn.end;
conn.release = (callback) => this._handleRelease(conn, callback);
conn.end = conn.release;
// Override destroy method to handle pool cleanup
this._overrideConnectionMethods(conn);
// Setup error handler for connection failures
this._setupConnectionErrorHandler(conn);
// Add to idle connections and mark creation as complete
this.#idleConnections.push(conn);
this.#connectionInCreation = false;
// Emit events and call success callback
this.emit('_idle');
this.emit('connection', conn);
onSuccess(conn);
}
/**
* Overrides connection methods for pool integration
* @param {Connection} conn - The connection to modify
*/
_overrideConnectionMethods(conn) {
const nativeDestroy = conn.destroy.bind(conn);
const pool = this;
conn.destroy = function () {
pool._endLeak(conn);
delete pool.#activeConnections[conn.threadId];
nativeDestroy();
pool.emit('validateSize');
};
}
/**
* Sets up error handler for a connection
* @param {Connection} conn - The connection to set up
*/
_setupConnectionErrorHandler(conn) {
const pool = this;
conn.once('error', () => {
// Clean up this connection
pool._endLeak(conn);
delete pool.#activeConnections[conn.threadId];
// Process idle connections
pool._processIdleConnectionsOnError(conn);
// Check if we need to create more connections
setImmediate(() => {
if (!pool.#requests.isEmpty()) {
pool._managePoolSize();
}
});
});
}
/**
* Processes idle connections when an error occurs
* @param {Connection} errorConn - The connection that had an error
*/
_processIdleConnectionsOnError(errorConn) {
let idx = 0;
while (idx < this.#idleConnections.length) {
const currConn = this.#idleConnections.peekAt(idx);
if (currConn === errorConn) {
this.#idleConnections.removeOne(idx);
continue;
}
// Force validation on other connections
currConn.lastUse = Math.min(currConn.lastUse, Date.now() - this.opts.minDelayValidation);
idx++;
}
}
/**
* Handles errors during connection creation
* @param {Error} err - The error that occurred
* @param {Function} onSuccess - Success callback
* @param {Function} onError - Error callback
* @param {number} timeoutEnd - Timestamp when connection attempt should time out
*/
_handleConnectionCreationError(err, onSuccess, onError, timeoutEnd) {
// Handle connection creation errors
if (err instanceof AggregateError) {
err = err.errors[0];
}
if (!this.#errorCreatingConnection) this.#errorCreatingConnection = err;
// Determine if we should retry or fail
const isFatalError =
this.#closed || (err.errno && [1524, 1045, 1698].includes(err.errno)) || timeoutEnd < Date.now();
if (isFatalError) {
// Fatal error - call error callback with additional pool info
err.message = err.message + this._errorMsgAddon();
this._connectionCreationTask = null;
onError(err);
return;
}
// Retry connection after delay
this._connectionCreationTask = setTimeout(
() => this._createPoolConnection(onSuccess, onError, timeoutEnd),
Math.min(500, timeoutEnd - Date.now())
);
}
/**
* Checks for timed-out requests and rejects them
*/
_checkRequestTimeouts() {
this.#requestTimeoutId = null;
const currentTime = Date.now();
while (this.#requests.length > 0) {
const request = this.#requests.peekFront();
if (this._hasRequestTimedOut(request, currentTime)) {
this._rejectTimedOutRequest(request, currentTime);
continue;
}
this._scheduleNextTimeoutCheck(request, currentTime);
return;
}
}
/**
* Checks if a request has timed out
* @param {Request} request - The request to check
* @param {number} currentTime - Current timestamp
* @returns {boolean} - True if request has timed out
*/
_hasRequestTimedOut(request, currentTime) {
return request.timeout <= currentTime;
}
/**
* Rejects a timed out request
* @param {Request} request - The request to reject
* @param {number} currentTime - Current timestamp
*/
_rejectTimedOutRequest(request, currentTime) {
this.#requests.shift();
// Determine the cause of the timeout
const timeoutCause = this.activeConnections() === 0 ? this.#errorCreatingConnection : null;
const waitTime = Math.abs(currentTime - (request.timeout - this.opts.acquireTimeout));
// Create appropriate error message with pool state information
const timeoutError = Errors.createError(
`pool timeout: failed to retrieve a connection from pool after ${waitTime}ms${this._errorMsgAddon()}`,
Errors.ER_GET_CONNECTION_TIMEOUT,
null,
'HY000',
null,
false,
request.stack,
null,
timeoutCause
);
request.reject(timeoutError);
}
/**
* Schedules the next timeout check
* @param {Request} request - The next request in queue
* @param {number} currentTime - Current timestamp
*/
_scheduleNextTimeoutCheck(request, currentTime) {
const timeUntilNextTimeout = request.timeout - currentTime;
this.#requestTimeoutId = setTimeout(() => this._checkRequestTimeouts(), timeUntilNextTimeout);
}
_destroy(conn) {
this._endLeak(conn);
delete this.#activeConnections[conn.threadId];
conn.lastUse = Date.now();
conn.forceEnd(
null,
() => {},
() => {}
);
if (this.totalConnections() === 0) {
this._stopConnectionReaping();
}
this.emit('validateSize');
}
release(conn) {
if (!this.#activeConnections[conn.threadId]) {
return; // Already released
}
this._endLeak(conn);
this.#activeConnections[conn.threadId] = null;
conn.lastUse = Date.now();
if (this.#closed) {
this._cleanupConnection(conn, 'pool_closed');
return;
}
// Only basic validation here - full validation happens when acquiring
if (conn.isValid()) {
this.emit('release', conn);
this.#idleConnections.push(conn);
process.nextTick(this.emit.bind(this, '_idle'));
} else {
this._cleanupConnection(conn, 'validation_failed');
}
}
_endLeak(conn) {
if (conn.leakProcess) {
clearTimeout(conn.leakProcess);
conn.leakProcess = null;
if (conn.leaked) {
conn.opts.logger.warning(
`Previous possible leak connection with thread ${conn.info.threadId} was returned to pool`
);
}
}
}
/**
* Permit to remove idle connection if unused for some time.
*/
_startConnectionReaping() {
if (!this.#unusedConnectionRemoverId && this.opts.idleTimeout > 0) {
this.#unusedConnectionRemoverId = setInterval(this._removeIdleConnections.bind(this), 500);
}
}
_stopConnectionReaping() {
if (this.#unusedConnectionRemoverId && this.totalConnections() === 0) {
clearInterval(this.#unusedConnectionRemoverId);
}
}
/**
* Removes idle connections that have been unused for too long
*/
_removeIdleConnections() {
const idleTimeRemoval = Date.now() - this.opts.idleTimeout * 1000;
let maxRemoval = Math.max(0, this.#idleConnections.length - this.opts.minimumIdle);
while (maxRemoval > 0) {
const conn = this.#idleConnections.peek();
maxRemoval--;
if (conn && conn.lastUse < idleTimeRemoval) {
this.#idleConnections.shift();
conn.forceEnd(
null,
() => {},
() => {}
);
continue;
}
break;
}
if (this.totalConnections() === 0) {
this._stopConnectionReaping();
}
this.emit('validateSize');
}
_shouldCreateMoreConnections() {
return (
!this.#connectionInCreation &&
this.#idleConnections.length < this.opts.minimumIdle &&
this.totalConnections() < this.opts.connectionLimit &&
!this.#closed
);
}
/**
* Processes the next request in the queue if connections are available
*/
_processNextPendingRequest() {
clearTimeout(this.#requestTimeoutId);
this.#requestTimeoutId = null;
const request = this.#requests.shift();
if (!request) return;
const conn = this.#idleConnections.shift();
if (conn) {
if (this.opts.leakDetectionTimeout > 0) {
this._startLeakDetection(conn);
}
this.#activeConnections[conn.threadId] = conn;
this.emit('acquire', conn);
request.resolver(conn);
} else {
this.#requests.unshift(request);
}
this._checkRequestTimeouts();
}
_hasIdleConnection() {
return !this.#idleConnections.isEmpty();
}
/**
* Acquires an idle connection from the pool
* @param {Function} callback - Callback function(err, conn)
*/
_acquireIdleConnection(callback) {
// Quick check if acquisition is possible
if (!this._hasIdleConnection() || this.#closed) {
callback(new Error('No idle connections available'));
return;
}
this._findValidIdleConnection(callback, false);
}
/**
* Search info object of an existing connection. to know server type and version.
* @returns information object if connection available.
*/
_searchInfo() {
let info = null;
let conn = this.#idleConnections.get(0);
if (!conn) {
for (const threadId in Object.keys(this.#activeConnections)) {
conn = this.#activeConnections[threadId];
if (!conn) {
break;
}
}
}
if (conn) {
info = conn.info;
}
return info;
}
/**
* Recursively searches for a valid idle connection
* @param {Function} callback - Callback function(err, conn)
* @param {boolean} needPoolSizeCheck - Whether to check pool size after
*/
_findValidIdleConnection(callback, needPoolSizeCheck) {
if (this.#idleConnections.isEmpty()) {
// No more connections to check
if (needPoolSizeCheck) {
setImmediate(() => this.emit('validateSize'));
}
callback(new Error('No valid connections found'));
return;
}
const conn = this.#idleConnections.shift();
this.#activeConnections[conn.threadId] = conn;
this._validateConnectionHealth(conn, (isValid) => {
if (isValid) {
if (this.opts.leakDetectionTimeout > 0) {
this._startLeakDetection(conn);
}
if (needPoolSizeCheck) {
setImmediate(() => this.emit('validateSize'));
}
callback(null, conn);
return;
} else {
delete this.#activeConnections[conn.threadId];
}
// Connection failed validation, try next one
this._findValidIdleConnection(callback, true);
});
}
/**
* Validates if a connection is healthy and can be used
* @param {Connection} conn - The connection to validate
* @param {Function} callback - Callback function(isValid)
*/
_validateConnectionHealth(conn, callback) {
if (!conn) {
callback(false);
return;
}
// Skip validation if connection is already invalid or was recently used
const recentlyUsed = this.opts.minDelayValidation > 0 && Date.now() - conn.lastUse <= this.opts.minDelayValidation;
if (!conn.isValid() || recentlyUsed) {
callback(conn.isValid());
return;
}
// Perform ping to verify connection is responsive
const pingOptions = { opts: { timeout: this.opts.pingTimeout } };
conn.ping(
pingOptions,
() => callback(true),
() => callback(false)
);
}
_leakedConnections() {
let counter = 0;
for (const connection of Object.values(this.#activeConnections)) {
if (connection && connection.leaked) counter++;
}
return counter;
}
_errorMsgAddon() {
if (this.opts.leakDetectionTimeout > 0) {
return `\n (pool connections: active=${this.activeConnections()} idle=${this.idleConnections()} leak=${this._leakedConnections()} limit=${
this.opts.connectionLimit
})`;
}
return `\n (pool connections: active=${this.activeConnections()} idle=${this.idleConnections()} limit=${
this.opts.connectionLimit
})`;
}
toString() {
return `active=${this.activeConnections()} idle=${this.idleConnections()} limit=${this.opts.connectionLimit}`;
}
//*****************************************************************
// public methods
//*****************************************************************
get closed() {
return this.#closed;
}
/**
* Get current total connection number.
* @return {number}
*/
totalConnections() {
return this.activeConnections() + this.idleConnections();
}
/**
* Get current active connections.
* @return {number}
*/
activeConnections() {
let counter = 0;
for (const connection of Object.values(this.#activeConnections)) {
if (connection) counter++;
}
return counter;
}
/**
* Get current idle connection number.
* @return {number}
*/
idleConnections() {
return this.#idleConnections.length;
}
/**
* Get current stacked connection request.
* @return {number}
*/
taskQueueSize() {
return this.#requests.length;
}
escape(value) {
return Utils.escape(this.opts.connOptions, this._searchInfo(), value);
}
escapeId(value) {
return Utils.escapeId(this.opts.connOptions, this._searchInfo(), value);
}
//*****************************************************************
// promise methods
//*****************************************************************
/**
* Retrieve a connection from the pool.
* Create a new one if limit is not reached.
* wait until acquireTimeout.
* @param cmdParam for stackTrace error
* @param {Function} callback - Callback function(err, conn)
*/
getConnection(cmdParam, callback) {
if (typeof cmdParam === 'function') {
callback = cmdParam;
cmdParam = {};
}
if (this.#closed) {
const err = Errors.createError(
'pool is closed',
Errors.ER_POOL_ALREADY_CLOSED,
null,
'HY000',
cmdParam === null ? null : cmdParam.sql,
false,
cmdParam.stack
);
callback(err);
return;
}
this._acquireIdleConnection((err, conn) => {
if (!err && conn) {
// connection is available
this.emit('acquire', conn);
callback(null, conn);
return;
}
if (this.#closed) {
callback(
Errors.createError(
'Cannot add request to pool, pool is closed',
Errors.ER_POOL_ALREADY_CLOSED,
null,
'HY000',
cmdParam === null ? null : cmdParam.sql,
false,
cmdParam.stack
)
);
return;
}
// no idle connection available
// creates a new connection if the limit is not reached
setImmediate(this.emit.bind(this, 'validateSize'));
// stack request
setImmediate(this.emit.bind(this, 'enqueue'));
const request = new Request(
Date.now() + this.opts.acquireTimeout,
cmdParam.stack,
(conn) => callback(null, conn),
(err) => callback(err)
);
this.#requests.push(request);
if (!this.#requestTimeoutId) {
this.#requestTimeoutId = setTimeout(this._checkRequestTimeouts.bind(this), this.opts.acquireTimeout);
}
});
}
/**
* Close all connection in pool
* Ends in multiple step :
* - close idle connections
* - ensure that no new request is possible
* (active connection release are automatically closed on release)
* - if remaining, after 10 seconds, close remaining active connections
*
* @return Promise
*/
end() {
if (this.#closed) {
return Promise.reject(Errors.createError('pool is already closed', Errors.ER_POOL_ALREADY_CLOSED));
}
this.#closed = true;
clearInterval(this.#unusedConnectionRemoverId);
clearInterval(this._managePoolSizeTask);
clearTimeout(this._connectionCreationTask);
clearTimeout(this.#requestTimeoutId);
const cmdParam = {};
if (this.opts.trace) Error.captureStackTrace(cmdParam);
//close unused connections
const idleConnectionsEndings = [];
let conn;
while ((conn = this.#idleConnections.shift())) {
idleConnectionsEndings.push(new Promise(conn.forceEnd.bind(conn, cmdParam)));
}
clearTimeout(this.#requestTimeoutId);
this.#requestTimeoutId = null;
//reject all waiting task
if (!this.#requests.isEmpty()) {
const err = Errors.createError(
'pool is ending, connection request aborted',
Errors.ER_CLOSING_POOL,
null,
'HY000',
null,
false,
cmdParam.stack
);
let task;
while ((task = this.#requests.shift())) {
task.reject(err);
}
}
const pool = this;
return Promise.all(idleConnectionsEndings).then(async () => {
if (pool.activeConnections() > 0) {
// wait up to 10 seconds, that active connection are released
let remaining = 100;
while (remaining-- > 0) {
if (pool.activeConnections() > 0) {
await new Promise((res) => setTimeout(() => res(), 100));
}
}
// force close any remaining active connections
for (const connection of Object.values(pool.#activeConnections)) {
if (connection) connection.destroy();
}
}
return Promise.resolve();
});
}
_cleanupConnection(conn, reason = '') {
if (!conn) return;
this._endLeak(conn);
delete this.#activeConnections[conn.threadId];
try {
// using end in case pool ends while connection succeed without still having function wrappers
const endingFct = conn.forceEnd ? conn.forceEnd : conn.end;
endingFct.call(
conn,
null,
() => this.emit('connectionClosed', { threadId: conn.threadId, reason }),
() => {}
);
} catch (err) {
this.emit('error', new Error(`Failed to cleanup connection: ${err.message}`));
}
if (this.totalConnections() === 0) {
this._stopConnectionReaping();
}
this.emit('validateSize');
}
/**
* Handles the release of a connection back to the pool
* @param {Connection} conn - The connection to release
* @param {Function} callback - Callback function when complete
*/
_handleRelease(conn, callback) {
callback = callback || function () {};
// Handle special cases first
if (this.#closed || !conn.isValid()) {
this._destroy(conn);
callback();
return;
}
// Skip transaction state reset if configured
if (this.opts.noControlAfterUse) {
this.release(conn);
callback();
return;
}
// Reset connection state before returning to pool
const resetFunction = this._getRevertFunction(conn);
resetFunction((err) => {
if (err) {
this._destroy(conn);
} else {
this.release(conn);
}
callback();
});
}
/**
* Get the appropriate function to reset connection state
* @returns {Function} Function that takes a callback
*/
_getRevertFunction(conn) {
const canUseReset =
this.opts.resetAfterUse &&
conn.info.isMariaDB() &&
((conn.info.serverVersion.minor === 2 && conn.info.hasMinVersion(10, 2, 22)) ||
conn.info.hasMinVersion(10, 3, 13));
return canUseReset
? (callback) => conn.reset({}, callback)
: (callback) =>
conn.changeTransaction(
{ sql: 'ROLLBACK' },
() => callback(null),
(err) => callback(err)
);
}
/**
* Sets up leak detection for a connection
* @param {Connection} conn - The connection to monitor
*/
_startLeakDetection(conn) {
conn.lastUse = Date.now();
conn.leaked = false;
// Set timeout to detect potential leaks
conn.leakProcess = setTimeout(
() => {
conn.leaked = true;
const unusedTime = Date.now() - conn.lastUse;
// Log warning about potential leak
conn.opts.logger.warning(
`A possible connection leak on thread ${conn.info.threadId} ` +
`(connection not returned to pool for ${unusedTime}ms). ` +
`Has connection.release() been called?${this._errorMsgAddon()}`
);
},
this.opts.leakDetectionTimeout,
conn
);
}
}
class Request {
constructor(timeout, stack, resolver, rejecter) {
this.timeout = timeout;
this.stack = stack;
this.resolver = resolver;
this.rejecter = rejecter;
}
reject(err) {
process.nextTick(this.rejecter, err);
}
}
module.exports = Pool;
+87
View File
@@ -0,0 +1,87 @@
{
"name": "mariadb",
"version": "3.4.4",
"description": "fast mariadb or mysql connector.",
"main": "promise.js",
"types": "types/index.d.ts",
"typesVersions": {
"*": {
"callback": ["types/callback.d.ts"],
"*": ["types/index.d.ts"]
}
},
"directories": {
"lib": "lib",
"test": "test"
},
"private": false,
"scripts": {
"test": "npm run test:types-prettier && npm run test:prettier && npm run test:types && npm run test:lint && npm run test:base",
"test:base": "mocha --no-parallel --timeout 5000 \"test/**/*.js\"",
"test:lint": "eslint \"*.js\" \"{lib,test}/**/*.js\"",
"test:types": "eslint \"types/*.ts\"",
"test:types-prettier": "prettier --write \"types/*.ts\"",
"test:prettier": "prettier --write \"*.js\" \"{tools,lib,test,benchmarks}/**/*.js\"",
"coverage": "npm run coverage:test && npm run coverage:create && npm run coverage:send",
"coverage:test": "nyc mocha --no-parallel --timeout 5000 \"test/**/*.js\"",
"coverage:report": "npm run coverage:create && npm run coverage:send",
"coverage:create": "nyc report --reporter=text-lcov > coverage.lcov",
"coverage:send": "./codecov --disable=gcov",
"benchmark": "node benchmarks/benchmarks-all.js",
"generate": "node ./tools/generate-mariadb.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mariadb-corporation/mariadb-connector-nodejs.git"
},
"keywords": [
"mariadb",
"mysql",
"client",
"driver",
"connector"
],
"files": [
"lib",
"types/index.d.ts",
"types/callback.d.ts",
"types/share.d.ts",
"promise.js",
"check-node.js",
"callback.js"
],
"engines": {
"node": ">= 14"
},
"author": "Diego Dupin <diego.dupin@mariadb.com>",
"license": "LGPL-2.1-or-later",
"dependencies": {
"denque": "^2.1.0",
"iconv-lite": "^0.6.3",
"lru-cache": "^10.3.0"
},
"devDependencies": {
"@types/geojson": "^7946.0.14",
"@types/node": "^22.5.4",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"benchmark": "^2.1.4",
"chai": "^4.4.1",
"chalk": "^4.1.2",
"error-stack-parser": "^2.1.4",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-markdown": "^3.0.1",
"eslint-plugin-prettier": "^5.0.0",
"mocha": "^10.2.0",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "^15.1.0",
"prettier": "^3.0.3",
"typescript": "^5.6.2",
"winston": "^3.10.0"
},
"bugs": {
"url": "https://jira.mariadb.org/projects/CONJS/"
},
"homepage": "https://github.com/mariadb-corporation/mariadb-connector-nodejs#readme"
}
+72
View File
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2024 MariaDB Corporation Ab
'use strict';
require('./check-node');
const Connection = require('./lib/connection');
const ConnectionPromise = require('./lib/connection-promise');
const PoolPromise = require('./lib/pool-promise');
const Cluster = require('./lib/cluster');
const ConnOptions = require('./lib/config/connection-options');
const PoolOptions = require('./lib/config/pool-options');
const ClusterOptions = require('./lib/config/cluster-options');
module.exports.version = require('./package.json').version;
module.exports.SqlError = require('./lib/misc/errors').SqlError;
module.exports.defaultOptions = function defaultOptions(opts) {
const connOpts = new ConnOptions(opts);
const res = {};
for (const [key, value] of Object.entries(connOpts)) {
if (!key.startsWith('_')) {
res[key] = value;
}
}
return res;
};
module.exports.createConnection = function createConnection(opts) {
try {
const options = new ConnOptions(opts);
const conn = new Connection(options);
const connPromise = new ConnectionPromise(conn);
return conn.connect().then(() => Promise.resolve(connPromise));
} catch (err) {
return Promise.reject(err);
}
};
module.exports.createPool = function createPool(opts) {
const options = new PoolOptions(opts);
const pool = new PoolPromise(options);
// adding a default error handler to avoid exiting application on connection error.
pool.on('error', (err) => {});
return pool;
};
module.exports.createPoolCluster = function createPoolCluster(opts) {
const options = new ClusterOptions(opts);
return new Cluster(options);
};
module.exports.importFile = function importFile(opts) {
try {
const options = new ConnOptions(opts);
const conn = new Connection(options);
return conn
.connect()
.then(() => {
return new Promise(conn.importFile.bind(conn, Object.assign({ skipDbCheck: true }, opts)));
})
.finally(() => {
new Promise(conn.end.bind(conn, {})).catch(console.log);
});
} catch (err) {
return Promise.reject(err);
}
};
+206
View File
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
/*
* Callback-based API for mariadb-connector-nodejs
* This file mirrors the structure of the promise-based API in index.d.ts,
* but all async methods use Node.js-style callbacks as the last argument.
*
* Callback signature: (err: SqlError | null, result?: T, meta?: any) => void
*
* All types are reused from share.d.ts where possible.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import stream = require('stream');
import events = require('events');
import type {
ConnectionConfig,
ImportFileConfig,
SqlError,
FieldInfo,
QueryOptions,
UpsertResult,
PoolConfig,
PoolClusterConfig,
SqlImportOptions,
ConnectionInfo,
UserConnectionConfig
} from './share';
export * from './share';
export const version: string;
export function createConnection(connectionUri: string | ConnectionConfig): Connection;
export function importFile(config: ImportFileConfig, callback: (err: SqlError | null) => void): void;
export interface Prepare {
id: number;
execute<T = any>(values: any, callback: (err: SqlError | null, result?: T, meta?: any) => void): void;
/**
* Execute query returning a Readable Object that will emit columns/data/end/error events
* to permit streaming big result-set
*/
executeStream(values: any): stream.Readable;
close(): void;
}
export interface Connection extends events.EventEmitter {
/** Connection information */
info: ConnectionInfo | null;
/** Alias of info.threadId for compatibility */
readonly threadId: number | null;
changeUser(options: UserConnectionConfig, callback: (err: SqlError | null) => void): void;
beginTransaction(callback: (err: SqlError | null) => void): void;
commit(callback: (err: SqlError | null) => void): void;
rollback(callback: (err: SqlError | null) => void): void;
query<T = any>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
query<T = any>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
prepare(sql: string | QueryOptions, callback: (err: SqlError | null, prepare?: Prepare) => void): void;
execute<T = any>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
execute<T = any>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
batch<T = UpsertResult | UpsertResult[]>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T) => void
): void;
batch<T = UpsertResult | UpsertResult[]>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T) => void
): void;
queryStream(sql: string | QueryOptions, values?: any): stream.Readable;
ping(callback: (err: SqlError | null) => void): void;
reset(callback: (err: SqlError | null) => void): void;
importFile(config: SqlImportOptions, callback: (err: SqlError | null) => void): void;
isValid(): boolean;
end(callback: (err: SqlError | null) => void): void;
close(callback: (err: SqlError | null) => void): void;
destroy(): void;
pause(): void;
resume(): void;
serverVersion(): string;
debug(value: boolean): void;
debugCompress(value: boolean): void;
escape(value: any): string;
escapeId(identifier: string): string;
on(ev: 'end', callback: () => void): Connection;
on(ev: 'error', callback: (err: SqlError) => void): Connection;
on(eventName: string | symbol, listener: (...args: any[]) => void): this;
listeners(ev: 'end'): (() => void)[];
listeners(ev: 'error'): ((err: SqlError) => void)[];
}
export interface PoolConnection extends Connection {
release(callback: (err: SqlError | null) => void): void;
}
export interface Pool {
closed: boolean;
getConnection(callback: (err: SqlError | null, conn?: PoolConnection) => void): void;
query<T = any>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
query<T = any>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
batch<T = UpsertResult | UpsertResult[]>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T) => void
): void;
batch<T = UpsertResult | UpsertResult[]>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T) => void
): void;
execute<T = any>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
execute<T = any>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
end(callback: (err: SqlError | null) => void): void;
importFile(config: SqlImportOptions, callback: (err: SqlError | null) => void): void;
activeConnections(): number;
totalConnections(): number;
idleConnections(): number;
taskQueueSize(): number;
escape(value: any): string;
escapeId(identifier: string): string;
on(ev: 'acquire', callback: (conn: Connection) => void): Pool;
on(ev: 'connection', callback: (conn: Connection) => void): Pool;
on(ev: 'enqueue', callback: () => void): Pool;
on(ev: 'release', callback: (conn: Connection) => void): Pool;
}
export interface FilteredPoolCluster {
getConnection(callback: (err: SqlError | null, conn?: PoolConnection) => void): void;
query<T = any>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
query<T = any>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
batch<T = UpsertResult | UpsertResult[]>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T) => void
): void;
batch<T = UpsertResult | UpsertResult[]>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T) => void
): void;
execute<T = any>(
sql: string | QueryOptions,
values: any,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
execute<T = any>(
sql: string | QueryOptions,
callback: (err: SqlError | null, result?: T, meta?: FieldInfo[]) => void
): void;
}
export interface PoolCluster {
add(id: string, config: PoolConfig): void;
end(callback: (err: SqlError | null) => void): void;
of(pattern: string, selector?: string): FilteredPoolCluster;
of(pattern: undefined | null | false, selector: string): FilteredPoolCluster;
remove(pattern: string): void;
getConnection(
pattern: string | undefined | null,
selector: string | undefined | null,
callback: (err: SqlError | null, conn?: PoolConnection) => void
): void;
getConnection(
pattern: string | undefined | null,
callback: (err: SqlError | null, conn?: PoolConnection) => void
): void;
getConnection(callback: (err: SqlError | null, conn?: PoolConnection) => void): void;
on(ev: 'remove', callback: (nodekey: string) => void): PoolCluster;
}
export function createPool(config: PoolConfig | string): Pool;
export function createPoolCluster(config?: PoolClusterConfig): PoolCluster;
+290
View File
@@ -0,0 +1,290 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
/* eslint-disable @typescript-eslint/no-explicit-any */
// Type definitions for mariadb 2.5
// Project: https://github.com/mariadb-corporation/mariadb-connector-nodejs
// Definitions by: Diego Dupin <https://github.com/rusher>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.1
import stream = require('stream');
import events = require('events');
import type {
ConnectionConfig,
ImportFileConfig,
SqlError,
QueryOptions,
UpsertResult,
PoolConfig,
PoolClusterConfig,
SqlImportOptions,
ConnectionInfo,
UserConnectionConfig
} from './share';
export * from './share';
export const version: string;
export function createConnection(connectionUri: string | ConnectionConfig): Promise<Connection>;
export function importFile(config: ImportFileConfig): Promise<void>;
export interface Prepare {
id: number;
execute<T = any>(values?: any): Promise<T>;
/**
* Execute query returning a Readable Object that will emit columns/data/end/error events
* to permit streaming big result-set
*/
executeStream(values?: any): stream.Readable;
close(): void;
}
export interface Connection extends events.EventEmitter {
/**
* Connection information
*/
info: ConnectionInfo | null;
/**
* Alias of info.threadId for compatibility
*/
readonly threadId: number | null;
/**
* Permit changing user during connection.
* All user variables will be reset, Prepare commands will be released.
* !!! mysql has a bug when CONNECT_ATTRS capability is set, that is default !!!!
*/
changeUser(options?: UserConnectionConfig): Promise<void>;
/**
* Start transaction
*/
beginTransaction(): Promise<void>;
/**
* Commit a transaction.
*/
commit(): Promise<void>;
/**
* Roll back a transaction.
*/
rollback(): Promise<void>;
/**
* Execute query using text protocol.
*/
query<T = any>(sql: string | QueryOptions, values?: any): Promise<T>;
/**
* Prepare query.
*/
prepare(sql: string | QueryOptions): Promise<Prepare>;
/**
* Execute query using binary (prepare) protocol
*/
execute<T = any>(sql: string | QueryOptions, values?: any): Promise<T>;
/**
* Execute batch. Values are Array of Array.
*/
batch<T = UpsertResult | UpsertResult[]>(sql: string | QueryOptions, values?: any): Promise<T>;
/**
* Execute query returning a Readable Object that will emit columns/data/end/error events
* to permit streaming big result-set
*/
queryStream(sql: string | QueryOptions, values?: any): stream.Readable;
/**
* Send an empty MySQL packet to ensure the connection is active, and reset @@wait_timeout
*/
ping(): Promise<void>;
/**
* Send a reset command that will
* - roll back any open transaction
* - reset transaction isolation level
* - reset session variables
* - delete user variables
* - remove temporary tables
* - remove all PREPARE statements
*/
reset(): Promise<void>;
/**
* import sql file
*/
importFile(config: SqlImportOptions): Promise<void>;
/**
* Indicates the state of the connection as the driver knows it
*/
isValid(): boolean;
/**
* Terminate connection gracefully.
*/
end(): Promise<void>;
/**
* @deprecated alias for end().
*/
close(): Promise<void>;
/**
* Force connection termination by closing the underlying socket and killing a server process if any.
*/
destroy(): void;
pause(): void;
resume(): void;
/**
* Alias for info.serverVersion.raw
*/
serverVersion(): string;
/**
* Change option "debug" during connection.
*/
debug(value: boolean): void;
/**
* Change option "debugCompress" during connection.
*/
debugCompress(value: boolean): void;
/**
* This function permits escaping a parameter properly, according to a parameter type, to avoid injection.
* @param value parameter
*/
escape(value: any): string;
/**
* This function permits escaping an Identifier properly. See Identifier Names for escaping. Value will be enclosed
* by '`' character if content doesn't satisfy:
* <OL>
* <LI>ASCII: [0-9,a-z,A-Z$_] (numerals 0-9, basic Latin letters, both lowercase and uppercase, dollar sign,
* underscore)</LI>
* <LI>Extended: U+0080 .. U+FFFF and escaping '`' character if needed.</LI>
* </OL>
* @param identifier identifier
*/
escapeId(identifier: string): string;
on(ev: 'end', callback: () => void): Connection;
on(ev: 'error', callback: (err: SqlError) => void): Connection;
on(eventName: string | symbol, listener: (...args: any[]) => void): this;
listeners(ev: 'end'): (() => void)[];
listeners(ev: 'error'): ((err: SqlError) => void)[];
}
export interface PoolConnection extends Connection {
/**
* Release the connection to pool internal cache.
*/
release(): Promise<void>;
}
export interface Pool {
closed: boolean;
/**
* Retrieve a connection from the pool.
* Create a new one if the limit is not reached.
* wait until acquireTimeout.
*/
getConnection(): Promise<PoolConnection>;
/**
* Execute a query on one connection from pool.
*/
query<T = any>(sql: string | QueryOptions, values?: any): Promise<T>;
/**
* Execute a batch on one connection from pool.
*/
batch<T = UpsertResult | UpsertResult[]>(sql: string | QueryOptions, values?: any): Promise<T>;
/**
* Execute query using binary (prepare) protocol
*/
execute<T = any>(sql: string | QueryOptions, values?: any): Promise<T>;
/**
* Close all connection in pool
*/
end(): Promise<void>;
/**
* import sql file
*/
importFile(config: SqlImportOptions): Promise<void>;
/**
* Get current active connections.
*/
activeConnections(): number;
/**
* Get the current total connection number.
*/
totalConnections(): number;
/**
* Get the current idle connection number.
*/
idleConnections(): number;
/**
* Get current stacked connection request.
*/
taskQueueSize(): number;
/**
* This function permits escaping a parameter properly, according to a parameter type, to avoid injection.
* @param value parameter
*/
escape(value: any): string;
/**
* This function permits escaping an Identifier properly. See Identifier Names for escaping. Value will be enclosed
* by '`' character if content doesn't satisfy:
* <OL>
* <LI>ASCII: [0-9,a-z,A-Z$_] (numerals 0-9, basic Latin letters, both lowercase and uppercase, dollar sign,
* underscore)</LI>
* <LI>Extended: U+0080 .. U+FFFF and escaping '`' character if needed.</LI>
* </OL>
* @param identifier identifier
*/
escapeId(identifier: string): string;
on(ev: 'acquire', callback: (conn: Connection) => void): Pool;
on(ev: 'connection', callback: (conn: Connection) => void): Pool;
on(ev: 'enqueue', callback: () => void): Pool;
on(ev: 'release', callback: (conn: Connection) => void): Pool;
}
export interface FilteredPoolCluster {
getConnection(): Promise<PoolConnection>;
query<T = any>(sql: string | QueryOptions, values?: any): Promise<T>;
batch<T = UpsertResult | UpsertResult[]>(sql: string | QueryOptions, values?: any): Promise<T>;
execute<T = any>(sql: string | QueryOptions, values?: any): Promise<T>;
}
export interface PoolCluster {
add(id: string, config: PoolConfig): void;
end(): Promise<void>;
of(pattern: string, selector?: string): FilteredPoolCluster;
of(pattern: undefined | null | false, selector: string): FilteredPoolCluster;
remove(pattern: string): void;
getConnection(pattern?: string, selector?: string): Promise<PoolConnection>;
on(ev: 'remove', callback: (nodekey: string) => void): PoolCluster;
}
export function createPool(config: PoolConfig | string): Pool;
export function createPoolCluster(config?: PoolClusterConfig): PoolCluster;
+804
View File
@@ -0,0 +1,804 @@
import geojson = require('geojson');
import stream = require('stream');
import tls = require('tls');
export type TypeCastResult = boolean | number | string | symbol | null | Date | geojson.Geometry | Buffer;
export type TypeCastNextFunction = () => TypeCastResult;
export type TypeCastFunction = (field: FieldInfo, next: TypeCastNextFunction) => TypeCastResult;
export function StreamCallback(err?: Error, stream?: stream.Duplex): void;
export interface LoggerConfig {
network?: (msg: string) => void;
query?: (msg: string) => void;
error?: (err: Error) => void;
warning?: (msg: string) => void;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export function defaultOptions(connectionUri?: string | ConnectionConfig): any;
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface FieldInfo {
collation: Collation;
columnLength: number;
columnType: TypeNumbers;
scale: number;
type: Types;
flags: Flags;
db(): string;
schema(): string; // Alias for db()
table(): string;
orgTable(): string;
name(): string;
orgName(): string;
// Note that you may only call *one* of these functions
// when decoding a column via the typeCast callback.
// Calling additional functions will give you incorrect results.
string(): string | null;
buffer(): Buffer | null;
float(): number | null;
int(): number | null;
long(): number | null;
decimal(): number | null;
date(): Date | null;
geometry(): geojson.Geometry | null;
}
export interface ImportFileConfig extends ConnectionConfig {
/**
* SQL file path to import
*/
file: string;
}
export interface PoolConfig extends ConnectionConfig {
/**
* The milliseconds before a timeout occurs during the connection acquisition. This is slightly different from
* connectTimeout, because acquiring a pool connection does not always involve making a connection.
* (Default: 10 seconds)
*/
acquireTimeout?: number;
/**
* The maximum number of connections to create at once. (Default: 10)
*/
connectionLimit?: number;
/**
* Indicate idle time after which a pool connection is released.
* The Value must be lower than
* [@@wait_timeout](https://mariadb.com/kb/en/library/server-system-variables/#wait_timeout).
* In seconds (0 means never release)
* Default: 1800 (= 30 minutes)
*/
idleTimeout?: number;
/**
* Timeout after which pool give up creating new connection.
*/
initializationTimeout?: number;
/**
* When asking a connection to pool, the pool will validate the connection state.
* "minDelayValidation" permits disabling this validation if the connection has been borrowed recently avoiding
* useless verifications in case of frequent reuse of connections.
* 0 means validation is done each time the connection is asked. (in ms)
* Default: 500 (in millisecond)
*/
minDelayValidation?: number;
/**
* Permit setting a minimum amount of connection in pool.
* **Recommendation is to use fixed pool, so not setting this value**
*/
minimumIdle?: number;
/**
* Use COM_STMT_RESET when releasing a connection to pool.
* Default: true
*/
resetAfterUse?: boolean;
/**
* No rollback or reset when releasing a connection to pool.
* Default: false
*/
noControlAfterUse?: boolean;
/**
* Permit indicating a timeout to log connection borrowed from pool.
* When a connection is borrowed from the pool and this timeout is reached,
* a message will be logged to the console indicating a possible connection leak.
* Another message will tell if the possible logged leak has been released.
* A value of 0 (default) meaning Leak detection is disabled
*/
leakDetectionTimeout?: number;
}
export interface PoolClusterConfig {
/**
* If true, PoolCluster will attempt to reconnect when the connection fails. (Default: true)
*/
canRetry?: boolean;
/**
* If connection fails, node's errorCount increases. When errorCount is greater than removeNodeErrorCount,
* remove a node in the PoolCluster. (Default: 5)
*/
removeNodeErrorCount?: number;
/**
* If connection fails, specifies the number of milliseconds before another connection attempt will be made.
* If set to 0, then the node will be removed instead and never re-used. (Default: 0)
*/
restoreNodeTimeout?: number;
/**
* The default selector. (Default: RR)
* RR: Select one alternately. (Round-Robin)
* RANDOM: Select the node by random function.
* ORDER: Select the first node available unconditionally.
*/
defaultSelector?: string;
}
export interface ServerVersion {
/**
* Raw string that database server send to connector.
* example : "10.4.3-MariaDB-1:10.4.3+maria~bionic-log"
*/
readonly raw: string;
/**
* indicate if server is a MariaDB or a MySQL server
*/
readonly mariaDb: boolean;
/**
* Server major version.
* Example for raw version "10.4.3-MariaDB" is 10
*/
readonly major: number;
/**
* Server major version.
* Example for raw version "10.4.3-MariaDB" is 4
*/
readonly minor: number;
/**
* Server major version.
* Example for raw version "10.4.3-MariaDB" is 3
*/
readonly patch: number;
}
export interface SqlImportOptions {
/**
* file path of sql file
*/
file: string;
/**
* Name of the database to use to import sql file.
* If not set, current database is used.
*/
database?: string;
}
export interface ConnectionInfo {
/**
* Server connection identifier value
*/
readonly threadId: number | null;
/**
* connection status flag
* see https://mariadb.com/kb/en/library/ok_packet/#server-status-flag
*/
readonly status: number;
/**
* Server version information
*/
serverVersion: ServerVersion;
/**
* connection collation
*/
collation: null;
/**
* Server capabilities
* see https://mariadb.com/kb/en/library/connection/#capabilities
*/
readonly serverCapabilities: number;
/**
* Indicate when connected if server is a MariaDB or MySQL one
*/
isMariaDB(): boolean;
/**
* return true if server version > to indicate version
* @param major server major version
* @param minor server minor version
* @param patch server patch version
*/
hasMinVersion(major: number, minor: number, patch: number): boolean;
}
export interface UserConnectionConfig {
/**
* Name of the database to use for this connection
*/
database?: string;
/**
* When enabled, sends information during connection to server
* - client name
* - version
* - operating system
* - Node.js version
*
* If JSON is set, add JSON key/value to those values.
*
* When Performance Schema is enabled, server can display client information on each connection.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
connectAttributes?: any;
/* eslint-enable @typescript-eslint/no-explicit-any */
/**
* Protocol character set used with the server.
* Connection collation will be the default collation associated with charset.
* It's mainly used for micro-optimizations. The default is often sufficient.
* example 'UTF8MB4', 'CP1250'.
* (default 'UTF8MB4')
*/
charset?: string;
/**
* Permit to defined collation used for connection.
* This will defined the charset encoding used for exchanges with database and defines the order used when
* comparing strings. It's mainly used for micro-optimizations
* (Default: 'UTF8MB4_UNICODE_CI')
*/
collation?: string;
/**
* The MySQL user to authenticate as
*/
user?: string;
/**
* The password of that MySQL user
*/
password?: string;
}
export interface UpsertResult {
affectedRows: number;
insertId: number | bigint;
warningStatus: number;
}
export interface SqlError extends Error {
/**
* Either a MySQL server error (e.g. 'ER_ACCESS_DENIED_ERROR'),
* a node.js error (e.g. 'ECONNREFUSED') or an internal error
* (e.g. 'PROTOCOL_CONNECTION_LOST').
*/
code: string | null;
/**
* original error message value
* @deprecated since 3.2.0 prefer using sqlMessage for compatibility with other drivers.
*/
text: string | null;
/**
* original error message value
*/
sqlMessage: string | null;
/**
* The sql command associate
*/
sql: string | null;
/**
* The error number for the error code
*/
errno: number;
/**
* The sql state
*/
sqlState?: string | null;
/**
* Boolean, indicating if this error is terminal to the connection object.
*/
fatal: boolean;
}
interface SqlErrorConstructor extends ErrorConstructor {
new (
msg: string,
sql?: string,
fatal?: boolean,
info?: { threadId?: number },
sqlState?: string | null,
errno?: number,
additionalStack?: string,
addHeader?: boolean,
cause?: unknown
): SqlError;
readonly prototype: SqlError;
}
declare const SqlError: SqlErrorConstructor;
export const enum TypeNumbers {
DECIMAL = 0,
TINY = 1,
SHORT = 2,
LONG = 3,
FLOAT = 4,
DOUBLE = 5,
NULL = 6,
TIMESTAMP = 7,
BIGINT = 8,
INT24 = 9,
DATE = 10,
TIME = 11,
DATETIME = 12,
YEAR = 13,
NEWDATE = 14,
VARCHAR = 15,
BIT = 16,
TIMESTAMP2 = 17,
DATETIME2 = 18,
TIME2 = 19,
JSON = 245, //only for MySQ,
NEWDECIMAL = 246,
ENUM = 247,
SET = 248,
TINY_BLOB = 249,
MEDIUM_BLOB = 250,
LONG_BLOB = 251,
BLOB = 252,
VAR_STRING = 253,
STRING = 254,
GEOMETRY = 255
}
export const enum Flags {
//field cannot be null
NOT_NULL = 1,
//field is a primary key
PRIMARY_KEY = 2,
// the field is unique
UNIQUE_KEY = 4,
//field is in a multiple key
MULTIPLE_KEY = 8,
//is this field a Blob?
BLOB = 1 << 4,
// is this field unsigned?
UNSIGNED = 1 << 5,
//is this field a zerofill?
ZEROFILL_FLAG = 1 << 6,
//whether this field has a binary collation
BINARY_COLLATION = 1 << 7,
//Field is an enumeration
ENUM = 1 << 8,
//field auto-increment
AUTO_INCREMENT = 1 << 9,
//field is a timestamp value
TIMESTAMP = 1 << 10,
//field is a SET
SET = 1 << 11,
//field doesn't have default value
NO_DEFAULT_VALUE_FLAG = 1 << 12,
//field is set to NOW on UPDATE
ON_UPDATE_NOW_FLAG = 1 << 13,
//field is num
NUM_FLAG = 1 << 14
}
export const enum Types {
DECIMAL = 'DECIMAL',
TINY = 'TINY',
SHORT = 'SHORT',
LONG = 'LONG',
FLOAT = 'FLOAT',
DOUBLE = 'DOUBLE',
NULL = 'NULL',
TIMESTAMP = 'TIMESTAMP',
BIGINT = 'BIGINT',
INT24 = 'INT24',
DATE = 'DATE',
TIME = 'TIME',
DATETIME = 'DATETIME',
YEAR = 'YEAR',
NEWDATE = 'NEWDATE',
VARCHAR = 'VARCHAR',
BIT = 'BIT',
TIMESTAMP2 = 'TIMESTAMP2',
DATETIME2 = 'DATETIME2',
TIME2 = 'TIME2',
JSON = 'JSON',
NEWDECIMAL = 'NEWDECIMAL',
ENUM = 'ENUM',
SET = 'SET',
TINY_BLOB = 'TINY_BLOB',
MEDIUM_BLOB = 'MEDIUM_BLOB',
LONG_BLOB = 'LONG_BLOB',
BLOB = 'BLOB',
VAR_STRING = 'VAR_STRING',
STRING = 'STRING',
GEOMETRY = 'GEOMETRY'
}
export interface Collation {
index: number;
name: string;
encoding: string;
maxLength: number;
fromEncoding(encoding: string): Collation;
fromIndex(index: number): Collation;
fromName(name: string): Collation;
}
export interface QueryConfig {
/**
* Presents result-sets by table to avoid results with colliding fields.
* See the query() description for more information.
*/
nestTables?: boolean | string;
/**
* Allows casting result types.
*/
typeCast?: TypeCastFunction;
/**
* Return result-sets as array, rather than a JSON object. This is a faster way to get results
*/
rowsAsArray?: boolean;
/**
* Compatibility option, causes Promise to return an array object,
* `[rows, metadata]` rather than the rows as JSON objects with a `meta` property.
* Default to false.
*/
metaAsArray?: boolean;
/**
* force returning insertId as Number in place of BigInt
*/
insertIdAsNumber?: boolean;
/**
* Whether to retrieve dates as strings or as Date objects.
*/
dateStrings?: boolean;
/**
* Forces use of the indicated timezone, rather than the current Node.js timezone.
* Possible values are Z for UTC, local or ±HH:MM format
*/
timezone?: string;
/**
* Allows the use of named placeholders.
*/
namedPlaceholders?: boolean;
/**
* Compatibility option to permit setting multiple value by a JSON object to replace one question mark.
* key values will replace the question mark with format like key1=val,key2='val2'.
* Since it doesn't respect the usual prepared statement format that one value is for one question mark,
* this can lead to incomprehension, even if badly use to possible injection.
*/
permitSetMultiParamEntries?: boolean;
/**
* disabled bulk command in batch.
*/
bulk?: boolean;
/**
* Sends queries one by one without waiting on the results of the previous entry.
* (Default: true)
*/
pipelining?: boolean;
/**
* Force server version check by explicitly using SELECT VERSION(), not relying on server initial handshake
* information
*/
forceVersionCheck?: boolean;
/**
* Allows the use of LOAD DATA INFILE statements.
* Loading data from a file from the client may be a security issue, as a man-in-the-middle proxy server can change
* the actual file the server loads. Being able to execute a query on the client gives you access to files on
* the client.
* (Default: false)
*/
permitLocalInfile?: boolean;
/**
* Allows timeout for command execution.
*/
timeout?: number;
/**
* indicate if JSON fields for MariaDB server 10.5.2+ results in JSON format (or String if disabled)
*/
autoJsonMap?: boolean;
/**
* Indicate if array are included in parenthesis. This option permit compatibility with version < 2.5
*/
arrayParenthesis?: boolean;
/**
* indicate to throw an exception if result-set will not contain some data due to having duplicate identifier
* (Default: true)
*/
checkDuplicate?: boolean;
/**
* force returning decimal values as Number in place of String
*
* Default: false;
*/
decimalAsNumber?: boolean;
/**
* Force returning BIGINT data as Number in place of BigInt.
*
* Default: false;
*/
bigIntAsNumber?: boolean;
/**
* @deprecated big numbers (BIGINT and DECIMAL columns) will result as string when not in safe number range.
* now replaced by decimalAsNumber, bigIntAsNumber and checkNumberRange options
*/
supportBigNumbers?: boolean;
/**
* @deprecated when used with supportBigNumbers, big numbers (BIGINT and DECIMAL columns) will always result as string
* even if in safe number range.
* now replaced by decimalAsNumber, bigIntAsNumber and checkNumberRange options
*/
bigNumberStrings?: boolean;
/**
* Throw if conversion to Number is not safe.
*
* Default: false;
*/
checkNumberRange?: boolean;
/**
* Configure logger
*/
logger?: LoggerConfig;
/**
* Permit to defined function to call for LOAD LOCAL command, for extra verification like path restriction.
* @param filepath
*/
infileStreamFactory?: (filepath: string) => stream.Readable;
}
export interface QueryOptions extends QueryConfig {
/**
* SQL command to execute
*/
sql: string;
}
export interface ConnectionConfig extends UserConnectionConfig, Omit<QueryConfig, 'timeout'> {
/**
* The hostname of the database you are connecting to. (Default: localhost)
*/
host?: string;
/**
* The port number to connect to. (Default: 3306)
*/
port?: number;
/**
* The path to an unix domain socket to connect to. When used host and port are ignored
*/
socketPath?: string;
/**
* The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 1000)
*/
connectTimeout?: number;
/**
* Socket timeout in milliseconds after the connection is established
*/
socketTimeout?: number;
/**
* Allows timeout for command execution.
*/
queryTimeout?: number;
/**
* This will print all incoming and outgoing packets on stdout.
* (Default: false)
*/
debug?: boolean;
/**
* This will print all incoming and outgoing compressed packets on stdout.
* (Default: false)
*/
debugCompress?: boolean;
/**
* When debugging, maximum packet length to write to console.
* (Default: 256)
*/
debugLen?: number;
/**
* indicate if parameters must be logged by query logger
* (Default: false)
*/
logParam?: boolean;
/**
* Adds the stack trace at the time of query creation to the error stack trace, making it easier to identify the
* part of the code that issued the query.
* Note: This feature is disabled by default due to the performance cost of stack creation.
* Only turn it on when you need to debug issues.
* (Default: false)
*/
trace?: boolean;
/**
* Allow multiple mysql statements per query. Be careful with this, it exposes you to SQL injection attacks.
* (Default: false)
*/
multipleStatements?: boolean;
/**
* object with ssl parameters or a boolean to enable ssl without setting any other ssl option.
* see
* https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/master/documentation/connection-options.md#ssl
* for more information
*/
ssl?: boolean | (tls.SecureContextOptions & { rejectUnauthorized?: boolean });
/**
* Compress exchanges using gzip.
* This can give you better performance when accessing a database in a different location.
* (Default: false)
*/
compress?: boolean;
/**
* Debug option: permit to save last exchanged packet.
* Error messages will display those last exchanged packet.
*
* (Default: false)
*/
logPackets?: boolean;
/**
* Force server version check by explicitly using SELECT VERSION(), not relying on server initial packet.
* (Default: false)
*/
forceVersionCheck?: boolean;
/**
* When enabled, the update number corresponds to update rows.
* When disabled, it indicates the real rows changed.
*/
foundRows?: boolean;
/**
* When a connection is established, permit executing commands before using connection
*/
initSql?: string | string[];
/**
* Permit setting session variables when connecting.
* Example: sessionVariables:{'idle_transaction_timeout':10000}
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
sessionVariables?: any;
/* eslint-enable @typescript-eslint/no-explicit-any */
/**
* permit to indicate server global variable max_allowed_packet value to ensure efficient batching.
* default is 4Mb. see batch documentation
*/
maxAllowedPacket?: number;
/**
* permit enabling socket keeping alive, setting delay. 0 means aren't enabled.
* Keep in mind that this don't reset server
* [@@wait_timeout](https://mariadb.com/kb/en/library/server-system-variables/#wait_timeout)
* (use pool option idleTimeout for that).
* in ms
* (Default: 0)
*/
keepAliveDelay?: number;
/**
* Indicate path/content to MySQL server RSA public key.
* use requires Node.js v11.6+
*/
rsaPublicKey?: string;
/**
* Indicate path/content to MySQL server caching RSA public key.
* use requires Node.js v11.6+
*/
cachingRsaPublicKey?: string;
/**
* Indicate that if `rsaPublicKey` or `cachingRsaPublicKey` public key are not provided, if client can ask server
* to send public key.
* default: false
*/
allowPublicKeyRetrieval?: boolean;
/**
* force returning insertId as Number in place of BigInt
*
* Default: false;
*/
insertIdAsNumber?: boolean;
/**
* Indicate prepare cache size when using a prepared statement
*
* default to 256.
*/
prepareCacheLength?: number;
/**
* Permit setting stream.
*
* @param err error is any error occurs during stream creation
* @param stream if wanting to set a special stream (Standard socket will be created if not set)
*/
stream?: (callback?: typeof StreamCallback) => void;
/**
* make result-set metadata property enumerable.
* Default to false.
*/
metaEnumerable?: boolean;
/**
* Compatibility option, causes Promise to return an array object,
* `[rows, metadata]` rather than the rows as JSON objects with a `meta` property.
* Default to false.
*/
metaAsArray?: boolean;
/**
* Return result-sets as array, rather than a JSON object. This is a faster way to get results
*/
rowsAsArray?: boolean;
/**
* Permit to defined function to call for LOAD LOCAL command, for extra verification like path restriction.
* @param filepath
*/
infileStreamFactory?: (filepath: string) => stream.Readable;
}