UNPKG

@supercat1337/mysql-schema-parser

Version:

A library for parsing and working with MySQL database schema metadata.

678 lines (594 loc) 22 kB
// @ts-check /** * Database column metadata object describing a table column's structure * @typedef {Object} ColumnMetadataRaw * @property {string} TABLE_CATALOG Table catalog (typically 'def' in MySQL) * @property {string} TABLE_SCHEMA Database/schema name containing the table * @property {string} TABLE_NAME Name of the table * @property {string} COLUMN_NAME Name of the column * @property {number} ORDINAL_POSITION Column position in table (1-based index) * @property {string|null} COLUMN_DEFAULT Default value for the column * @property {'YES'|'NO'} IS_NULLABLE Whether the column is nullable * @property {string} DATA_TYPE Column's data type (e.g., 'int', 'varchar') * @property {number|null} CHARACTER_MAXIMUM_LENGTH Maximum length for string types (in characters) * @property {number|null} CHARACTER_OCTET_LENGTH Maximum length for string types (in bytes) * @property {number|null} NUMERIC_PRECISION Precision for numeric types * @property {number|null} NUMERIC_SCALE Scale for numeric types * @property {number|null} DATETIME_PRECISION Precision for datetime types * @property {string|null} CHARACTER_SET_NAME Character set for string types * @property {string|null} COLLATION_NAME Collation for string types * @property {string} COLUMN_TYPE Full column type description (e.g., 'int(10) unsigned') * @property {'PRI'|'UNI'|'MUL'|''} COLUMN_KEY Column index type (PRI=primary key, UNI=unique, etc.) * @property {string} EXTRA Additional information (e.g., 'auto_increment') * @property {string} PRIVILEGES Comma-separated column privileges * @property {string} COLUMN_COMMENT Column comment * @property {'NEVER'|'ALWAYS'|string} IS_GENERATED Whether column value is generated * @property {string|null} GENERATION_EXPRESSION Expression for generated columns */ /** * @typedef {Object} ColumnMetadataParams * @property {string} tableCatalog - Table catalog (usually 'def') * @property {string} tableSchema - Database/schema name * @property {string} tableName - Table name * @property {string} columnName - Column name * @property {number} ordinalPosition - Position in table (1-based) * @property {string|null} columnDefault - Default value * @property {'YES'|'NO'} isNullable - Nullable status * @property {string} dataType - Data type (e.g. 'int', 'varchar') * @property {number|null} characterMaximumLength - Max length for string types (characters) * @property {number|null} characterOctetLength - Max length for string types (bytes) * @property {number|null} numericPrecision - Precision for numeric types * @property {number|null} numericScale - Scale for numeric types * @property {number|null} datetimePrecision - Precision for datetime types * @property {string|null} characterSetName - Character set for string types * @property {string|null} collationName - Collation for string types * @property {string} columnType - Full column type (e.g. 'int(11) unsigned') * @property {'PRI'|'UNI'|'MUL'|''} columnKey - Key type (primary/unique/etc.) * @property {string} extra - Extra information (e.g. 'auto_increment') * @property {string} privileges - Column privileges * @property {string} columnComment - Column comment * @property {'NEVER'|'ALWAYS'|string} isGenerated - Generation status * @property {string|null} generationExpression - Generation expression */ /** * Validate a raw column metadata object against the expected structure and types. * Throws an error if the object is invalid. * @param {ColumnMetadataRaw} obj Raw column metadata object * @returns {true} If the object is valid * @throws {Error} If the object is invalid */ function assertColumnMetadataRaw(obj) { if (typeof obj !== "object" || obj === null) { throw new Error("Input must be a non-null object"); } const requiredKeys = [ "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "ORDINAL_POSITION", "IS_NULLABLE", "DATA_TYPE", "COLUMN_TYPE", "COLUMN_KEY", "EXTRA", "PRIVILEGES", "COLUMN_COMMENT", "IS_GENERATED", ]; for (const key of requiredKeys) { if (!(key in obj)) { throw new Error(`Missing required field: ${key}`); } } const validators = { TABLE_CATALOG: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, TABLE_SCHEMA: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, TABLE_NAME: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, COLUMN_NAME: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, ORDINAL_POSITION: (val) => typeof val === "number" || `Expected number, got ${typeof val}`, COLUMN_DEFAULT: (val) => val === null || typeof val === "string" || `Expected string or null, got ${typeof val}`, IS_NULLABLE: (val) => val === "YES" || val === "NO" || `Expected 'YES' or 'NO', got ${val}`, DATA_TYPE: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, CHARACTER_MAXIMUM_LENGTH: (val) => val === null || typeof val === "number" || `Expected number or null, got ${typeof val}`, CHARACTER_OCTET_LENGTH: (val) => val === null || typeof val === "number" || `Expected number or null, got ${typeof val}`, NUMERIC_PRECISION: (val) => val === null || typeof val === "number" || `Expected number or null, got ${typeof val}`, NUMERIC_SCALE: (val) => val === null || typeof val === "number" || `Expected number or null, got ${typeof val}`, DATETIME_PRECISION: (val) => val === null || typeof val === "number" || `Expected number or null, got ${typeof val}`, CHARACTER_SET_NAME: (val) => val === null || typeof val === "string" || `Expected string or null, got ${typeof val}`, COLLATION_NAME: (val) => val === null || typeof val === "string" || `Expected string or null, got ${typeof val}`, COLUMN_TYPE: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, COLUMN_KEY: (val) => ["PRI", "UNI", "MUL", ""].includes(val) || `Expected 'PRI', 'UNI', 'MUL' or empty string, got ${val}`, EXTRA: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, PRIVILEGES: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, COLUMN_COMMENT: (val) => typeof val === "string" || `Expected string, got ${typeof val}`, IS_GENERATED: (val) => val === "NEVER" || val === "ALWAYS" || typeof val === "string" || `Expected 'NEVER', 'ALWAYS' or string, got ${val}`, GENERATION_EXPRESSION: (val) => val === null || typeof val === "string" || `Expected string or null, got ${typeof val}`, }; for (const [key, validator] of Object.entries(validators)) { const validationResult = validator(obj[key]); if (typeof validationResult === "string") { throw new Error(`Invalid ${key}: ${validationResult}`); } } return true; } /** * Class representing normalized database column metadata */ class MySQLTableColumn { /** * Table catalog (typically 'def' in MySQL) * @type {string} */ tableCatalog; /** * Database/schema name containing the table * @type {string} */ tableSchema; /** * Name of the table * @type {string} */ tableName; /** * Name of the column * @type {string} */ columnName; /** * Column position in table (1-based index) * @type {number} */ ordinalPosition; /** * Default value for the column * @type {string|null} */ columnDefault; /** * Whether the column is nullable * @type {'YES'|'NO'} */ isNullable; /** * Column's data type (e.g., 'int', 'varchar') * @type {string} */ dataType; /** * Maximum length for string types (in characters) * @type {number|null} */ characterMaximumLength; /** * Maximum length for string types (in bytes) * @type {number|null} */ characterOctetLength; /** * Precision for numeric types * @type {number|null} */ numericPrecision; /** * Scale for numeric types * @type {number|null} */ numericScale; /** * Precision for datetime types * @type {number|null} */ datetimePrecision; /** * Character set for string types * @type {string|null} */ characterSetName; /** * Collation for string types * @type {string|null} */ collationName; /** * Full column type description (e.g., 'int(10) unsigned') * @type {string} */ columnType; /** * Column index type (PRI=primary key, UNI=unique, etc.) * @type {'PRI'|'UNI'|'MUL'|''} */ columnKey; /** * Additional information (e.g., 'auto_increment') * @type {string} */ extra; /** * Comma-separated column privileges * @type {string} */ privileges; /** * Column comment * @type {string} */ columnComment; /** * Whether column value is generated * @type {'NEVER'|'ALWAYS'|string} */ isGenerated; /** * Expression for generated columns * @type {string|null} */ generationExpression; /** * Creates an instance of ColumnMetadata from raw data * @param {ColumnMetadataParams} [data] */ constructor(data) { if (!data) return; this.tableCatalog = data.tableCatalog; this.tableSchema = data.tableSchema; this.tableName = data.tableName; this.columnName = data.columnName; this.ordinalPosition = data.ordinalPosition; this.columnDefault = data.columnDefault; this.isNullable = data.isNullable; this.dataType = data.dataType; this.characterMaximumLength = data.characterMaximumLength; this.characterOctetLength = data.characterOctetLength; this.numericPrecision = data.numericPrecision; this.numericScale = data.numericScale; this.datetimePrecision = data.datetimePrecision; this.characterSetName = data.characterSetName; this.collationName = data.collationName; this.columnType = data.columnType; this.columnKey = data.columnKey; this.extra = data.extra; this.privileges = data.privileges; this.columnComment = data.columnComment; this.isGenerated = data.isGenerated; this.generationExpression = data.generationExpression; } /** * Import raw metadata into this object * @param {ColumnMetadataRaw} rawMetadata */ importFromRawData(rawMetadata) { assertColumnMetadataRaw(rawMetadata); this.tableCatalog = rawMetadata.TABLE_CATALOG; this.tableSchema = rawMetadata.TABLE_SCHEMA; this.tableName = rawMetadata.TABLE_NAME; this.columnName = rawMetadata.COLUMN_NAME; this.ordinalPosition = rawMetadata.ORDINAL_POSITION; this.columnDefault = rawMetadata.COLUMN_DEFAULT; this.isNullable = rawMetadata.IS_NULLABLE; this.dataType = rawMetadata.DATA_TYPE; this.characterMaximumLength = rawMetadata.CHARACTER_MAXIMUM_LENGTH; this.characterOctetLength = rawMetadata.CHARACTER_OCTET_LENGTH; this.numericPrecision = rawMetadata.NUMERIC_PRECISION; this.numericScale = rawMetadata.NUMERIC_SCALE; this.datetimePrecision = rawMetadata.DATETIME_PRECISION; this.characterSetName = rawMetadata.CHARACTER_SET_NAME; this.collationName = rawMetadata.COLLATION_NAME; this.columnType = rawMetadata.COLUMN_TYPE; this.columnKey = rawMetadata.COLUMN_KEY; this.extra = rawMetadata.EXTRA; this.privileges = rawMetadata.PRIVILEGES; this.columnComment = rawMetadata.COLUMN_COMMENT; this.isGenerated = rawMetadata.IS_GENERATED; this.generationExpression = rawMetadata.GENERATION_EXPRESSION; } /** * Check if column is a primary key * @returns {boolean} */ isPrimaryKey() { return this.columnKey === "PRI"; } /** * Check if column allows NULL values * @returns {boolean} */ allowsNull() { return this.isNullable === "YES"; } /** * Check if column auto-increments * @returns {boolean} */ isAutoIncrement() { return this.extra.includes("auto_increment"); } /** * Get full column definition as string * @returns {string} */ getColumnDefinition() { return ( `${this.columnName} ${this.columnType}` + (this.isPrimaryKey() ? " PRIMARY KEY" : "") + (this.isAutoIncrement() ? " AUTO_INCREMENT" : "") + (this.allowsNull() ? "" : " NOT NULL") ); } /** * Get a JSON representation of the column metadata * @returns {ColumnMetadataParams} JSON-serializable object with column metadata */ toJSON() { return { ...this, }; } } class MySQLDatabase { /** @type {string} */ databaseName; /** @type {Map<string, MySQLTable>} */ tables = new Map(); /** * Creates an instance of MySQLDatabase. * * @param {string} databaseName - The name of the database. * @param {ColumnMetadataRaw[]} [cols=[]] - An array of table objects with table name and columns metadata. */ constructor(databaseName, cols = []) { this.databaseName = databaseName; let tableNames = new Set(); // Validate and import tables for (let i = 0; i < cols.length; i++) { tableNames.add(cols[i].TABLE_NAME); } for (let tableName of tableNames) { let tableCols = cols.filter((col) => col.TABLE_NAME === tableName); if (tableCols.length === 0) { continue; // Skip empty tables } let table = new MySQLTable(tableName, tableCols); this.addTable(table); } } /** * Adds a table to the database. * * @param {MySQLTable} table - The table to add. */ addTable(table) { this.tables.set(table.tableName, table); } /** * Get all table names in the database * @returns {string[]} An array of table names */ getTableNames() { return Array.from(this.tables.keys()); } } class MySQLTable { /** @type {string} */ tableName; /** @type {Map<string, MySQLTableColumn>} */ columns = new Map(); /** * Creates MySQLTable instance from table name and columns data * @param {string} tableName Table name * @param {ColumnMetadataRaw[]} columns Columns data in snake_case format */ constructor(tableName, columns = []) { this.tableName = tableName; for (let i = 0; i < columns.length; i++) { let column = new MySQLTableColumn(); if (columns[i].TABLE_NAME !== tableName) { continue; } column.importFromRawData(columns[i]); this.columns.set(columns[i].COLUMN_NAME, column); } } /** * Adds a column to the table * @param {MySQLTableColumn} column The column to add */ addColumn(column) { this.columns.set(column.columnName, column); } /** * Get all columns in table * @returns {MySQLTableColumn[]} */ getColumns() { return Array.from(this.columns.values()); } /** * Get column by name * @param {string} columnName * @returns {MySQLTableColumn|null} */ getColumn(columnName) { return this.columns.get(columnName) || null; } /** * Generates CREATE TABLE SQL statement based on table metadata * @param {Object} [options] Additional options * @param {string} [options.engine] Storage engine (e.g. 'InnoDB') * @param {string} [options.charset] Default charset (e.g. 'utf8mb4') * @param {string} [options.collation] Default collation (e.g. 'utf8mb4_unicode_ci') * @param {string} [options.comment] Table comment * @returns {string} CREATE TABLE SQL query */ generateCreateTableQuery(options = {}) { const columns = this.getColumns(); if (columns.length === 0) { throw new Error(`Table ${this.tableName} has no columns`); } const columnDefinitions = []; const primaryKeys = []; const uniqueKeys = []; const indexes = []; for (const column of columns) { let definition = `\`${column.columnName}\` ${column.columnType}`; // NOT NULL if (!column.allowsNull()) { definition += " NOT NULL"; } // DEFAULT if (column.columnDefault !== null) { const defaultValue = this.#formatDefaultValue(column); definition += ` DEFAULT ${defaultValue}`; } // AUTO_INCREMENT if (column.isAutoIncrement()) { definition += " AUTO_INCREMENT"; } // COMMENT if (column.columnComment) { definition += ` COMMENT '${this.#escapeString( column.columnComment )}'`; } columnDefinitions.push(definition); // Indexes if (column.isPrimaryKey()) { primaryKeys.push(`\`${column.columnName}\``); } else if (column.columnKey === "UNI") { uniqueKeys.push(`\`${column.columnName}\``); } else if (column.columnKey === "MUL") { indexes.push(`\`${column.columnName}\``); } } if (primaryKeys.length > 0) { columnDefinitions.push(`PRIMARY KEY (${primaryKeys.join(", ")})`); } for (const uniqueCol of uniqueKeys) { columnDefinitions.push( `UNIQUE KEY \`${uniqueCol.replace( /`/g, "" )}_unique\` (${uniqueCol})` ); } let query = `CREATE TABLE \`${this.tableName}\` (\n `; query += columnDefinitions.join(",\n "); query += "\n)"; if (options.engine) { query += ` ENGINE=${options.engine}`; } const charset = options.charset || columns[0].characterSetName || "utf8mb4"; const collation = options.collation || columns[0].collationName || "utf8mb4_unicode_ci"; query += ` DEFAULT CHARSET=${charset} COLLATE=${collation}`; if (options.comment) { query += ` COMMENT='${this.#escapeString(options.comment)}'`; } return query + ";"; } /** * Formats default value for SQL query * @param {MySQLTableColumn} column * @returns {string} */ #formatDefaultValue(column) { if (column.columnDefault === null) return "NULL"; if ( ["char", "varchar", "text", "enum", "set"].includes( column.dataType.toLowerCase() ) ) { return `'${this.#escapeString(column.columnDefault)}'`; } if ( ["timestamp", "datetime"].includes(column.dataType.toLowerCase()) && column.columnDefault.toUpperCase() === "CURRENT_TIMESTAMP" ) { return "CURRENT_TIMESTAMP"; } if (["blob", "binary"].includes(column.dataType.toLowerCase())) { return `x'${column.columnDefault}'`; } return column.columnDefault; } /** * Escapes string for SQL * @param {string} str * @returns {string} */ #escapeString(str) { return str.replace(/'/g, "''").replace(/\\/g, "\\\\"); } /** * Gets an array of column names in the table * @returns {string[]} */ getColumnNames() { return Array.from(this.columns.keys()); } } /** * Parses a MySQL schema definition into a structured representation * * @param {ColumnMetadataRaw[]} schema - MySQL schema definition * @returns {MySQLDatabase} Structured representation of the database */ function parseMySQLSchema(schema) { if (!Array.isArray(schema)) throw new Error("Schema must be an array"); if (schema.length === 0) throw new Error("Schema must not be empty"); let databaseName = schema[0].TABLE_SCHEMA; return new MySQLDatabase(databaseName, schema); } export { MySQLDatabase, MySQLTable, MySQLTableColumn, assertColumnMetadataRaw, parseMySQLSchema };