node.jsexpresssequelize.jsmysql-connector

How to get list of column names from a query that won't return any rows


I am using MySQL, ExpressJS, and Sequelize to build a function that returns a list of columns from a given SQL query. The function should work even if the query returns no rows.

Here is the current implementation of my function:

async getColumnsFromQuery ( query ) {
    try {
        let queryResponse = await db.sequelize.query ( query, {
            type: db.sequelize.QueryTypes.SELECT
        });
        
        let columns = [];
        
        if ( queryResponse.length > 0 ) {
            columns = Object.keys( queryResponse[ 0 ] );
        }
        
        return columns;

    } catch ( err ) {
        throw err;
    }
}

This function works perfectly when the query returns rows, but if the query returns no rows, the columns array is empty. I need to ensure that the function still returns the correct column names even when no data is returned.

I tried using INFORMATION_SCHEMA , SHOW COLUMNS or DESCRIBE but they are only for tables not queries.


Solution

  • I think I was able to find a way to get all the column names from a query including the datatypes of columns.

    I used node-sql-parser a library that parses SQL statements. By using the result I got from parsing the query, I was able to get all the column names.

    Here is how I did it:

    const { Parser } = require('node-sql-parser');
    
    // For SELECT TableName.* FROM ....
    getAllColumnsFromAllTables (allTableAllColumns) {
        const TablesFromDB = Object.keys(allTableAllColumns);
        return TablesFromDB.map(dbTable => {
            return allTableAllColumns[dbTable].columns;
        });
    }
    // For SELECT TableName.* FROM ....
    getAllColumnsFromATable(allTableAllColumns, tableAlias) {
        const TablesFromDB = Object.keys(allTableAllColumns);
        for ( let i = 0 ; i < TablesFromDB.length; i++ ) {
            if (TablesFromDB[i].toLowerCase() === tableAlias[table].toLowerCase()) {
                // let tableColumns = allTableAllColumns[TablesFromDB[i]].columns.map( column => `${table.as ? table.as : table.table}.${column}`)
                return allTableAllColumns[TablesFromDB[i]].columns;
            }
        }
    }
    // For SELECT TableName.ColumnName FROM ....
    getAColumnFromATable(allTableAllColumns, tableAlias, col) {
        const TablesFromDB = Object.keys(allTableAllColumns);
        for ( let i = 0 ; i < TablesFromDB.length; i++ ) {
            if (TablesFromDB[i].toLowerCase() === tableAlias[table].toLowerCase()) {
                for (let j = 0; j < allTableAllColumns[TablesFromDB[i]].columns.length; j++) {
                    const dbCol = allTableAllColumns[TablesFromDB[i]].columns[j];
                    if (dbCol.COLUMN_NAME.toLowerCase() === col.expr.column.toLowerCase()) {
                        return dbCol;
                    }
                }
            }
        }
    }
    // For SELECT ColumnName FROM ....
    getAColumnFromAllTables (allTableAllColumns, col) {
        const TablesFromDB = Object.keys(allTableAllColumns);
        for ( let i = 0 ; i < TablesFromDB.length; i++ ) {
            for (let j = 0; j < allTableAllColumns[TablesFromDB[i]].columns.length; j++) {
                const dbCol = allTableAllColumns[TablesFromDB[i]].columns[j];
                if (dbCol.COLUMN_NAME.toLowerCase() === col.expr.column.toLowerCase()) {
                    return dbCol;
                }
            }
        }
    }
    // Gets all columns of tables from database
    async getAllColumnsOfTablesFromDB(tableNames) {
        const allTableAllColumns = {};
    
        var query = `
            SELECT 
                TABLE_NAME,
                COLUMN_NAME,
                DATA_TYPE,
                CHARACTER_MAXIMUM_LENGTH,
                NUMERIC_PRECISION,
                NUMERIC_SCALE
            FROM 
                INFORMATION_SCHEMA.COLUMNS
            WHERE 
                TABLE_NAME IN (:tableNames)
                AND TABLE_SCHEMA = :DB_NAME;        
        `;
    
        await db.sequelize.query(query,  {
            replacements: {
                tableNames: tableNames,
                DB_NAME: this.dbConfig.database
            }
        }).spread((results, metadata) => {
            results.map(row => {
                if (allTableAllColumns[row.TABLE_NAME] && allTableAllColumns[row.TABLE_NAME].columns) {
                    allTableAllColumns[row.TABLE_NAME].columns.push(row)
                } else {
                    allTableAllColumns[row.TABLE_NAME] = {};
                    allTableAllColumns[row.TABLE_NAME].columns = [row]
                }
            });
        });
    
        return allTableAllColumns;
    }
    
    async getColumnNames(ast) {
        let columns = [];
        let tableAlias = {};
        let tableNames = ast.from.map( t => t.table );
        let allTableAllColumns = await this.getAllColumnsOfTablesFromDB(tableNames);
    
        ast.from.forEach (table => {
            if (table.as) {
                tableAlias[table.as] = table.table
            } else {
                tableAlias[table.table] = table.table
            }
        });
    
        ast.columns.forEach(col => {
            if (col.expr.type === 'column_ref') {
                const table = col.expr.table;
                const column = col.expr.column === '*' ? '*' : `${table}.${col.expr.column}`;
    
                if (!col.expr.table && col.expr.column === '*') {
                    // For SELECT * FROM ....
                    return columns.push(...this.getAllColumnsFromAllTables(allTableAllColumns));
                } else if (col.expr.table && column === '*') {
                    // For SELECT TableName.* FROM ....
                    return columns.push(...this.getAllColumnsFromATable (allTableAllColumns, tableAlias));
                } else if (col.expr.table && col.expr.column) {
                    // For SELECT TableName.ColumnName FROM ....
                    return columns.push(this.getAColumnFromATable(allTableAllColumns, tableAlias, col));
                } else if (!col.expr.table && col.expr.column) {
                    // For SELECT ColumnName FROM ....
                    return columns.push(this.getAColumnFromAllTables(allTableAllColumns, col));
                }
            }
        });
    
        return columns;
    }
    
    async getColumnsFromQuery( query ) {
        try {
            await db.sequelize.query ( query ); // to check if query works
            const parser = new Parser();
    
            const pasredQuery = parser.astify(query);
            let columns = [];
    
            if (Array.isArray(pasredQuery)) {
                // only giving columns for 1st query if the result of parsed queries results in multiple queries
                columns = await this.getColumnNames( pasredQuery[0] );
            } else {
                columns = await this.getColumnNames( pasredQuery );
            }
    
            return response.SendResponse(res, columns, 0, "List of Columns");
    
        } catch ( err ) {
            return response.SendResponse(res, "An Error occoured", 1, err);
        }
    
    
    
    }
    

    Sample test of the above code:

    let query = `
        SELECT * FROM Customer INNER JOIN database.Order ON database.Order.CustomerId = Customer.Id;
    `
    getColumnsFromQuery ( query );
    

    Returns:

    [{
                "TABLE_NAME": "Customer",
                "COLUMN_NAME": "Id",
                "DATA_TYPE": "int",
                "CHARACTER_MAXIMUM_LENGTH": null,
                "NUMERIC_PRECISION": 10,
                "NUMERIC_SCALE": 0
            },
            {
                "TABLE_NAME": "Customer",
                "COLUMN_NAME": "Name",
                "DATA_TYPE": "varchar",
                "CHARACTER_MAXIMUM_LENGTH": 10,
                "NUMERIC_PRECISION": null,
                "NUMERIC_SCALE": null
            },
            {
                "TABLE_NAME": "Customer",
                "COLUMN_NAME": "Age",
                "DATA_TYPE": "int",
                "CHARACTER_MAXIMUM_LENGTH": 10,
                "NUMERIC_PRECISION": 10,
                "NUMERIC_SCALE": 0
            },
            .
            .
            .
            {
                "TABLE_NAME": "Order",
                "COLUMN_NAME": "OrderName",
                "DATA_TYPE": "varchar",
                "CHARACTER_MAXIMUM_LENGTH": 10,
                "NUMERIC_PRECISION": null,
                "NUMERIC_SCALE": null
            },
            {
                "TABLE_NAME": "Order",
                "COLUMN_NAME": "OrderDate",
                "DATA_TYPE": "datetime",
                "CHARACTER_MAXIMUM_LENGTH": null,
                "NUMERIC_PRECISION": null,
                "NUMERIC_SCALE": null
            },
            {
                "TABLE_NAME": "Customer",
                "COLUMN_NAME": "Age",
                "DATA_TYPE": "int",
                "CHARACTER_MAXIMUM_LENGTH": 10,
                "NUMERIC_PRECISION": 10,
                "NUMERIC_SCALE": 0
            },
            .
            .
            .
    ]
    

    You can modify the query in the getAllColumnsOfTablesFromDB to get any data about all the columns.

    I tested it with many queries including queries that use alias for table names and also for columns.

    This code can probably be refactored much further but this was the maximum I could do since I am not enough experienced to take this further.