Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -765,12 +765,7 @@ def process_wheres(self, query=None, qmark=False, strip_first_where=False):
query_value = 0
elif qmark and value_type != "column":
query_value = "?"
if (
value is not True
and value_type != "value_equals"
and value_type != "NULL"
and value_type != "BETWEEN"
):
if value_type not in ("value_equals", "NULL", "NOT NULL", "BETWEEN"):
self.add_binding(value)
elif value_type == "value":
if qmark:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import datetime
import unittest

from fastapi_startkit.masoniteorm.models.builder import QueryBuilder
from fastapi_startkit.masoniteorm.query.grammars.MSSQLGrammar import MSSQLGrammar
from fastapi_startkit.masoniteorm.query.grammars.MySQLGrammar import MySQLGrammar
from fastapi_startkit.masoniteorm.query.grammars.PostgresGrammar import PostgresGrammar
from fastapi_startkit.masoniteorm.query.grammars.SQLiteGrammar import SQLiteGrammar

GRAMMARS = {
"sqlite": SQLiteGrammar,
"mysql": MySQLGrammar,
"postgres": PostgresGrammar,
"mssql": MSSQLGrammar,
}

NULL_METHODS = ("where_null", "where_not_null", "or_where_null", "or_where_not_null")

SINCE = datetime.date(2026, 1, 1)


def qb(grammar, table="users"):
q = QueryBuilder(connection=None, grammar=grammar, processor=None)
q._table = table
return q


class TestNullWhereBindingsGrammar(unittest.TestCase):
def assert_bindings(self, query, expected):
sql = query.to_qmark()
bindings = list(query.get_bindings())
self.assertEqual(sql.count("?"), len(bindings), sql)
self.assertEqual(bindings, expected)
return sql

def test_null_clauses_followed_by_where_bind_only_the_value(self):
for method in NULL_METHODS:
for name, grammar in GRAMMARS.items():
with self.subTest(method=method, grammar=name):
query = getattr(qb(grammar), method)("deleted_at").where("created_at", ">=", SINCE)
sql = self.assert_bindings(query, [SINCE])
self.assertIn("IS NOT NULL" if "not" in method else "IS NULL", sql)

def test_null_clauses_between_bound_values_keep_order(self):
for method in NULL_METHODS:
for name, grammar in GRAMMARS.items():
with self.subTest(method=method, grammar=name):
query = getattr(qb(grammar).where("name", "Joe"), method)("deleted_at")
query.where("created_at", ">=", SINCE).where_in("id", [1, 2]).or_where("age", "<", 30)
self.assert_bindings(query, ["Joe", SINCE, 1, 2, 30])

def test_chained_null_clauses_bind_nothing(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
query = qb(grammar).where_not_null("a").where_null("b").or_where_not_null("c").or_where_null("d")
self.assert_bindings(query, [])

def test_no_value_clauses_mixed_with_null_clauses(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
query = (
qb(grammar)
.where_not_null("deleted_at")
.where_column("updated_at", ">", "created_at")
.where_raw("1 = 1")
.where("created_at", ">=", SINCE)
.where_raw("age > ?", (18,))
)
self.assert_bindings(query, [SINCE, 18])

def test_exists_subquery_with_null_clause(self):
for name, grammar in GRAMMARS.items():
with self.subTest(grammar=name):
inner = qb(grammar, "posts").where_not_null("published_at").where("votes", ">", 5)
query = qb(grammar).where_exists(inner).where("created_at", ">=", SINCE)
self.assert_bindings(query, [5, SINCE])


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from fastapi_startkit.masoniteorm import Model

from ..fixtures.db import DB
from ..test_case import TestCase


class Ticket(Model):
__table__ = "tickets"
__timestamps__ = None
id: int
closed_at: str
created_at: str


class TestSqliteNullWhereBindings(TestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
conn = DB.connection("default")
await conn.execute("CREATE TABLE tickets (id INTEGER PRIMARY KEY, closed_at TEXT, created_at TEXT)")
await conn.execute(
"INSERT INTO tickets (id, closed_at, created_at) VALUES "
"(1, '2026-02-01', '2026-01-15'), "
"(2, NULL, '2026-01-20'), "
"(3, '2025-12-01', '2025-11-30'), "
"(4, '2026-03-01', '2026-02-10')"
)

async def test_where_not_null_then_date_filter(self):
rows = (
await Ticket.query()
.where_not_null("closed_at")
.where("created_at", ">=", "2026-01-01")
.order_by("id")
.get()
)
self.assertEqual([t.id for t in rows], [1, 4])

async def test_where_null_then_date_filter(self):
rows = await Ticket.query().where_null("closed_at").where("created_at", ">=", "2026-01-01").get()
self.assertEqual([t.id for t in rows], [2])

async def test_or_where_not_null_then_value_filter(self):
rows = (
await Ticket.query().where("id", 2).or_where_not_null("closed_at").where("id", "<", 4).order_by("id").get()
)
self.assertEqual([t.id for t in rows], [1, 2, 3])
Loading