Skip to content

fix: ensure logical operators do not modify string literal casing#49

Open
Egorrko wants to merge 1 commit into
dodopizza:mainfrom
Egorrko:main
Open

fix: ensure logical operators do not modify string literal casing#49
Egorrko wants to merge 1 commit into
dodopizza:mainfrom
Egorrko:main

Conversation

@Egorrko

@Egorrko Egorrko commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

This PR fixes two things:

  1. OPERATORS[operators.and_] = " and " - this line overrides the and operator for the entire sqlalchemy library as soon as the KustoKqlCompiler is imported.

  2. where_clause = re.sub(r"\s+AND\s+", r" and ", where_clause, flags=re.IGNORECASE) - this regular expression replaces AND with and not only in operators, but also in values. Look at the tests, for an example

Copilot AI review requested due to automatic review settings February 10, 2026 12:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the KQL dialect’s handling of logical operators so that compiling queries no longer (a) mutates SQLAlchemy’s global operator map and (b) lowercases AND/OR inside string literals during post-processing.

Changes:

  • Removed the global override of SQLAlchemy OPERATORS[and_] to avoid affecting the entire SQLAlchemy runtime.
  • Implemented KustoKqlCompiler.visit_clauselist() to emit KQL-style and / or separators during compilation instead of regex-rewriting the rendered WHERE clause.
  • Added unit tests covering operator precedence (parentheses) and ensuring literal casing is preserved.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
sqlalchemy_kusto/dialect_kql.py Stops mutating SQLAlchemy globals; moves logical operator formatting into the compiler and removes regex-based AND/OR rewriting in _sql_to_kql_where().
tests/unit/test_dialect_kql.py Adds regression tests for precedence and ensuring AND/OR inside string literals are not modified.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +168 to +172

if clauselist.operator in kql_operators:
sep = kql_operators[clauselist.operator]
return self._generate_delimited_list(clauselist.clauses, sep, **kw)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visit_clauselist() currently ignores clauselist.group / grouping semantics and always returns the raw _generate_delimited_list(...) output for AND/OR. SQLAlchemy uses grouping to emit parentheses for nested boolean expressions (e.g. and_(or_(...), ...)), so this override should preserve that behavior (e.g. wrap the rendered list when grouping is requested) to avoid changing operator precedence in the emitted KQL.

Suggested change
if clauselist.operator in kql_operators:
sep = kql_operators[clauselist.operator]
return self._generate_delimited_list(clauselist.clauses, sep, **kw)
if clauselist.operator in kql_operators:
sep = kql_operators[clauselist.operator]
text = self._generate_delimited_list(clauselist.clauses, sep, **kw)
# Preserve SQLAlchemy's grouping semantics so nested boolean expressions
# get parentheses when required (e.g. and_(or_(...), ...)).
if getattr(clauselist, "group", False):
return f"({text})"
return text

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +172

if clauselist.operator in kql_operators:
sep = kql_operators[clauselist.operator]
return self._generate_delimited_list(clauselist.clauses, sep, **kw)

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s trailing whitespace on the blank lines inside visit_clauselist (will be flagged by linters like Ruff/pycodestyle). Please remove the extra spaces.

Suggested change
if clauselist.operator in kql_operators:
sep = kql_operators[clauselist.operator]
return self._generate_delimited_list(clauselist.clauses, sep, **kw)
if clauselist.operator in kql_operators:
sep = kql_operators[clauselist.operator]
return self._generate_delimited_list(clauselist.clauses, sep, **kw)

Copilot uses AI. Check for mistakes.
Comment on lines +612 to +624
column("C") == 3
)
query1 = select([column("Field")]).select_from(text("logs")).where(cond_nested_or)
compiled1 = str(query1.compile(engine, compile_kwargs={"literal_binds": True})).replace("\n", " ")

assert '(["A"] == 1 or ["B"] == 2) and ["C"] == 3' in compiled1

cond_nested_and = sa.or_(
column("A") == 1,
sa.and_(column("B") == 2, column("C") == 3)
)
query2 = select([column("Field")]).select_from(text("logs")).where(cond_nested_and)
compiled2 = str(query2.compile(engine, compile_kwargs={"literal_binds": True})).replace("\n", " ")

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new test lines are very long and aren’t formatted consistently with the rest of the file (Black formatting / line-length). Consider wrapping the select_from(...).where(...) and compiledX = ...compile(...).replace(...) statements across multiple lines and adding trailing commas in multi-line calls so formatters don’t churn the diff.

Suggested change
column("C") == 3
)
query1 = select([column("Field")]).select_from(text("logs")).where(cond_nested_or)
compiled1 = str(query1.compile(engine, compile_kwargs={"literal_binds": True})).replace("\n", " ")
assert '(["A"] == 1 or ["B"] == 2) and ["C"] == 3' in compiled1
cond_nested_and = sa.or_(
column("A") == 1,
sa.and_(column("B") == 2, column("C") == 3)
)
query2 = select([column("Field")]).select_from(text("logs")).where(cond_nested_and)
compiled2 = str(query2.compile(engine, compile_kwargs={"literal_binds": True})).replace("\n", " ")
column("C") == 3,
)
query1 = (
select([column("Field")])
.select_from(text("logs"))
.where(cond_nested_or)
)
compiled1 = (
str(query1.compile(engine, compile_kwargs={"literal_binds": True}))
.replace("\n", " ")
)
assert '(["A"] == 1 or ["B"] == 2) and ["C"] == 3' in compiled1
cond_nested_and = sa.or_(
column("A") == 1,
sa.and_(column("B") == 2, column("C") == 3),
)
query2 = (
select([column("Field")])
.select_from(text("logs"))
.where(cond_nested_and)
)
compiled2 = (
str(query2.compile(engine, compile_kwargs={"literal_binds": True}))
.replace("\n", " ")
)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants