Repository: kayak/pypika
Pull request #765 (2023-10-18 04:20:32)

support final keyword for ClickHouse dialect

coveralls: 
[![Coverage Status](https://coveralls.io/builds/63364082/badge)](https://coveralls.io/builds/63364082)

coverage: 98.389% (+0.002%) from 98.387% when pulling **27bcb5bb8649888257ffcef205eb361cb84bc1d1 on YiuRULE:final_keyword** into **627b60ac760445e496d6aff890ccc7ab3990176c on kayak:master**.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cce2084..74a31f2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-python@v4
+      - uses: actions/setup-python@v5
       - uses: pre-commit/action@v3.0.0
 
   test:
@@ -20,12 +20,12 @@ jobs:
     strategy:
       max-parallel: 5
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
diff --git a/README.rst b/README.rst
index f36f34b..6794643 100644
--- a/README.rst
+++ b/README.rst
@@ -510,7 +510,7 @@ Example of a correlated subquery in the `SELECT`
 Unions
 """"""
 
-Both ``UNION`` and ``UNION ALL`` are supported. ``UNION DISTINCT`` is synonymous with "UNION`` so |Brand| does not
+Both ``UNION`` and ``UNION ALL`` are supported. ``UNION DISTINCT`` is synonymous with ``UNION`` so |Brand| does not
 provide a separate function for it.  Unions require that queries have the same number of ``SELECT`` clauses so
 trying to cast a unioned query to string will throw a ``SetOperationException`` if the column sizes are mismatched.
 
@@ -1368,6 +1368,58 @@ This produces:
 
     DROP INDEX IF EXISTS my_index
 
+
+Chaining Functions
+^^^^^^^^^^^^^^^^^^
+
+The ``QueryBuilder.pipe`` method gives a more readable alternative while chaining functions.
+
+.. code-block:: python 
+
+    # This 
+    (
+        query
+        .pipe(func1, *args)
+        .pipe(func2, **kwargs)
+        .pipe(func3)
+    )
+
+    # Is equivalent to this
+    func3(func2(func1(query, *args), **kwargs))
+
+Or for a more concrete example:
+
+.. code-block:: python 
+
+    from pypika import Field, Query, functions as fn
+    from pypika.queries import QueryBuilder
+
+    def filter_days(query: QueryBuilder, col, num_days: int) -> QueryBuilder: 
+        if isinstance(col, str): 
+            col = Field(col)
+
+        return query.where(col > fn.Now() - num_days)
+
+    def count_groups(query: QueryBuilder, *groups) -> QueryBuilder: 
+        return query.groupby(*groups).select(*groups, fn.Count("*").as_("n_rows"))
+
+    base_query = Query.from_("table")
+
+    query = (
+        base_query
+        .pipe(filter_days, "date", num_days=7)
+        .pipe(count_groups, "col1", "col2")
+    )
+
+This produces: 
+
+.. code-block:: sql
+
+    SELECT "col1","col2",COUNT(*) n_rows 
+    FROM "table" 
+    WHERE "date">NOW()-7 
+    GROUP BY "col1","col2"
+
 .. _tutorial_end:
 
 .. _contributing_start: 
diff --git a/pypika/__init__.py b/pypika/__init__.py
index a875ae0..66f564f 100644
--- a/pypika/__init__.py
+++ b/pypika/__init__.py
@@ -112,9 +112,63 @@ from pypika.utils import (
     FunctionException,
 )
 
+
 __author__ = "Timothy Heys"
 __email__ = "theys@kayak.com"
 __version__ = "0.48.9"
 
 NULL = NullValue()
 SYSTEM_TIME = SystemTimeValue()
+
+__all__ = (
+    'ClickHouseQuery',
+    'Dialects',
+    'MSSQLQuery',
+    'MySQLQuery',
+    'OracleQuery',
+    'PostgreSQLQuery',
+    'RedshiftQuery',
+    'SQLLiteQuery',
+    'VerticaQuery',
+    'DatePart',
+    'JoinType',
+    'Order',
+    'AliasedQuery',
+    'Query',
+    'Schema',
+    'Table',
+    'Column',
+    'Database',
+    'Tables',
+    'Columns',
+    'Array',
+    'Bracket',
+    'Case',
+    'Criterion',
+    'EmptyCriterion',
+    'Field',
+    'Index',
+    'Interval',
+    'JSON',
+    'Not',
+    'NullValue',
+    'SystemTimeValue',
+    'Parameter',
+    'QmarkParameter',
+    'NumericParameter',
+    'NamedParameter',
+    'FormatParameter',
+    'PyformatParameter',
+    'Rollup',
+    'Tuple',
+    'CustomFunction',
+    'CaseException',
+    'GroupingException',
+    'JoinException',
+    'QueryException',
+    'RollupException',
+    'SetOperationException',
+    'FunctionException',
+    'NULL',
+    'SYSTEM_TIME',
+)
diff --git a/pypika/dialects.py b/pypika/dialects.py
index 8894e56..2255243 100644
--- a/pypika/dialects.py
+++ b/pypika/dialects.py
@@ -1,4 +1,5 @@
 import itertools
+import warnings
 from copy import copy
 from typing import Any, Optional, Union, Tuple as TypedTuple
 
@@ -87,7 +88,7 @@ class MySQLQueryBuilder(QueryBuilder):
     QUERY_CLS = MySQLQuery
 
     def __init__(self, **kwargs: Any) -> None:
-        super().__init__(dialect=Dialects.MYSQL, wrap_set_operation_queries=False, **kwargs)
+        super().__init__(dialect=Dialects.MYSQL, **kwargs)
         self._duplicate_updates = []
         self._ignore_duplicates = False
         self._modifiers = []
@@ -347,6 +348,19 @@ class VerticaCopyQueryBuilder:
         return self.get_sql()
 
 
+class FetchNextAndOffsetRowsQueryBuilder(QueryBuilder):
+    def _limit_sql(self) -> str:
+        return " FETCH NEXT {limit} ROWS ONLY".format(limit=self._limit)
+
+    def _offset_sql(self) -> str:
+        return " OFFSET {offset} ROWS".format(offset=self._offset or 0)
+
+    @builder
+    def fetch_next(self, limit: int):
+        warnings.warn("`fetch_next` is deprecated - please use the `limit` method", DeprecationWarning)
+        self._limit = limit
+
+
 class OracleQuery(Query):
     """
     Defines a query class for use with Oracle.
@@ -357,7 +371,7 @@ class OracleQuery(Query):
         return OracleQueryBuilder(**kwargs)
 
 
-class OracleQueryBuilder(QueryBuilder):
+class OracleQueryBuilder(FetchNextAndOffsetRowsQueryBuilder):
     QUOTE_CHAR = None
     QUERY_CLS = OracleQuery
 
@@ -370,6 +384,16 @@ class OracleQueryBuilder(QueryBuilder):
         kwargs['groupby_alias'] = False
         return super().get_sql(*args, **kwargs)
 
+    def _apply_pagination(self, querystring: str) -> str:
+        # Note: Overridden as Oracle specifies offset before the fetch next limit
+        if self._offset:
+            querystring += self._offset_sql()
+
+        if self._limit is not None:
+            querystring += self._limit_sql()
+
+        return querystring
+
 
 class PostgreSQLQuery(Query):
     """
@@ -670,7 +694,7 @@ class MSSQLQuery(Query):
         return MSSQLQueryBuilder(**kwargs)
 
 
-class MSSQLQueryBuilder(QueryBuilder):
+class MSSQLQueryBuilder(FetchNextAndOffsetRowsQueryBuilder):
     QUERY_CLS = MSSQLQuery
 
     def __init__(self, **kwargs: Any) -> None:
@@ -695,17 +719,6 @@ class MSSQLQueryBuilder(QueryBuilder):
         self._top_percent: bool = percent
         self._top_with_ties: bool = with_ties
 
-    @builder
-    def fetch_next(self, limit: int) -> "MSSQLQueryBuilder":
-        # Overridden to provide a more domain-specific API for T-SQL users
-        self._limit = limit
-
-    def _offset_sql(self) -> str:
-        return " OFFSET {offset} ROWS".format(offset=self._offset or 0)
-
-    def _limit_sql(self) -> str:
-        return " FETCH NEXT {limit} ROWS ONLY".format(limit=self._limit)
-
     def _apply_pagination(self, querystring: str) -> str:
         # Note: Overridden as MSSQL specifies offset before the fetch next limit
         if self._limit is not None or self._offset:
@@ -783,9 +796,14 @@ class ClickHouseQueryBuilder(QueryBuilder):
 
     def __init__(self, **kwargs) -> None:
         super().__init__(**kwargs)
+        self._final = False
         self._sample = None
         self._sample_offset = None
 
+    @builder
+    def final(self) -> "ClickHouseQueryBuilder":
+        self._final = True
+
     @builder
     def sample(self, sample: int, offset: Optional[int] = None) -> "ClickHouseQueryBuilder":
         self._sample = sample
@@ -803,6 +821,8 @@ class ClickHouseQueryBuilder(QueryBuilder):
         if self._delete_from:
             return " {selectable} DELETE".format(selectable=selectable)
         clauses = [selectable]
+        if self._final is not False:
+            clauses.append("FINAL")
         if self._sample is not None:
             clauses.append(f"SAMPLE {self._sample}")
         if self._sample_offset is not None:
diff --git a/pypika/functions.py b/pypika/functions.py
index c820040..5e693f0 100644
--- a/pypika/functions.py
+++ b/pypika/functions.py
@@ -1,6 +1,11 @@
 """
 Package for SQL functions wrappers
 """
+from __future__ import annotations
+
+from typing import Optional
+
+from pypika import Field
 from pypika.enums import SqlTypes
 from pypika.terms import (
     AggregateFunction,
@@ -10,6 +15,7 @@ from pypika.terms import (
 )
 from pypika.utils import builder
 
+
 __author__ = "Timothy Heys"
 __email__ = "theys@kayak.com"
 
@@ -34,64 +40,64 @@ class DistinctOptionFunction(AggregateFunction):
 
 
 class Count(DistinctOptionFunction):
-    def __init__(self, param, alias=None):
+    def __init__(self, param: str | Field, alias: Optional[str] = None) -> None:
         is_star = isinstance(param, str) and "*" == param
         super(Count, self).__init__("COUNT", Star() if is_star else param, alias=alias)
 
 
 # Arithmetic Functions
 class Sum(DistinctOptionFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Sum, self).__init__("SUM", term, alias=alias)
 
 
 class Avg(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Avg, self).__init__("AVG", term, alias=alias)
 
 
 class Min(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Min, self).__init__("MIN", term, alias=alias)
 
 
 class Max(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Max, self).__init__("MAX", term, alias=alias)
 
 
 class Std(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Std, self).__init__("STD", term, alias=alias)
 
 
 class StdDev(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(StdDev, self).__init__("STDDEV", term, alias=alias)
 
 
 class Abs(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Abs, self).__init__("ABS", term, alias=alias)
 
 
 class First(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(First, self).__init__("FIRST", term, alias=alias)
 
 
 class Last(AggregateFunction):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Last, self).__init__("LAST", term, alias=alias)
 
 
 class Sqrt(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Sqrt, self).__init__("SQRT", term, alias=alias)
 
 
 class Floor(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Floor, self).__init__("FLOOR", term, alias=alias)
 
 
@@ -131,17 +137,17 @@ class ToChar(Function):
 
 
 class Signed(Cast):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Signed, self).__init__(term, SqlTypes.SIGNED, alias=alias)
 
 
 class Unsigned(Cast):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Unsigned, self).__init__(term, SqlTypes.UNSIGNED, alias=alias)
 
 
 class Date(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Date, self).__init__("DATE", term, alias=alias)
 
 
@@ -156,7 +162,7 @@ class TimeDiff(Function):
 
 
 class DateAdd(Function):
-    def __init__(self, date_part, interval, term, alias=None):
+    def __init__(self, date_part, interval, term: str, alias: Optional[str] = None):
         date_part = getattr(date_part, "value", date_part)
         super(DateAdd, self).__init__("DATE_ADD", LiteralValue(date_part), interval, term, alias=alias)
 
@@ -167,19 +173,19 @@ class ToDate(Function):
 
 
 class Timestamp(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Timestamp, self).__init__("TIMESTAMP", term, alias=alias)
 
 
 class TimestampAdd(Function):
-    def __init__(self, date_part, interval, term, alias=None):
+    def __init__(self, date_part, interval, term: str, alias: Optional[str] = None):
         date_part = getattr(date_part, 'value', date_part)
         super(TimestampAdd, self).__init__("TIMESTAMPADD", LiteralValue(date_part), interval, term, alias=alias)
 
 
 # String Functions
 class Ascii(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Ascii, self).__init__("ASCII", term, alias=alias)
 
 
@@ -189,7 +195,7 @@ class NullIf(Function):
 
 
 class Bin(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Bin, self).__init__("BIN", term, alias=alias)
 
 
@@ -205,17 +211,17 @@ class Insert(Function):
 
 
 class Length(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Length, self).__init__("LENGTH", term, alias=alias)
 
 
 class Upper(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Upper, self).__init__("UPPER", term, alias=alias)
 
 
 class Lower(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Lower, self).__init__("LOWER", term, alias=alias)
 
 
@@ -225,12 +231,12 @@ class Substring(Function):
 
 
 class Reverse(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Reverse, self).__init__("REVERSE", term, alias=alias)
 
 
 class Trim(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(Trim, self).__init__("TRIM", term, alias=alias)
 
 
@@ -297,7 +303,7 @@ class Extract(Function):
 
 # Null Functions
 class IsNull(Function):
-    def __init__(self, term, alias=None):
+    def __init__(self, term: str | Field, alias: Optional[str] = None):
         super(IsNull, self).__init__("ISNULL", term, alias=alias)
 
 
@@ -312,5 +318,5 @@ class IfNull(Function):
 
 
 class NVL(Function):
-    def __init__(self, condition, term, alias=None):
+    def __init__(self, condition, term: str, alias: Optional[str] = None):
         super(NVL, self).__init__("NVL", condition, term, alias=alias)
diff --git a/pypika/py.typed b/pypika/py.typed
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/pypika/py.typed
@@ -0,0 +1,1 @@
+
diff --git a/pypika/queries.py b/pypika/queries.py
index 223c3c9..d786120 100644
--- a/pypika/queries.py
+++ b/pypika/queries.py
@@ -206,7 +206,7 @@ class Table(Selectable):
     def __hash__(self) -> int:
         return hash(str(self))
 
-    def select(self, *terms: Sequence[Union[int, float, str, bool, Term, Field]]) -> "QueryBuilder":
+    def select(self, *terms: Union[int, float, str, bool, Term, Field]) -> "QueryBuilder":
         """
         Perform a SELECT operation on the current table
 
@@ -1560,6 +1560,54 @@ class QueryBuilder(Selectable, Term):
             )
         )
 
+    def pipe(self, func, *args, **kwargs):
+        """Call a function on the current object and return the result.
+
+        Example usage:
+
+        .. code-block:: python
+
+            from pypika import Query, functions as fn
+            from pypika.queries import QueryBuilder
+
+            def rows_by_group(query: QueryBuilder, *groups) -> QueryBuilder:
+                return (
+                    query
+                    .select(*groups, fn.Count("*").as_("n_rows"))
+                    .groupby(*groups)
+                )
+
+            base_query = Query.from_("table")
+
+            col1_agg = base_query.pipe(rows_by_group, "col1")
+            col2_agg = base_query.pipe(rows_by_group, "col2")
+            col1_col2_agg = base_query.pipe(rows_by_group, "col1", "col2")
+
+        Makes chaining functions together easier, especially when the functions are
+        defined elsewhere. For example, you could define a function that filters
+        rows by a date range and then group by a set of columns:
+
+
+        .. code-block:: python
+
+            from datetime import datetime, timedelta
+
+            from pypika import Field
+
+            def days_since(query: QueryBuilder, n_days: int) -> QueryBuilder:
+                return (
+                    query
+                    .where("date" > fn.Date(datetime.now().date() - timedelta(days=n_days)))
+                )
+
+            (
+                base_query
+                .pipe(days_since, n_days=7)
+                .pipe(rows_by_group, "col1", "col2")
+            )
+        """
+        return func(self, *args, **kwargs)
+
 
 class Joiner:
     def __init__(
diff --git a/pypika/terms.py b/pypika/terms.py
index b25af5c..a277e1a 100644
--- a/pypika/terms.py
+++ b/pypika/terms.py
@@ -3,7 +3,21 @@ import re
 import uuid
 from datetime import date
 from enum import Enum
-from typing import TYPE_CHECKING, Any, Iterable, Iterator, List, Optional, Sequence, Set, Type, TypeVar, Union
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    Iterable,
+    Iterator,
+    List,
+    Optional,
+    Sequence,
+    Set,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+)
 
 from pypika.enums import Arithmetic, Boolean, Comparator, Dialects, Equality, JSONOperators, Matching, Order
 from pypika.utils import (
@@ -288,57 +302,111 @@ class Term(Node):
         raise NotImplementedError()
 
 
+def idx_placeholder_gen(idx: int) -> str:
+    return str(idx + 1)
+
+
+def named_placeholder_gen(idx: int) -> str:
+    return f'param{idx + 1}'
+
+
 class Parameter(Term):
     is_aggregate = None
 
     def __init__(self, placeholder: Union[str, int]) -> None:
         super().__init__()
-        self.placeholder = placeholder
+        self._placeholder = placeholder
+
+    @property
+    def placeholder(self):
+        return self._placeholder
 
     def get_sql(self, **kwargs: Any) -> str:
         return str(self.placeholder)
 
+    def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
+        pass
 
-class QmarkParameter(Parameter):
-    """Question mark style, e.g. ...WHERE name=?"""
+    def get_param_key(self, placeholder: Any, **kwargs):
+        return placeholder
 
-    def __init__(self) -> None:
-        pass
 
-    def get_sql(self, **kwargs: Any) -> str:
-        return "?"
+class ListParameter(Parameter):
+    def __init__(self, placeholder: Union[str, int, Callable[[int], str]] = idx_placeholder_gen) -> None:
+        super().__init__(placeholder=placeholder)
+        self._parameters = list()
 
+    @property
+    def placeholder(self) -> str:
+        if callable(self._placeholder):
+            return self._placeholder(len(self._parameters))
 
-class NumericParameter(Parameter):
-    """Numeric, positional style, e.g. ...WHERE name=:1"""
+        return str(self._placeholder)
 
-    def get_sql(self, **kwargs: Any) -> str:
-        return ":{placeholder}".format(placeholder=self.placeholder)
+    def get_parameters(self, **kwargs):
+        return self._parameters
 
+    def update_parameters(self, value: Any, **kwargs):
+        self._parameters.append(value)
 
-class NamedParameter(Parameter):
-    """Named style, e.g. ...WHERE name=:name"""
+
+class DictParameter(Parameter):
+    def __init__(self, placeholder: Union[str, int, Callable[[int], str]] = named_placeholder_gen) -> None:
+        super().__init__(placeholder=placeholder)
+        self._parameters = dict()
+
+    @property
+    def placeholder(self) -> str:
+        if callable(self._placeholder):
+            return self._placeholder(len(self._parameters))
+
+        return str(self._placeholder)
+
+    def get_parameters(self, **kwargs):
+        return self._parameters
+
+    def get_param_key(self, placeholder: Any, **kwargs):
+        return placeholder[1:]
+
+    def update_parameters(self, param_key: Any, value: Any, **kwargs):
+        self._parameters[param_key] = value
+
+
+class QmarkParameter(ListParameter):
+    def get_sql(self, **kwargs):
+        return '?'
+
+
+class NumericParameter(ListParameter):
+    """Numeric, positional style, e.g. ...WHERE name=:1"""
 
     def get_sql(self, **kwargs: Any) -> str:
         return ":{placeholder}".format(placeholder=self.placeholder)
 
 
-class FormatParameter(Parameter):
+class FormatParameter(ListParameter):
     """ANSI C printf format codes, e.g. ...WHERE name=%s"""
 
-    def __init__(self) -> None:
-        pass
-
     def get_sql(self, **kwargs: Any) -> str:
         return "%s"
 
 
-class PyformatParameter(Parameter):
+class NamedParameter(DictParameter):
+    """Named style, e.g. ...WHERE name=:name"""
+
+    def get_sql(self, **kwargs: Any) -> str:
+        return ":{placeholder}".format(placeholder=self.placeholder)
+
+
+class PyformatParameter(DictParameter):
     """Python extended format codes, e.g. ...WHERE name=%(name)s"""
 
     def get_sql(self, **kwargs: Any) -> str:
         return "%({placeholder})s".format(placeholder=self.placeholder)
 
+    def get_param_key(self, placeholder: Any, **kwargs):
+        return placeholder[2:-2]
+
 
 class Negative(Term):
     def __init__(self, term: Term) -> None:
@@ -385,9 +453,44 @@ class ValueWrapper(Term):
             return "null"
         return str(value)
 
-    def get_sql(self, quote_char: Optional[str] = None, secondary_quote_char: str = "'", **kwargs: Any) -> str:
-        sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
-        return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
+    def _get_param_data(self, parameter: Parameter, **kwargs) -> Tuple[str, str]:
+        param_sql = parameter.get_sql(**kwargs)
+        param_key = parameter.get_param_key(placeholder=param_sql)
+
+        return param_sql, param_key
+
+    def get_sql(
+        self,
+        quote_char: Optional[str] = None,
+        secondary_quote_char: str = "'",
+        parameter: Parameter = None,
+        **kwargs: Any,
+    ) -> str:
+        if parameter is None:
+            sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
+            return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
+
+        # Don't stringify numbers when using a parameter
+        if isinstance(self.value, (int, float)):
+            value_sql = self.value
+        else:
+            value_sql = self.get_value_sql(quote_char=quote_char, **kwargs)
+        param_sql, param_key = self._get_param_data(parameter, **kwargs)
+        parameter.update_parameters(param_key=param_key, value=value_sql, **kwargs)
+
+        return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
+
+
+class ParameterValueWrapper(ValueWrapper):
+    def __init__(self, parameter: Parameter, value: Any, alias: Optional[str] = None) -> None:
+        super().__init__(value, alias)
+        self._parameter = parameter
+
+    def _get_param_data(self, parameter: Parameter, **kwargs) -> Tuple[str, str]:
+        param_sql = self._parameter.get_sql(**kwargs)
+        param_key = self._parameter.get_param_key(placeholder=param_sql)
+
+        return param_sql, param_key
 
 
 class JSON(Term):
@@ -548,6 +651,11 @@ class Field(Criterion, JSON):
     ) -> None:
         super().__init__(alias=alias)
         self.name = name
+        if isinstance(table, str):
+            # avoid circular import at load time
+            from pypika.queries import Table
+
+            table = Table(table)
         self.table = table
 
     def nodes_(self) -> Iterator[NodeT]:
diff --git a/setup.py b/setup.py
index fb376d4..41cce7d 100644
--- a/setup.py
+++ b/setup.py
@@ -48,6 +48,7 @@ setup(
       packages=["pypika", "pypika.clickhouse"],
 
       # Include additional files into the package
+      package_data={"pypika": ["py.typed"]},
       include_package_data=True,
 
       install_requires=[],


Test patch:
diff --git a/pypika/tests/dialects/test_clickhouse.py b/pypika/tests/dialects/test_clickhouse.py
index 8da6270..62afd7c 100644
--- a/pypika/tests/dialects/test_clickhouse.py
+++ b/pypika/tests/dialects/test_clickhouse.py
@@ -23,6 +23,11 @@ class ClickHouseQueryTests(TestCase):
         query = ClickHouseQuery.from_(t).select(t.foo).sample(10, 5)
         self.assertEqual(str(query), 'SELECT "foo" FROM "abc" SAMPLE 10 OFFSET 5')
 
+    def test_use_FINAL_keyword(self):
+        t = Table('abc')
+        query = ClickHouseQuery.from_(t).select(t.foo).final()
+        self.assertEqual(str(query), 'SELECT "foo" FROM "abc" FINAL')
+
 
 class ClickHouseDeleteTests(TestCase):
     table_abc = Table("abc")
diff --git a/pypika/tests/dialects/test_mssql.py b/pypika/tests/dialects/test_mssql.py
index f8940b0..7cf53b2 100644
--- a/pypika/tests/dialects/test_mssql.py
+++ b/pypika/tests/dialects/test_mssql.py
@@ -53,18 +53,13 @@ class SelectTests(unittest.TestCase):
 
         self.assertEqual('SELECT "def" FROM "abc" ORDER BY "def" OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', str(q))
 
-    def test_fetch_next(self):
-        q = MSSQLQuery.from_("abc").select("def").orderby("def").fetch_next(10)
-
-        self.assertEqual('SELECT "def" FROM "abc" ORDER BY "def" OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', str(q))
-
     def test_offset(self):
         q = MSSQLQuery.from_("abc").select("def").orderby("def").offset(10)
 
         self.assertEqual('SELECT "def" FROM "abc" ORDER BY "def" OFFSET 10 ROWS', str(q))
 
-    def test_fetch_next_with_offset(self):
-        q = MSSQLQuery.from_("abc").select("def").orderby("def").fetch_next(10).offset(10)
+    def test_limit_with_offset(self):
+        q = MSSQLQuery.from_("abc").select("def").orderby("def").limit(10).offset(10)
 
         self.assertEqual('SELECT "def" FROM "abc" ORDER BY "def" OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY', str(q))
 
diff --git a/pypika/tests/dialects/test_oracle.py b/pypika/tests/dialects/test_oracle.py
index c3a757a..21b54f1 100644
--- a/pypika/tests/dialects/test_oracle.py
+++ b/pypika/tests/dialects/test_oracle.py
@@ -19,3 +19,33 @@ class SelectTests(unittest.TestCase):
         q = OracleQuery.from_(subquery).select(col, Count('*')).groupby(col)
 
         self.assertEqual('SELECT sq0.abc a,COUNT(\'*\') FROM (SELECT abc FROM table1) sq0 GROUP BY sq0.abc', str(q))
+
+    def test_limit_query(self):
+        t = Table('table1')
+        limit = 5
+        q = OracleQuery.from_(t).select(t.test).limit(limit)
+
+        self.assertEqual(f'SELECT test FROM table1 FETCH NEXT {limit} ROWS ONLY', str(q))
+
+    def test_offset_query(self):
+        t = Table('table1')
+        offset = 5
+        q = OracleQuery.from_(t).select(t.test).offset(offset)
+
+        self.assertEqual(f'SELECT test FROM table1 OFFSET {offset} ROWS', str(q))
+
+    def test_limit_offset_query(self):
+        t = Table('table1')
+        limit = 5
+        offset = 5
+        q = OracleQuery.from_(t).select(t.test).limit(limit).offset(offset)
+
+        self.assertEqual(f'SELECT test FROM table1 OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY', str(q))
+
+    def test_fetch_next_method_deprecated(self):
+        with self.assertWarns(DeprecationWarning):
+            t = Table('table1')
+            limit = 5
+            q = OracleQuery.from_(t).select(t.test).fetch_next(limit)
+
+            self.assertEqual(f'SELECT test FROM table1 FETCH NEXT {limit} ROWS ONLY', str(q))
diff --git a/pypika/tests/test_joins.py b/pypika/tests/test_joins.py
index 6e54f88..03142d5 100644
--- a/pypika/tests/test_joins.py
+++ b/pypika/tests/test_joins.py
@@ -845,12 +845,12 @@ class UnionTests(unittest.TestCase):
         with self.assertRaises(SetOperationException):
             str(query1 + query2)
 
-    def test_mysql_query_does_not_wrap_unioned_queries_with_params(self):
+    def test_mysql_query_wraps_unioned_queries(self):
         query1 = MySQLQuery.from_(self.table1).select(self.table1.foo)
         query2 = Query.from_(self.table2).select(self.table2.bar)
 
         self.assertEqual(
-            "SELECT `foo` FROM `abc` UNION SELECT `bar` FROM `efg`",
+            "(SELECT `foo` FROM `abc`) UNION (SELECT `bar` FROM `efg`)",
             str(query1 + query2),
         )
 
@@ -968,12 +968,12 @@ class IntersectTests(unittest.TestCase):
         with self.assertRaises(SetOperationException):
             str(query1.intersect(query2))
 
-    def test_mysql_query_does_not_wrap_intersected_queries_with_params(self):
+    def test_mysql_query_wraps_intersected_queries(self):
         query1 = MySQLQuery.from_(self.table1).select(self.table1.foo)
         query2 = Query.from_(self.table2).select(self.table2.bar)
 
         self.assertEqual(
-            "SELECT `foo` FROM `abc` INTERSECT SELECT `bar` FROM `efg`",
+            "(SELECT `foo` FROM `abc`) INTERSECT (SELECT `bar` FROM `efg`)",
             str(query1.intersect(query2)),
         )
 
@@ -1064,12 +1064,12 @@ class MinusTests(unittest.TestCase):
         with self.assertRaises(SetOperationException):
             str(query1.minus(query2))
 
-    def test_mysql_query_does_not_wrap_minus_queries_with_params(self):
+    def test_mysql_query_wraps_minus_queries(self):
         query1 = MySQLQuery.from_(self.table1).select(self.table1.foo)
         query2 = Query.from_(self.table2).select(self.table2.bar)
 
         self.assertEqual(
-            "SELECT `foo` FROM `abc` MINUS SELECT `bar` FROM `efg`",
+            "(SELECT `foo` FROM `abc`) MINUS (SELECT `bar` FROM `efg`)",
             str(query1 - query2),
         )
 
diff --git a/pypika/tests/test_parameter.py b/pypika/tests/test_parameter.py
index e19666a..c11e9af 100644
--- a/pypika/tests/test_parameter.py
+++ b/pypika/tests/test_parameter.py
@@ -1,4 +1,5 @@
 import unittest
+from datetime import date
 
 from pypika import (
     FormatParameter,
@@ -10,6 +11,7 @@ from pypika import (
     Query,
     Tables,
 )
+from pypika.terms import ListParameter, ParameterValueWrapper
 
 
 class ParametrizedTests(unittest.TestCase):
@@ -92,3 +94,113 @@ class ParametrizedTests(unittest.TestCase):
 
     def test_pyformat_parameter(self):
         self.assertEqual('%(buz)s', PyformatParameter('buz').get_sql())
+
+
+class ParametrizedTestsWithValues(unittest.TestCase):
+    table_abc, table_efg = Tables("abc", "efg")
+
+    def test_param_insert(self):
+        q = Query.into(self.table_abc).columns("a", "b", "c").insert(1, 2.2, 'foo')
+
+        parameter = QmarkParameter()
+        sql = q.get_sql(parameter=parameter)
+        self.assertEqual('INSERT INTO "abc" ("a","b","c") VALUES (?,?,?)', sql)
+        self.assertEqual([1, 2.2, 'foo'], parameter.get_parameters())
+
+    def test_param_select_join(self):
+        q = (
+            Query.from_(self.table_abc)
+            .select("*")
+            .where(self.table_abc.category == 'foobar')
+            .join(self.table_efg)
+            .on(self.table_abc.id == self.table_efg.abc_id)
+            .where(self.table_efg.date >= date(2024, 2, 22))
+            .limit(10)
+        )
+
+        parameter = FormatParameter()
+        sql = q.get_sql(parameter=parameter)
+        self.assertEqual(
+            'SELECT * FROM "abc" JOIN "efg" ON "abc"."id"="efg"."abc_id" WHERE "abc"."category"=%s AND "efg"."date">=%s LIMIT 10',
+            sql,
+        )
+        self.assertEqual(['foobar', '2024-02-22'], parameter.get_parameters())
+
+    def test_param_select_subquery(self):
+        q = (
+            Query.from_(self.table_abc)
+            .select("*")
+            .where(self.table_abc.category == 'foobar')
+            .where(
+                self.table_abc.id.isin(
+                    Query.from_(self.table_efg)
+                    .select(self.table_efg.abc_id)
+                    .where(self.table_efg.date >= date(2024, 2, 22))
+                )
+            )
+            .limit(10)
+        )
+
+        parameter = ListParameter(placeholder=lambda idx: f'&{idx+1}')
+        sql = q.get_sql(parameter=parameter)
+        self.assertEqual(
+            'SELECT * FROM "abc" WHERE "category"=&1 AND "id" IN (SELECT "abc_id" FROM "efg" WHERE "date">=&2) LIMIT 10',
+            sql,
+        )
+        self.assertEqual(['foobar', '2024-02-22'], parameter.get_parameters())
+
+    def test_join(self):
+        subquery = (
+            Query.from_(self.table_efg)
+            .select(self.table_efg.fiz, self.table_efg.buz)
+            .where(self.table_efg.buz == 'buz')
+        )
+
+        q = (
+            Query.from_(self.table_abc)
+            .join(subquery)
+            .on(self.table_abc.bar == subquery.buz)
+            .select(self.table_abc.foo, subquery.fiz)
+            .where(self.table_abc.bar == 'bar')
+        )
+
+        parameter = NamedParameter()
+        sql = q.get_sql(parameter=parameter)
+        self.assertEqual(
+            'SELECT "abc"."foo","sq0"."fiz" FROM "abc" JOIN (SELECT "fiz","buz" FROM "efg" WHERE "buz"=:param1)'
+            ' "sq0" ON "abc"."bar"="sq0"."buz" WHERE "abc"."bar"=:param2',
+            sql,
+        )
+        self.assertEqual({'param1': 'buz', 'param2': 'bar'}, parameter.get_parameters())
+
+    def test_join_with_parameter_value_wrapper(self):
+        subquery = (
+            Query.from_(self.table_efg)
+            .select(self.table_efg.fiz, self.table_efg.buz)
+            .where(self.table_efg.buz == ParameterValueWrapper(Parameter(':buz'), 'buz'))
+        )
+
+        q = (
+            Query.from_(self.table_abc)
+            .join(subquery)
+            .on(self.table_abc.bar == subquery.buz)
+            .select(self.table_abc.foo, subquery.fiz)
+            .where(self.table_abc.bar == ParameterValueWrapper(NamedParameter('bar'), 'bar'))
+        )
+
+        parameter = NamedParameter()
+        sql = q.get_sql(parameter=parameter)
+        self.assertEqual(
+            'SELECT "abc"."foo","sq0"."fiz" FROM "abc" JOIN (SELECT "fiz","buz" FROM "efg" WHERE "buz"=:buz)'
+            ' "sq0" ON "abc"."bar"="sq0"."buz" WHERE "abc"."bar"=:bar',
+            sql,
+        )
+        self.assertEqual({':buz': 'buz', 'bar': 'bar'}, parameter.get_parameters())
+
+    def test_pyformat_parameter(self):
+        q = Query.into(self.table_abc).columns("a", "b", "c").insert(1, 2.2, 'foo')
+
+        parameter = PyformatParameter()
+        sql = q.get_sql(parameter=parameter)
+        self.assertEqual('INSERT INTO "abc" ("a","b","c") VALUES (%(param1)s,%(param2)s,%(param3)s)', sql)
+        self.assertEqual({"param1": 1, "param2": 2.2, "param3": "foo"}, parameter.get_parameters())
diff --git a/pypika/tests/test_query.py b/pypika/tests/test_query.py
index 460b7d7..ae8dc01 100644
--- a/pypika/tests/test_query.py
+++ b/pypika/tests/test_query.py
@@ -1,6 +1,6 @@
 import unittest
 
-from pypika import Case, Query, Tables, Tuple, functions
+from pypika import Case, Query, Tables, Tuple, functions, Field
 from pypika.dialects import (
     ClickHouseQuery,
     ClickHouseQueryBuilder,
@@ -204,3 +204,39 @@ class QueryBuilderTests(unittest.TestCase):
 
         with self.subTest('OracleQueryBuilder'):
             self.assertEqual(OracleQuery, OracleQueryBuilder.QUERY_CLS)
+
+    def test_pipe(self) -> None:
+        base_query = Query.from_("test")
+
+        def select(query: QueryBuilder) -> QueryBuilder:
+            return query.select("test1", "test2")
+
+        def count_group(query: QueryBuilder, *groups) -> QueryBuilder:
+            return query.groupby(*groups).select(*groups, functions.Count("*"))
+
+        for func, args, kwargs, expected_str in [
+            (select, [], {}, 'SELECT "test1","test2" FROM "test"'),
+            (
+                count_group,
+                ["test1", "test2"],
+                {},
+                'SELECT "test1","test2",COUNT(*) FROM "test" GROUP BY "test1","test2"',
+            ),
+            (count_group, ["test1"], {}, 'SELECT "test1",COUNT(*) FROM "test" GROUP BY "test1"'),
+        ]:
+            result_str = str(base_query.pipe(func, *args, **kwargs))
+            self.assertEqual(result_str, str(func(base_query, *args, **kwargs)))
+            self.assertEqual(result_str, expected_str)
+
+        def where_clause(query: QueryBuilder, num_days: int) -> QueryBuilder:
+            return query.where(Field("date") > functions.Now() - num_days)
+
+        result_str = str(base_query.pipe(select).pipe(where_clause, num_days=1))
+        self.assertEqual(
+            result_str,
+            str(select(where_clause(base_query, num_days=1))),
+        )
+        self.assertEqual(
+            result_str,
+            'SELECT "test1","test2" FROM "test" WHERE "date">NOW()-1',
+        )
diff --git a/pypika/tests/test_terms.py b/pypika/tests/test_terms.py
index 562e0d2..4c7590d 100644
--- a/pypika/tests/test_terms.py
+++ b/pypika/tests/test_terms.py
@@ -15,6 +15,13 @@ class FieldAliasTests(TestCase):
         self.assertEqual('bar', str(c1.alias))
 
 
+class FieldInitTests(TestCase):
+    def test_init_with_str_table(self):
+        test_table_name = "test_table"
+        field = Field(name="name", table=test_table_name)
+        self.assertEqual(field.table, Table(name=test_table_name))
+
+
 class FieldHashingTests(TestCase):
     def test_tabled_eq_fields_equally_hashed(self):
         client_name1 = Field(name="name", table=Table("clients"))