From 2fec8453a1754512d8aa382e4476cdaddc2a7cd1 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 3 Feb 2025 13:54:46 +0530 Subject: [PATCH 1/6] fix(sqlite): allow overriding wrapper_cls Signed-off-by: Akhil Narang --- pypika/dialects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pypika/dialects.py b/pypika/dialects.py index 57955d85..87e5ae55 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -950,7 +950,9 @@ class SQLLiteQueryBuilder(QueryBuilder): QUERY_CLS = SQLLiteQuery def __init__(self, **kwargs: Any) -> None: - super().__init__(dialect=Dialects.SQLLITE, wrapper_cls=SQLLiteValueWrapper, **kwargs) + if kwargs.get("wrapper_cls") is None: + kwargs["wrapper_cls"] = SQLLiteValueWrapper + super().__init__(dialect=Dialects.SQLLITE, **kwargs) self._insert_or_replace = False @builder From ee8f2c38eeca05f8e5ca0fa6e00afc767b4d9e7a Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 3 Feb 2025 13:55:17 +0530 Subject: [PATCH 2/6] fix: sqlite doesn't support for_update For now, make sure it doesn't run Signed-off-by: Akhil Narang --- pypika/dialects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pypika/dialects.py b/pypika/dialects.py index 87e5ae55..7caa66f1 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -964,3 +964,9 @@ def insert_or_replace(self, *terms: Any) -> "SQLLiteQueryBuilder": def _replace_sql(self, **kwargs: Any) -> str: prefix = "INSERT OR " if self._insert_or_replace else "" return prefix + super()._replace_sql(**kwargs) + + @builder + def for_update( + self, nowait: bool = False, skip_locked: bool = False, of: TypedTuple[str, ...] = () + ) -> "QueryBuilder": + self._for_update = False From ace4ac28f1b3d00519ddd9794165d037f3ff929e Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 3 Feb 2025 16:58:46 +0530 Subject: [PATCH 3/6] fix: sqlite uses `INSERT OR IGNORE` Signed-off-by: Akhil Narang --- pypika/dialects.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pypika/dialects.py b/pypika/dialects.py index 7caa66f1..459d20d1 100644 --- a/pypika/dialects.py +++ b/pypika/dialects.py @@ -970,3 +970,9 @@ def for_update( self, nowait: bool = False, skip_locked: bool = False, of: TypedTuple[str, ...] = () ) -> "QueryBuilder": self._for_update = False + + def _insert_sql(self, **kwargs: Any) -> str: + return "INSERT {ignore}INTO {table}".format( + table=self._insert_table.get_sql(**kwargs), + ignore="OR IGNORE " if self._ignore else "", + ) From 692ce2f52936ca40e29182c301e91c79c3400f30 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Thu, 6 Feb 2025 16:29:02 +0530 Subject: [PATCH 4/6] fix(README): contributing guide link was wrong Signed-off-by: Akhil Narang --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 67946435..3c1025b3 100644 --- a/README.rst +++ b/README.rst @@ -1427,7 +1427,7 @@ This produces: Contributing ------------ -We welcome community contributions to |Brand|. Please see the `contributing guide <6_contributing.html>`_ to more info. +We welcome community contributions to |Brand|. Please see the `contributing guide `_ to more info. .. _contributing_end: From 0ae2ec4cbd5354c4b5969423581382d9e8fdc548 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 7 Apr 2025 13:03:06 +0530 Subject: [PATCH 5/6] feat(sqlite): add in support for `Interval` Signed-off-by: Akhil Narang --- pypika/terms.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pypika/terms.py b/pypika/terms.py index a277e1a5..574ae660 100644 --- a/pypika/terms.py +++ b/pypika/terms.py @@ -1623,11 +1623,24 @@ class Interval(Node): # Oracle and MySQL requires just single quotes around the expr Dialects.ORACLE: "INTERVAL '{expr}' {unit}", Dialects.MYSQL: "INTERVAL '{expr}' {unit}", + # SQLite doesn't have a direct INTERVAL type, use datetime functions instead + Dialects.SQLLITE: "{expr} {unit}", } units = ["years", "months", "days", "hours", "minutes", "seconds", "microseconds"] labels = ["YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND", "MICROSECOND"] + # SQLite unit conversion mapping + sqlite_units = { + "YEAR": "years", + "MONTH": "months", + "DAY": "days", + "HOUR": "hours", + "MINUTE": "minutes", + "SECOND": "seconds", + "MICROSECOND": "microseconds", + } + trim_pattern = re.compile(r"(^0+\.)|(\.0+$)|(^[0\-.: ]+[\-: ])|([\-:. ][0\-.: ]+$)") def __init__( @@ -1675,6 +1688,9 @@ def __str__(self) -> str: def get_sql(self, **kwargs: Any) -> str: dialect = self.dialect or kwargs.get("dialect") + if dialect == Dialects.SQLLITE: + return self._get_sqlite_sql() + if self.largest == "MICROSECOND": expr = getattr(self, "microseconds") unit = "MICROSECOND" @@ -1717,6 +1733,34 @@ def get_sql(self, **kwargs: Any) -> str: return self.templates.get(dialect, "INTERVAL '{expr} {unit}'").format(expr=expr, unit=unit) + def _get_sqlite_sql(self) -> str: + """Generate SQLite-compatible interval expression using datetime functions""" + if hasattr(self, "quarters"): + # Convert quarters to months for SQLite + value = getattr(self, "quarters") * 3 + sign = "-" if self.is_negative else "+" + return f"datetime('now', '{sign}{value} months')" + + if hasattr(self, "weeks"): + # Convert weeks to days for SQLite + value = getattr(self, "weeks") * 7 + sign = "-" if self.is_negative else "+" + return f"datetime('now', '{sign}{value} days')" + + # Construct datetime expression with each non-zero component + components = [] + for unit, label in zip(self.units, self.labels): + if hasattr(self, unit) and getattr(self, unit): + value = getattr(self, unit) + sign = "-" if self.is_negative else "+" + sqlite_unit = self.sqlite_units.get(label, unit) + components.append(f"'{sign}{value} {sqlite_unit}'") + + if not components: + return "datetime('now')" + + return f"datetime('now', {', '.join(components)})" + class Pow(Function): def __init__(self, term: Term, exponent: float, alias: Optional[str] = None) -> None: From 093984977ce157d35e048c51d9ff55a1f0f44570 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 7 Apr 2025 13:03:31 +0530 Subject: [PATCH 6/6] feat(sqlite): add in support for `Now` Signed-off-by: Akhil Narang --- pypika/functions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pypika/functions.py b/pypika/functions.py index 5e693f0d..c2d4b803 100644 --- a/pypika/functions.py +++ b/pypika/functions.py @@ -265,6 +265,14 @@ class Now(Function): def __init__(self, alias=None): super(Now, self).__init__("NOW", alias=alias) + def get_function_sql(self, **kwargs): + # Handle SQLite compatibility - it uses CURRENT_TIMESTAMP instead of NOW() + from .enums import Dialects + + if kwargs.get('dialect') == Dialects.SQLLITE: + return "CURRENT_TIMESTAMP" + return super(Now, self).get_function_sql(**kwargs) + class UtcTimestamp(Function): def __init__(self, alias=None):