From e8ab4f9f65da8bcdbeaa698d3e9834e4bef79404 Mon Sep 17 00:00:00 2001 From: Ricky Lee Whittemore Date: Thu, 6 Nov 2025 12:40:14 -0500 Subject: [PATCH] fix(mysql): Escape column names with backticks to handle reserved keywords Fixes issue where MySQL tables with reserved keyword columns (e.g., `order`) cannot be queried due to missing backtick escaping in generated SQL. Changes: - Add flavor parameter to constructSQLQuery() and efficientConstructSQLQuery() - Use flavor.Quote() to properly escape column names based on database type - MySQL uses backticks (`column`), PostgreSQL uses double quotes ("column") - Apply escaping to both SELECT columns and ORDER BY clauses Impact: - Enables querying WordPress Content Connect tables (wp_post_to_post) - Fixes queries on any MySQL table with reserved keyword columns - Similar to PR #57 which fixed PostgreSQL column name handling Tested: - Successfully queried wp_post_to_post table with 'order' column - Verified COUNT, SELECT, and aggregation queries work correctly - Tested with 284,909 relationship records Example working query: SELECT id1, id2, name FROM msresearch.wp_post_to_post LIMIT 5 Previously failed with: Error 1064: syntax error near 'order FROM `local`.`wp_post_to_post`' --- module/clickhouse.go | 2 +- module/db_helper.go | 22 +++++++++++++++------- module/duckdb.go | 2 +- module/mysql.go | 2 +- module/postgres.go | 2 +- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/module/clickhouse.go b/module/clickhouse.go index 6bd862d..79735fd 100644 --- a/module/clickhouse.go +++ b/module/clickhouse.go @@ -375,7 +375,7 @@ func (t *ClickHouseTable) Destroy() error { // To find the method, we will ask the database to explain the query and return the best method func (t *ClickHouseTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy, info sqlite3.IndexInformation) (*sqlite3.IndexResult, error) { // Create the SQL query - queryBuilder, limitCstIndex, offsetCstIndex, used := efficientConstructSQLQuery(cst, ob, t.schema, t.tableName, info.ColUsed) + queryBuilder, limitCstIndex, offsetCstIndex, used := efficientConstructSQLQuery(cst, ob, t.schema, t.tableName, info.ColUsed, sqlbuilder.ClickHouse) queryBuilder.SetFlavor(sqlbuilder.ClickHouse) rawQuery, args := queryBuilder.Build() rawQuery += sqlQuerySuffix diff --git a/module/db_helper.go b/module/db_helper.go index 4775110..cb27984 100644 --- a/module/db_helper.go +++ b/module/db_helper.go @@ -30,15 +30,18 @@ func constructSQLQuery( ob []sqlite3.InfoOrderBy, columns []databaseColumn, table string, + flavor sqlbuilder.Flavor, ) (query *sqlbuilder.SelectBuilder, limit int, offset int, used []bool) { // Initialize the SQL query builder query = sqlbuilder.NewSelectBuilder() + query.SetFlavor(flavor) // Add all the columns to the query cols := []string{} for _, col := range columns { - cols = append(cols, col.Realname) + // Quote column names to handle reserved keywords (e.g., `order` for MySQL) + cols = append(cols, flavor.Quote(col.Realname)) } query.Select(cols...).From(table) @@ -112,10 +115,11 @@ func constructSQLQuery( // Add the order by for _, o := range ob { + quotedCol := flavor.Quote(columns[o.Column].Realname) if o.Desc { - query.OrderBy(columns[o.Column].Realname + " DESC") + query.OrderBy(quotedCol + " DESC") } else { - query.OrderBy(columns[o.Column].Realname + " ASC") + query.OrderBy(quotedCol + " ASC") } } @@ -129,11 +133,13 @@ func efficientConstructSQLQuery( columns []databaseColumn, table string, colUsed uint64, + flavor sqlbuilder.Flavor, ) (query *sqlbuilder.SelectBuilder, limit int, offset int, used []bool) { // Initialize the SQL query builder query = sqlbuilder.NewSelectBuilder() + query.SetFlavor(flavor) // Add all the columns to the query cols := []string{} for i, col := range columns { @@ -141,7 +147,8 @@ func efficientConstructSQLQuery( // If the column is not used, we skip it continue } - cols = append(cols, col.Realname) + // Quote column names to handle reserved keywords (e.g., `order` for MySQL) + cols = append(cols, flavor.Quote(col.Realname)) } // If no columns are used, we add the first one @@ -149,7 +156,7 @@ func efficientConstructSQLQuery( // When SQLite does a SELECT count(*), it doesn't use any column, so we need to add at least one column // because most SQL engines require at least one column in the SELECT clause. if len(cols) == 0 { - cols = append(cols, columns[0].Realname) + cols = append(cols, flavor.Quote(columns[0].Realname)) } query.Select(cols...).From(table) @@ -223,10 +230,11 @@ func efficientConstructSQLQuery( // Add the order by for _, o := range ob { + quotedCol := flavor.Quote(columns[o.Column].Realname) if o.Desc { - query.OrderBy(columns[o.Column].Realname + " DESC") + query.OrderBy(quotedCol + " DESC") } else { - query.OrderBy(columns[o.Column].Realname + " ASC") + query.OrderBy(quotedCol + " ASC") } } diff --git a/module/duckdb.go b/module/duckdb.go index 5425410..1706a31 100644 --- a/module/duckdb.go +++ b/module/duckdb.go @@ -275,7 +275,7 @@ func (t *DuckDBTable) Destroy() error { // To find the method, we will ask the database to explain the query and return the best method func (t *DuckDBTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy, info sqlite3.IndexInformation) (*sqlite3.IndexResult, error) { // Create the SQL query - queryBuilder, limitCstIndex, offsetCstIndex, used := efficientConstructSQLQuery(cst, ob, t.schema, t.tableName, info.ColUsed) + queryBuilder, limitCstIndex, offsetCstIndex, used := efficientConstructSQLQuery(cst, ob, t.schema, t.tableName, info.ColUsed, sqlbuilder.PostgreSQL) queryBuilder.SetFlavor(sqlbuilder.PostgreSQL) rawQuery, args := queryBuilder.Build() diff --git a/module/mysql.go b/module/mysql.go index a688210..ba892aa 100644 --- a/module/mysql.go +++ b/module/mysql.go @@ -476,7 +476,7 @@ func (t *MySQLTable) Destroy() error { // To find the method, we will ask the database to explain the query and return the best method func (t *MySQLTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy, info sqlite3.IndexInformation) (*sqlite3.IndexResult, error) { // Create the SQL query - queryBuilder, limitCstIndex, offsetCstIndex, used := constructSQLQuery(cst, ob, t.schema, t.tableName) + queryBuilder, limitCstIndex, offsetCstIndex, used := constructSQLQuery(cst, ob, t.schema, t.tableName, sqlbuilder.MySQL) queryBuilder.SetFlavor(sqlbuilder.MySQL) rawQuery, args := queryBuilder.Build() rawQuery += sqlQuerySuffix diff --git a/module/postgres.go b/module/postgres.go index 2f37b7d..e7ff1b6 100644 --- a/module/postgres.go +++ b/module/postgres.go @@ -336,7 +336,7 @@ func (t *PostgresTable) Destroy() error { // To find the method, we will ask the database to explain the query and return the best method func (t *PostgresTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy, info sqlite3.IndexInformation) (*sqlite3.IndexResult, error) { // Create the SQL query - queryBuilder, limitCstIndex, offsetCstIndex, used := constructSQLQuery(cst, ob, t.schema, t.tableName) + queryBuilder, limitCstIndex, offsetCstIndex, used := constructSQLQuery(cst, ob, t.schema, t.tableName, sqlbuilder.PostgreSQL) queryBuilder.SetFlavor(sqlbuilder.PostgreSQL) rawQuery, args := queryBuilder.Build() rawQuery += sqlQuerySuffix