Background/Problem
According to Microsoft docs, the syntax for a CONTAINSTABLE query uses a CONTAINS search condition which as far as I can tell, must be entirely enclosed in single quotes. The problem occurs when I try to parameterize the query in pyodbc. Any form of quotation that is required for specifying the condition part of the query seems to cause pyodbc/the odbc driver to ignore the parameter markers, resulting in the infamous "The SQL contains x parameter markers, but >x parameters were supplied' error.
What I'm (Trying) to do
I get user search input that I break into a list of words and build a dynamic SQL CONTAINS search condition that puts in parameter markers instead of the actual words that is finally inserted into the rest of the SQL statement string in my cursor.execute() statement.
Ex: User searches for "Backflow prevention device"
Used this answer as the general way to solve the problem of putting multiple words in a fulltext CONTAINS/CONTAINSTABLE query. My dynamic CONTAINSTABLE condition is generated as ? AND ? AND ?
which is then inserted into my query (which actually contains this and another fulltext query combined with UNION
that uses the NEAR
keyword between words instead of AND
) via .format()
.
My Code
def parameterize(search_string, kw):
expanded = search_string.split(maxsplit=14)
sql = ""
for word in expanded:
sql = "{sql}{cond_keyword} ? ".format(sql=sql,
cond_keyword=kw if sql and len(expanded) > 1 else "",
word=word
)
sql = "{sql}".format(sql=sql.lstrip().rstrip())
qargs = tuple(expanded)
return sql, qargs
def ka_search(search_str):
and_cond = parameterize(search_str, "AND")
near_cond = parameterize(search_str, "NEAR")
near_params = near_cond[0]
and_params = and_cond[0]
# build a final tuple of all parameters in order of the individual sub-queries that are union-ed together
args1 = list(and_cond[1])
args2 = [search_str]
args3 = list(near_cond[1])
args = args1 + args2 + args3 + args2
try:
cursor = db_connect()
cursor.execute("""
SELECT Title, CONCAT(RTRIM(LEFT(Bodytext,150)), '...') as BodyText, Keywords, RecId,
rank, CONCAT('/kb/', RecId) AS Link
FROM KnowledgeArticle AS FT_TBL INNER JOIN
CONTAINSTABLE(KnowledgeArticle, (Title, keywords), {and_params}) AS KEY_TBL
ON FT_TBL.RecID = KEY_TBL.[KEY]
WHERE VisibleToCustomerPortal = 'true' AND
Status = 'Published'
UNION
SELECT Title, CONCAT(RTRIM(LEFT(Bodytext,150)), '...') as BodyText, Keywords, RecId,
rank, CONCAT('/kb/', RecId) AS Link
FROM KnowledgeArticle AS FT_TBL INNER JOIN
FREETEXTTABLE(KnowledgeArticle, Title, ?) AS KEY_TBL
ON FT_TBL.RecID = KEY_TBL.[KEY]
WHERE VisibleToCustomerPortal = 'true' AND
Status = 'Published'
UNION
SELECT Title, CONCAT(RTRIM(LEFT(Bodytext,150)), '...') as BodyText, Keywords, RecId,
rank, CONCAT('/kb/', RecId) AS Link
FROM KnowledgeArticle AS FT_TBL INNER JOIN
CONTAINSTABLE(KnowledgeArticle, (Title, keywords), {near_params}) AS KEY_TBL
ON FT_TBL.RecID = KEY_TBL.[KEY]
WHERE VisibleToCustomerPortal = 'true' AND
Status = 'Published'
SELECT Title, CONCAT(RTRIM(LEFT(Bodytext,150)), '...') as BodyText, Keywords, RecId,
rank, CONCAT('/kb/', RecId) AS Link
FROM KnowledgeArticle AS FT_TBL INNER JOIN
FREETEXTTABLE(KnowledgeArticle, Bodytext, ?) AS KEY_TBL
ON FT_TBL.RecID = KEY_TBL.[KEY]
WHERE VisibleToCustomerPortal = 'true' AND
Status = 'Published'
ORDER BY rank DESC
""".format(and_params=and_params, near_params=near_params), args)
The above code manages to pass the search terms into the parameters and the correct number of parameter markers is recognized, but I hit a SQL error when the statement executes:
"('42000', "[42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near the keyword 'AND'. (156) (SQLExecDirectW); [42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Incorrect syntax near 'NEAR'. (102); [42000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Statement(s) could not be prepared. (8180)")"
The query does work as expected when plugging in the search string in the FREETEXTTABLE sub-queries and the '"[word] AND [word] AND ...' and '"[word] NEAR [word] NEAR ...' conditions directly and running them in SSMS.
It seems like I have mutually exclusive options when trying to make this work as a dynamic T-SQL parameterized query using pyodbc. I attempted to find an answer on here but none of the parameterized query/pyodbc/full text T-SQL questions seem to address my problem. Thank you in advance for the help!
Additional Info
- SQL Server 2016
- Python 3.6
- Running on Windows