From 7931aade79fc585acf275c296b28c7dcdb23cce1 Mon Sep 17 00:00:00 2001 From: Pierce Freeman Date: Fri, 20 Dec 2024 08:11:03 -0800 Subject: [PATCH] Add func-based date modifiers --- iceaxe/__tests__/test_queries.py | 233 +++++++++++++ iceaxe/functions.py | 545 +++++++++++++++++++++++++++++++ 2 files changed, 778 insertions(+) diff --git a/iceaxe/__tests__/test_queries.py b/iceaxe/__tests__/test_queries.py index a3ed8e1..db60cb9 100644 --- a/iceaxe/__tests__/test_queries.py +++ b/iceaxe/__tests__/test_queries.py @@ -176,6 +176,239 @@ def test_function_distinct(): ) +def test_function_abs(): + new_query = QueryBuilder().select(func.abs(UserDemo.balance)) + assert new_query.build() == ( + 'SELECT abs("userdemo"."balance") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_date_trunc(): + new_query = QueryBuilder().select(func.date_trunc('month', UserDemo.created_at)) + assert new_query.build() == ( + 'SELECT date_trunc(\'month\', "userdemo"."created_at") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_date_part(): + new_query = QueryBuilder().select(func.date_part('year', UserDemo.created_at)) + assert new_query.build() == ( + 'SELECT date_part(\'year\', "userdemo"."created_at") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_extract(): + new_query = QueryBuilder().select(func.extract('month', UserDemo.created_at)) + assert new_query.build() == ( + 'SELECT extract(month from "userdemo"."created_at") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_age(): + # Test age with single argument + new_query = QueryBuilder().select(func.age(UserDemo.birth_date)) + assert new_query.build() == ( + 'SELECT age("userdemo"."birth_date") AS aggregate_0 FROM "userdemo"', + [], + ) + + # Test age with two arguments + new_query = QueryBuilder().select(func.age(UserDemo.end_date, UserDemo.start_date)) + assert new_query.build() == ( + 'SELECT age("userdemo"."end_date", "userdemo"."start_date") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_current_date(): + new_query = QueryBuilder().select(func.current_date()) + assert new_query.build() == ( + 'SELECT current_date AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_current_time(): + new_query = QueryBuilder().select(func.current_time()) + assert new_query.build() == ( + 'SELECT current_time AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_current_timestamp(): + new_query = QueryBuilder().select(func.current_timestamp()) + assert new_query.build() == ( + 'SELECT current_timestamp AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_date(): + new_query = QueryBuilder().select(func.date(UserDemo.created_at)) + assert new_query.build() == ( + 'SELECT date("userdemo"."created_at") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_make_date(): + new_query = QueryBuilder().select(func.make_date(UserDemo.year, UserDemo.month, UserDemo.day)) + assert new_query.build() == ( + 'SELECT make_date("userdemo"."year", "userdemo"."month", "userdemo"."day") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_make_time(): + new_query = QueryBuilder().select(func.make_time(UserDemo.hour, UserDemo.minute, UserDemo.second)) + assert new_query.build() == ( + 'SELECT make_time("userdemo"."hour", "userdemo"."minute", "userdemo"."second") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_make_timestamp(): + new_query = QueryBuilder().select( + func.make_timestamp( + UserDemo.year, + UserDemo.month, + UserDemo.day, + UserDemo.hour, + UserDemo.minute, + UserDemo.second + ) + ) + assert new_query.build() == ( + 'SELECT make_timestamp("userdemo"."year", "userdemo"."month", "userdemo"."day", ' + '"userdemo"."hour", "userdemo"."minute", "userdemo"."second") AS aggregate_0 FROM "userdemo"', + [], + ) + + +def test_function_make_interval(): + # Test with some components + new_query = QueryBuilder().select( + func.make_interval(years=UserDemo.years, months=UserDemo.months, days=UserDemo.days) + ) + assert new_query.build() == ( + 'SELECT make_interval(years => "userdemo"."years", months => "userdemo"."months", ' + 'days => "userdemo"."days") AS aggregate_0 FROM "userdemo"', + [], + ) + + # Test with all components + new_query = QueryBuilder().select( + func.make_interval( + years=UserDemo.years, + months=UserDemo.months, + weeks=UserDemo.weeks, + days=UserDemo.days, + hours=UserDemo.hours, + mins=UserDemo.minutes, + secs=UserDemo.seconds + ) + ) + assert new_query.build() == ( + 'SELECT make_interval(years => "userdemo"."years", months => "userdemo"."months", ' + 'weeks => "userdemo"."weeks", days => "userdemo"."days", hours => "userdemo"."hours", ' + 'mins => "userdemo"."minutes", secs => "userdemo"."seconds") AS aggregate_0 FROM "userdemo"', + [], + ) + + # Test with no components should raise ValueError + with pytest.raises(ValueError): + QueryBuilder().select(func.make_interval()) + + +def test_function_transformations(): + # Test string functions + new_query = QueryBuilder().select(( + func.lower(UserDemo.name), + func.upper(UserDemo.name), + func.length(UserDemo.name), + func.trim(UserDemo.name), + func.substring(UserDemo.name, 1, 3), + )) + assert new_query.build() == ( + 'SELECT lower("userdemo"."name") AS aggregate_0, ' + 'upper("userdemo"."name") AS aggregate_1, ' + 'length("userdemo"."name") AS aggregate_2, ' + 'trim("userdemo"."name") AS aggregate_3, ' + 'substring("userdemo"."name" from 1 for 3) AS aggregate_4 ' + 'FROM "userdemo"', + [], + ) + + # Test mathematical functions + new_query = QueryBuilder().select(( + func.round(UserDemo.balance), + func.ceil(UserDemo.balance), + func.floor(UserDemo.balance), + func.power(UserDemo.balance, 2), + func.sqrt(UserDemo.balance), + )) + assert new_query.build() == ( + 'SELECT round("userdemo"."balance") AS aggregate_0, ' + 'ceil("userdemo"."balance") AS aggregate_1, ' + 'floor("userdemo"."balance") AS aggregate_2, ' + 'power("userdemo"."balance", 2) AS aggregate_3, ' + 'sqrt("userdemo"."balance") AS aggregate_4 ' + 'FROM "userdemo"', + [], + ) + + # Test aggregate functions + new_query = QueryBuilder().select(( + func.array_agg(UserDemo.name), + func.string_agg(UserDemo.name, ','), + )) + assert new_query.build() == ( + 'SELECT array_agg("userdemo"."name") AS aggregate_0, ' + 'string_agg("userdemo"."name", \',\') AS aggregate_1 ' + 'FROM "userdemo"', + [], + ) + + # Test window functions + new_query = QueryBuilder().select(( + func.row_number().over(), + func.rank().over(), + func.dense_rank().over(), + func.lag(UserDemo.balance).over(), + func.lead(UserDemo.balance).over(), + )) + assert new_query.build() == ( + 'SELECT row_number() OVER () AS aggregate_0, ' + 'rank() OVER () AS aggregate_1, ' + 'dense_rank() OVER () AS aggregate_2, ' + 'lag("userdemo"."balance") OVER () AS aggregate_3, ' + 'lead("userdemo"."balance") OVER () AS aggregate_4 ' + 'FROM "userdemo"', + [], + ) + + # Test type conversion functions + new_query = QueryBuilder().select(( + func.cast(UserDemo.balance, 'integer'), + func.to_char(UserDemo.created_at, 'YYYY-MM-DD'), + func.to_number(UserDemo.balance_str, '999999.99'), + func.to_timestamp(UserDemo.timestamp_str, 'YYYY-MM-DD HH24:MI:SS'), + )) + assert new_query.build() == ( + 'SELECT cast("userdemo"."balance" as integer) AS aggregate_0, ' + 'to_char("userdemo"."created_at", \'YYYY-MM-DD\') AS aggregate_1, ' + 'to_number("userdemo"."balance_str", \'999999.99\') AS aggregate_2, ' + 'to_timestamp("userdemo"."timestamp_str", \'YYYY-MM-DD HH24:MI:SS\') AS aggregate_3 ' + 'FROM "userdemo"', + [], + ) + + def test_invalid_where_condition(): with pytest.raises(ValueError): QueryBuilder().select(UserDemo.id).where("invalid condition") # type: ignore diff --git a/iceaxe/functions.py b/iceaxe/functions.py index ca315e3..c634aa7 100644 --- a/iceaxe/functions.py +++ b/iceaxe/functions.py @@ -211,6 +211,551 @@ def min(self, field: T) -> T: metadata.literal = QueryLiteral(f"min({metadata.literal})") return cast(T, metadata) + def abs(self, field: T) -> T: + """ + Creates an ABS function call to get the absolute value. + + :param field: The numeric field to get the absolute value of + :return: A function metadata object preserving the input type + + ```python {{sticky: True}} + # Get absolute value of balance + abs_balance = await conn.execute(select(func.abs(Account.balance))) + ``` + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"abs({metadata.literal})") + return cast(T, metadata) + + def date_trunc(self, precision: str, field: T) -> T: + """ + Truncates a timestamp or interval value to specified precision. + + :param precision: The precision to truncate to ('microseconds', 'milliseconds', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade', 'century', 'millennium') + :param field: The timestamp or interval field to truncate + :return: A function metadata object preserving the input type + + ```python {{sticky: True}} + # Truncate timestamp to month + monthly = await conn.execute(select(func.date_trunc('month', User.created_at))) + ``` + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"date_trunc('{precision}', {metadata.literal})") + return cast(T, metadata) + + def date_part(self, field: str, source: T) -> int: + """ + Extracts a subfield from a date/time value. + + :param field: The subfield to extract ('century', 'day', 'decade', 'dow', 'doy', 'epoch', 'hour', 'isodow', 'isoyear', 'microseconds', 'millennium', 'milliseconds', 'minute', 'month', 'quarter', 'second', 'timezone', 'timezone_hour', 'timezone_minute', 'week', 'year') + :param source: The date/time field to extract from + :return: A function metadata object that resolves to an integer + + ```python {{sticky: True}} + # Get month from timestamp + month = await conn.execute(select(func.date_part('month', User.created_at))) + ``` + """ + metadata = self._column_to_metadata(source) + metadata.literal = QueryLiteral(f"date_part('{field}', {metadata.literal})") + return cast(int, metadata) + + def extract(self, field: str, source: T) -> int: + """ + Extracts a subfield from a date/time value using SQL standard syntax. + + :param field: The subfield to extract ('century', 'day', 'decade', 'dow', 'doy', 'epoch', 'hour', 'isodow', 'isoyear', 'microseconds', 'millennium', 'milliseconds', 'minute', 'month', 'quarter', 'second', 'timezone', 'timezone_hour', 'timezone_minute', 'week', 'year') + :param source: The date/time field to extract from + :return: A function metadata object that resolves to an integer + + ```python {{sticky: True}} + # Get year from timestamp + year = await conn.execute(select(func.extract('year', User.created_at))) + ``` + """ + metadata = self._column_to_metadata(source) + metadata.literal = QueryLiteral(f"extract({field} from {metadata.literal})") + return cast(int, metadata) + + def age(self, timestamp: T, reference: T | None = None) -> T: + """ + Calculates the difference between two timestamps. + If reference is not provided, current_date is used. + + :param timestamp: The timestamp to calculate age from + :param reference: Optional reference timestamp (defaults to current_date) + :return: A function metadata object preserving the input type + + ```python {{sticky: True}} + # Get age of a timestamp + age = await conn.execute(select(func.age(User.birth_date))) + + # Get age between two timestamps + age_diff = await conn.execute(select(func.age(Event.end_time, Event.start_time))) + ``` + """ + metadata = self._column_to_metadata(timestamp) + if reference is not None: + ref_metadata = self._column_to_metadata(reference) + metadata.literal = QueryLiteral(f"age({metadata.literal}, {ref_metadata.literal})") + else: + metadata.literal = QueryLiteral(f"age({metadata.literal})") + return cast(T, metadata) + + def current_date(self) -> T: + """ + Returns the current date. + + :return: A function metadata object that resolves to a date + + ```python {{sticky: True}} + # Get current date + today = await conn.execute(select(func.current_date())) + ``` + """ + metadata = FunctionMetadata( + literal=QueryLiteral("current_date"), + original_field=None, # type: ignore + ) + return cast(T, metadata) + + def current_time(self) -> T: + """ + Returns the current time with time zone. + + :return: A function metadata object that resolves to a time with time zone + + ```python {{sticky: True}} + # Get current time + now = await conn.execute(select(func.current_time())) + ``` + """ + metadata = FunctionMetadata( + literal=QueryLiteral("current_time"), + original_field=None, # type: ignore + ) + return cast(T, metadata) + + def current_timestamp(self) -> T: + """ + Returns the current timestamp with time zone. + + :return: A function metadata object that resolves to a timestamp with time zone + + ```python {{sticky: True}} + # Get current timestamp + now = await conn.execute(select(func.current_timestamp())) + ``` + """ + metadata = FunctionMetadata( + literal=QueryLiteral("current_timestamp"), + original_field=None, # type: ignore + ) + return cast(T, metadata) + + def date(self, field: T) -> T: + """ + Converts a timestamp to a date by dropping the time component. + + :param field: The timestamp field to convert + :return: A function metadata object that resolves to a date + + ```python {{sticky: True}} + # Get just the date part + event_date = await conn.execute(select(func.date(Event.timestamp))) + ``` + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"date({metadata.literal})") + return cast(T, metadata) + + def make_date(self, year: T, month: T, day: T) -> T: + """ + Creates a date from year, month, and day values. + + :param year: The year value + :param month: The month value (1-12) + :param day: The day value (1-31) + :return: A function metadata object that resolves to a date + + ```python {{sticky: True}} + # Create a date from components + date = await conn.execute(select(func.make_date(2023, 12, 25))) + ``` + """ + year_meta = self._column_to_metadata(year) + month_meta = self._column_to_metadata(month) + day_meta = self._column_to_metadata(day) + metadata = FunctionMetadata( + literal=QueryLiteral(f"make_date({year_meta.literal}, {month_meta.literal}, {day_meta.literal})"), + original_field=year_meta.original_field, + ) + return cast(T, metadata) + + def make_time(self, hour: T, min: T, sec: T) -> T: + """ + Creates a time from hour, minute, and second values. + + :param hour: The hour value (0-23) + :param min: The minute value (0-59) + :param sec: The second value (0-59.999999) + :return: A function metadata object that resolves to a time + + ```python {{sticky: True}} + # Create a time from components + time = await conn.execute(select(func.make_time(14, 30, 0))) + ``` + """ + hour_meta = self._column_to_metadata(hour) + min_meta = self._column_to_metadata(min) + sec_meta = self._column_to_metadata(sec) + metadata = FunctionMetadata( + literal=QueryLiteral(f"make_time({hour_meta.literal}, {min_meta.literal}, {sec_meta.literal})"), + original_field=hour_meta.original_field, + ) + return cast(T, metadata) + + def make_timestamp(self, year: T, month: T, day: T, hour: T, min: T, sec: T) -> T: + """ + Creates a timestamp from year, month, day, hour, minute, and second values. + + :param year: The year value + :param month: The month value (1-12) + :param day: The day value (1-31) + :param hour: The hour value (0-23) + :param min: The minute value (0-59) + :param sec: The second value (0-59.999999) + :return: A function metadata object that resolves to a timestamp + + ```python {{sticky: True}} + # Create a timestamp from components + ts = await conn.execute(select(func.make_timestamp(2023, 12, 25, 14, 30, 0))) + ``` + """ + year_meta = self._column_to_metadata(year) + month_meta = self._column_to_metadata(month) + day_meta = self._column_to_metadata(day) + hour_meta = self._column_to_metadata(hour) + min_meta = self._column_to_metadata(min) + sec_meta = self._column_to_metadata(sec) + metadata = FunctionMetadata( + literal=QueryLiteral( + f"make_timestamp({year_meta.literal}, {month_meta.literal}, {day_meta.literal}, " + f"{hour_meta.literal}, {min_meta.literal}, {sec_meta.literal})" + ), + original_field=year_meta.original_field, + ) + return cast(T, metadata) + + def make_interval(self, years: T | None = None, months: T | None = None, weeks: T | None = None, + days: T | None = None, hours: T | None = None, mins: T | None = None, + secs: T | None = None) -> T: + """ + Creates an interval from various time unit values. + + :param years: Number of years + :param months: Number of months + :param weeks: Number of weeks + :param days: Number of days + :param hours: Number of hours + :param mins: Number of minutes + :param secs: Number of seconds + :return: A function metadata object that resolves to an interval + + ```python {{sticky: True}} + # Create an interval + interval = await conn.execute( + select(func.make_interval(years=1, months=6, days=15)) + ) + ``` + """ + parts = [] + if years is not None: + years_meta = self._column_to_metadata(years) + parts.append(f"years => {years_meta.literal}") + original_field = years_meta.original_field + if months is not None: + months_meta = self._column_to_metadata(months) + parts.append(f"months => {months_meta.literal}") + original_field = months_meta.original_field + if weeks is not None: + weeks_meta = self._column_to_metadata(weeks) + parts.append(f"weeks => {weeks_meta.literal}") + original_field = weeks_meta.original_field + if days is not None: + days_meta = self._column_to_metadata(days) + parts.append(f"days => {days_meta.literal}") + original_field = days_meta.original_field + if hours is not None: + hours_meta = self._column_to_metadata(hours) + parts.append(f"hours => {hours_meta.literal}") + original_field = hours_meta.original_field + if mins is not None: + mins_meta = self._column_to_metadata(mins) + parts.append(f"mins => {mins_meta.literal}") + original_field = mins_meta.original_field + if secs is not None: + secs_meta = self._column_to_metadata(secs) + parts.append(f"secs => {secs_meta.literal}") + original_field = secs_meta.original_field + + if not parts: + raise ValueError("At least one interval component must be specified") + + metadata = FunctionMetadata( + literal=QueryLiteral(f"make_interval({', '.join(parts)})"), + original_field=original_field, # type: ignore + ) + return cast(T, metadata) + + # String Functions + def lower(self, field: T) -> T: + """ + Converts string to lowercase. + + :param field: The string field to convert + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"lower({metadata.literal})") + return cast(T, metadata) + + def upper(self, field: T) -> T: + """ + Converts string to uppercase. + + :param field: The string field to convert + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"upper({metadata.literal})") + return cast(T, metadata) + + def length(self, field: T) -> int: + """ + Returns length of string. + + :param field: The string field to measure + :return: A function metadata object that resolves to an integer + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"length({metadata.literal})") + return cast(int, metadata) + + def trim(self, field: T) -> T: + """ + Removes whitespace from both ends of string. + + :param field: The string field to trim + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"trim({metadata.literal})") + return cast(T, metadata) + + def substring(self, field: T, start: int, length: int) -> T: + """ + Extracts substring. + + :param field: The string field to extract from + :param start: Starting position (1-based) + :param length: Number of characters to extract + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"substring({metadata.literal} from {start} for {length})") + return cast(T, metadata) + + # Mathematical Functions + def round(self, field: T) -> T: + """ + Rounds to nearest integer. + + :param field: The numeric field to round + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"round({metadata.literal})") + return cast(T, metadata) + + def ceil(self, field: T) -> T: + """ + Rounds up to nearest integer. + + :param field: The numeric field to round up + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"ceil({metadata.literal})") + return cast(T, metadata) + + def floor(self, field: T) -> T: + """ + Rounds down to nearest integer. + + :param field: The numeric field to round down + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"floor({metadata.literal})") + return cast(T, metadata) + + def power(self, field: T, exponent: int | float) -> T: + """ + Raises a number to the specified power. + + :param field: The numeric field to raise + :param exponent: The power to raise to + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"power({metadata.literal}, {exponent})") + return cast(T, metadata) + + def sqrt(self, field: T) -> T: + """ + Calculates square root. + + :param field: The numeric field to calculate square root of + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"sqrt({metadata.literal})") + return cast(T, metadata) + + # Aggregate Functions + def array_agg(self, field: T) -> list[T]: + """ + Collects values into an array. + + :param field: The field to aggregate + :return: A function metadata object that resolves to a list + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"array_agg({metadata.literal})") + return cast(list[T], metadata) + + def string_agg(self, field: T, delimiter: str) -> str: + """ + Concatenates values with delimiter. + + :param field: The field to aggregate + :param delimiter: The delimiter to use between values + :return: A function metadata object that resolves to a string + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"string_agg({metadata.literal}, '{delimiter}')") + return cast(str, metadata) + + # Window Functions + def row_number(self) -> int: + """ + Returns the row number within the current partition. + + :return: A function metadata object that resolves to an integer + """ + metadata = FunctionMetadata( + literal=QueryLiteral("row_number()"), + original_field=None, # type: ignore + ) + return cast(int, metadata) + + def rank(self) -> int: + """ + Returns the rank with gaps. + + :return: A function metadata object that resolves to an integer + """ + metadata = FunctionMetadata( + literal=QueryLiteral("rank()"), + original_field=None, # type: ignore + ) + return cast(int, metadata) + + def dense_rank(self) -> int: + """ + Returns the rank without gaps. + + :return: A function metadata object that resolves to an integer + """ + metadata = FunctionMetadata( + literal=QueryLiteral("dense_rank()"), + original_field=None, # type: ignore + ) + return cast(int, metadata) + + def lag(self, field: T) -> T: + """ + Returns value from previous row. + + :param field: The field to get previous value of + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"lag({metadata.literal})") + return cast(T, metadata) + + def lead(self, field: T) -> T: + """ + Returns value from next row. + + :param field: The field to get next value of + :return: A function metadata object preserving the input type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"lead({metadata.literal})") + return cast(T, metadata) + + # Type Conversion Functions + def cast(self, field: T, type_name: str) -> Any: + """ + Converts value to specified type. + + :param field: The field to convert + :param type_name: The target type name + :return: A function metadata object with the new type + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"cast({metadata.literal} as {type_name})") + return metadata + + def to_char(self, field: T, format: str) -> str: + """ + Converts value to string with format. + + :param field: The field to convert + :param format: The format string + :return: A function metadata object that resolves to a string + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"to_char({metadata.literal}, '{format}')") + return cast(str, metadata) + + def to_number(self, field: T, format: str) -> float: + """ + Converts string to number with format. + + :param field: The string field to convert + :param format: The format string + :return: A function metadata object that resolves to a float + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"to_number({metadata.literal}, '{format}')") + return cast(float, metadata) + + def to_timestamp(self, field: T, format: str) -> T: + """ + Converts string to timestamp with format. + + :param field: The string field to convert + :param format: The format string + :return: A function metadata object that resolves to a timestamp + """ + metadata = self._column_to_metadata(field) + metadata.literal = QueryLiteral(f"to_timestamp({metadata.literal}, '{format}')") + return cast(T, metadata) + def _column_to_metadata(self, field: Any) -> FunctionMetadata: """ Internal helper method to convert a field to FunctionMetadata.