393 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			393 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| //  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;
 |