292 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			292 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | 'use strict'; | ||
|  | 
 | ||
|  | const Packet = require('../packets/packet'); | ||
|  | const StringParser = require('../parsers/string'); | ||
|  | const CharsetToEncoding = require('../constants/charset_encodings.js'); | ||
|  | 
 | ||
|  | const fields = ['catalog', 'schema', 'table', 'orgTable', 'name', 'orgName']; | ||
|  | 
 | ||
|  | // creating JS string is relatively expensive (compared to
 | ||
|  | // reading few bytes from buffer) because all string properties
 | ||
|  | // except for name are unlikely to be used we postpone
 | ||
|  | // string conversion until property access
 | ||
|  | //
 | ||
|  | // TODO: watch for integration benchmarks (one with real network buffer)
 | ||
|  | // there could be bad side effect as keeping reference to a buffer makes it
 | ||
|  | // sit in the memory longer (usually until final .query() callback)
 | ||
|  | // Latest v8 perform much better in regard to bufferer -> string conversion,
 | ||
|  | // at some point of time this optimisation might become unnecessary
 | ||
|  | // see https://github.com/sidorares/node-mysql2/pull/137
 | ||
|  | //
 | ||
|  | class ColumnDefinition { | ||
|  |   constructor(packet, clientEncoding) { | ||
|  |     this._buf = packet.buffer; | ||
|  |     this._clientEncoding = clientEncoding; | ||
|  |     this._catalogLength = packet.readLengthCodedNumber(); | ||
|  |     this._catalogStart = packet.offset; | ||
|  |     packet.offset += this._catalogLength; | ||
|  |     this._schemaLength = packet.readLengthCodedNumber(); | ||
|  |     this._schemaStart = packet.offset; | ||
|  |     packet.offset += this._schemaLength; | ||
|  |     this._tableLength = packet.readLengthCodedNumber(); | ||
|  |     this._tableStart = packet.offset; | ||
|  |     packet.offset += this._tableLength; | ||
|  |     this._orgTableLength = packet.readLengthCodedNumber(); | ||
|  |     this._orgTableStart = packet.offset; | ||
|  |     packet.offset += this._orgTableLength; | ||
|  |     // name is always used, don't make it lazy
 | ||
|  |     const _nameLength = packet.readLengthCodedNumber(); | ||
|  |     const _nameStart = packet.offset; | ||
|  |     packet.offset += _nameLength; | ||
|  |     this._orgNameLength = packet.readLengthCodedNumber(); | ||
|  |     this._orgNameStart = packet.offset; | ||
|  |     packet.offset += this._orgNameLength; | ||
|  |     packet.skip(1); //  length of the following fields (always 0x0c)
 | ||
|  |     this.characterSet = packet.readInt16(); | ||
|  |     this.encoding = CharsetToEncoding[this.characterSet]; | ||
|  |     this.name = StringParser.decode( | ||
|  |       this._buf, | ||
|  |       this.encoding === 'binary' ? this._clientEncoding : this.encoding, | ||
|  |       _nameStart, | ||
|  |       _nameStart + _nameLength | ||
|  |     ); | ||
|  |     this.columnLength = packet.readInt32(); | ||
|  |     this.columnType = packet.readInt8(); | ||
|  |     this.type = this.columnType; | ||
|  |     this.flags = packet.readInt16(); | ||
|  |     this.decimals = packet.readInt8(); | ||
|  |   } | ||
|  | 
 | ||
|  |   inspect() { | ||
|  |     return { | ||
|  |       catalog: this.catalog, | ||
|  |       schema: this.schema, | ||
|  |       name: this.name, | ||
|  |       orgName: this.orgName, | ||
|  |       table: this.table, | ||
|  |       orgTable: this.orgTable, | ||
|  |       characterSet: this.characterSet, | ||
|  |       encoding: this.encoding, | ||
|  |       columnLength: this.columnLength, | ||
|  |       type: this.columnType, | ||
|  |       flags: this.flags, | ||
|  |       decimals: this.decimals, | ||
|  |     }; | ||
|  |   } | ||
|  | 
 | ||
|  |   [Symbol.for('nodejs.util.inspect.custom')](depth, inspectOptions, inspect) { | ||
|  |     const Types = require('../constants/types.js'); | ||
|  |     const typeNames = []; | ||
|  |     for (const t in Types) { | ||
|  |       typeNames[Types[t]] = t; | ||
|  |     } | ||
|  |     const fiedFlags = require('../constants/field_flags.js'); | ||
|  |     const flagNames = []; | ||
|  |     // TODO: respect inspectOptions.showHidden
 | ||
|  |     //const inspectFlags = inspectOptions.showHidden ? this.flags : this.flags & ~fiedFlags.PRI_KEY;
 | ||
|  |     const inspectFlags = this.flags; | ||
|  |     for (const f in fiedFlags) { | ||
|  |       if (inspectFlags & fiedFlags[f]) { | ||
|  |         if (f === 'PRI_KEY') { | ||
|  |           flagNames.push('PRIMARY KEY'); | ||
|  |         } else if (f === 'NOT_NULL') { | ||
|  |           flagNames.push('NOT NULL'); | ||
|  |         } else if (f === 'BINARY') { | ||
|  |           // ignore flag for now
 | ||
|  |         } else if (f === 'MULTIPLE_KEY') { | ||
|  |           // not sure if that should be part of inspection.
 | ||
|  |           // in the schema usually this is part of index definition
 | ||
|  |           // example: UNIQUE KEY `my_uniq_id` (`id_box_elements`,`id_router`)
 | ||
|  |           // note that only first column has MULTIPLE_KEY flag set in this case
 | ||
|  |           // so there is no good way of knowing that this is part of index just
 | ||
|  |           // by looking at indifidual field flags
 | ||
|  |         } else if (f === 'NO_DEFAULT_VALUE') { | ||
|  |           // almost the same as NOT_NULL?
 | ||
|  |         } else if (f === 'BLOB') { | ||
|  |           // included in the type
 | ||
|  |         } else if (f === 'UNSIGNED') { | ||
|  |           // this should be first after type
 | ||
|  |         } else if (f === 'TIMESTAMP') { | ||
|  |           // timestamp flag is redundant for inspection - already included in type
 | ||
|  |         } else if (f === 'ON_UPDATE_NOW') { | ||
|  |           flagNames.push('ON UPDATE CURRENT_TIMESTAMP'); | ||
|  |         } else { | ||
|  |           flagNames.push(f); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (depth > 1) { | ||
|  |       return inspect({ | ||
|  |         ...this.inspect(), | ||
|  |         typeName: typeNames[this.columnType], | ||
|  |         flags: flagNames, | ||
|  |       }); | ||
|  |     } | ||
|  | 
 | ||
|  |     const isUnsigned = this.flags & fiedFlags.UNSIGNED; | ||
|  | 
 | ||
|  |     let typeName = typeNames[this.columnType]; | ||
|  |     if (typeName === 'BLOB') { | ||
|  |       // TODO: check for non-utf8mb4 encoding
 | ||
|  |       if (this.columnLength === 4294967295) { | ||
|  |         typeName = 'LONGTEXT'; | ||
|  |       } else if (this.columnLength === 67108860) { | ||
|  |         typeName = 'MEDIUMTEXT'; | ||
|  |       } else if (this.columnLength === 262140) { | ||
|  |         typeName = 'TEXT'; | ||
|  |       } else if (this.columnLength === 1020) { | ||
|  |         // 255*4
 | ||
|  |         typeName = 'TINYTEXT'; | ||
|  |       } else { | ||
|  |         typeName = `BLOB(${this.columnLength})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'VAR_STRING') { | ||
|  |       // TODO: check for non-utf8mb4 encoding
 | ||
|  |       typeName = `VARCHAR(${Math.ceil(this.columnLength / 4)})`; | ||
|  |     } else if (typeName === 'TINY') { | ||
|  |       if ( | ||
|  |         (this.columnLength === 3 && isUnsigned) || | ||
|  |         (this.columnLength === 4 && !isUnsigned) | ||
|  |       ) { | ||
|  |         typeName = 'TINYINT'; | ||
|  |       } else { | ||
|  |         typeName = `TINYINT(${this.columnLength})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'LONGLONG') { | ||
|  |       if (this.columnLength === 20) { | ||
|  |         typeName = 'BIGINT'; | ||
|  |       } else { | ||
|  |         typeName = `BIGINT(${this.columnLength})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'SHORT') { | ||
|  |       if (isUnsigned && this.columnLength === 5) { | ||
|  |         typeName = 'SMALLINT'; | ||
|  |       } else if (!isUnsigned && this.columnLength === 6) { | ||
|  |         typeName = 'SMALLINT'; | ||
|  |       } else { | ||
|  |         typeName = `SMALLINT(${this.columnLength})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'LONG') { | ||
|  |       if (isUnsigned && this.columnLength === 10) { | ||
|  |         typeName = 'INT'; | ||
|  |       } else if (!isUnsigned && this.columnLength === 11) { | ||
|  |         typeName = 'INT'; | ||
|  |       } else { | ||
|  |         typeName = `INT(${this.columnLength})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'INT24') { | ||
|  |       if (isUnsigned && this.columnLength === 8) { | ||
|  |         typeName = 'MEDIUMINT'; | ||
|  |       } else if (!isUnsigned && this.columnLength === 9) { | ||
|  |         typeName = 'MEDIUMINT'; | ||
|  |       } else { | ||
|  |         typeName = `MEDIUMINT(${this.columnLength})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'DOUBLE') { | ||
|  |       // DOUBLE without modifiers is reported as DOUBLE(22, 31)
 | ||
|  |       if (this.columnLength === 22 && this.decimals === 31) { | ||
|  |         typeName = 'DOUBLE'; | ||
|  |       } else { | ||
|  |         typeName = `DOUBLE(${this.columnLength},${this.decimals})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'FLOAT') { | ||
|  |       // FLOAT without modifiers is reported as FLOAT(12, 31)
 | ||
|  |       if (this.columnLength === 12 && this.decimals === 31) { | ||
|  |         typeName = 'FLOAT'; | ||
|  |       } else { | ||
|  |         typeName = `FLOAT(${this.columnLength},${this.decimals})`; | ||
|  |       } | ||
|  |     } else if (typeName === 'NEWDECIMAL') { | ||
|  |       if (this.columnLength === 11 && this.decimals === 0) { | ||
|  |         typeName = 'DECIMAL'; | ||
|  |       } else if (this.decimals === 0) { | ||
|  |         // not sure why, but DECIMAL(13) is reported as DECIMAL(14, 0)
 | ||
|  |         // and DECIMAL(13, 9) is reported as NEWDECIMAL(15, 9)
 | ||
|  |         if (isUnsigned) { | ||
|  |           typeName = `DECIMAL(${this.columnLength})`; | ||
|  |         } else { | ||
|  |           typeName = `DECIMAL(${this.columnLength - 1})`; | ||
|  |         } | ||
|  |       } else { | ||
|  |         typeName = `DECIMAL(${this.columnLength - 2},${this.decimals})`; | ||
|  |       } | ||
|  |     } else { | ||
|  |       typeName = `${typeNames[this.columnType]}(${this.columnLength})`; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (isUnsigned) { | ||
|  |       typeName += ' UNSIGNED'; | ||
|  |     } | ||
|  | 
 | ||
|  |     // TODO respect colors option
 | ||
|  |     return `\`${this.name}\` ${[typeName, ...flagNames].join(' ')}`; | ||
|  |   } | ||
|  | 
 | ||
|  |   static toPacket(column, sequenceId) { | ||
|  |     let length = 17; // = 4 padding + 1 + 12 for the rest
 | ||
|  |     fields.forEach((field) => { | ||
|  |       length += Packet.lengthCodedStringLength( | ||
|  |         column[field], | ||
|  |         CharsetToEncoding[column.characterSet] | ||
|  |       ); | ||
|  |     }); | ||
|  |     const buffer = Buffer.allocUnsafe(length); | ||
|  | 
 | ||
|  |     const packet = new Packet(sequenceId, buffer, 0, length); | ||
|  |     function writeField(name) { | ||
|  |       packet.writeLengthCodedString( | ||
|  |         column[name], | ||
|  |         CharsetToEncoding[column.characterSet] | ||
|  |       ); | ||
|  |     } | ||
|  |     packet.offset = 4; | ||
|  |     fields.forEach(writeField); | ||
|  |     packet.writeInt8(0x0c); | ||
|  |     packet.writeInt16(column.characterSet); | ||
|  |     packet.writeInt32(column.columnLength); | ||
|  |     packet.writeInt8(column.columnType); | ||
|  |     packet.writeInt16(column.flags); | ||
|  |     packet.writeInt8(column.decimals); | ||
|  |     packet.writeInt16(0); // filler
 | ||
|  |     return packet; | ||
|  |   } | ||
|  | 
 | ||
|  |   // node-mysql compatibility: alias "db" to "schema"
 | ||
|  |   get db() { | ||
|  |     return this.schema; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const addString = function (name) { | ||
|  |   Object.defineProperty(ColumnDefinition.prototype, name, { | ||
|  |     get: function () { | ||
|  |       const start = this[`_${name}Start`]; | ||
|  |       const end = start + this[`_${name}Length`]; | ||
|  |       const val = StringParser.decode( | ||
|  |         this._buf, | ||
|  |         this.encoding === 'binary' ? this._clientEncoding : this.encoding, | ||
|  |         start, | ||
|  |         end | ||
|  |       ); | ||
|  | 
 | ||
|  |       Object.defineProperty(this, name, { | ||
|  |         value: val, | ||
|  |         writable: false, | ||
|  |         configurable: false, | ||
|  |         enumerable: false, | ||
|  |       }); | ||
|  | 
 | ||
|  |       return val; | ||
|  |     }, | ||
|  |   }); | ||
|  | }; | ||
|  | 
 | ||
|  | addString('catalog'); | ||
|  | addString('schema'); | ||
|  | addString('table'); | ||
|  | addString('orgTable'); | ||
|  | addString('orgName'); | ||
|  | 
 | ||
|  | module.exports = ColumnDefinition; |