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
22 changes: 15 additions & 7 deletions src/sqlbuild/adapter/contract/classes/base_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def render_max_cursor_at_or_before(

quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand All @@ -425,9 +425,11 @@ def build_cursor_filter(

if cursor_column is None or start_cursor is None:
return ""
clauses: list[str] = [f"{cursor_column} >= '{start_cursor.value}'"]
clauses: list[str] = [
f"{cursor_column} >= {self._quote_sql_string(str(start_cursor.value))}"
]
if end_cursor is not None:
clauses.append(f"{cursor_column} < '{end_cursor.value}'")
clauses.append(f"{cursor_column} < {self._quote_sql_string(str(end_cursor.value))}")
return " AND ".join(clauses)

def schema_exists(
Expand Down Expand Up @@ -710,8 +712,9 @@ def render_delete_insert_cursor(
) -> tuple[str, ...]:
delete_sql: str = (
f"DELETE FROM {destination} "
f"WHERE {self.render_identifier(cursor_column)} >= '{cursor_start}' "
f"AND {self.render_identifier(cursor_column)} < '{cursor_end}'"
f"WHERE {self.render_identifier(cursor_column)} >= "
f"{self._quote_sql_string(cursor_start)} "
f"AND {self.render_identifier(cursor_column)} < {self._quote_sql_string(cursor_end)}"
)
insert_stmts: tuple[str, ...] = self.render_append(
destination=destination, sql=sql, columns=columns
Expand Down Expand Up @@ -2482,14 +2485,19 @@ def expression_inference_profile(self) -> ExpressionInferenceProfile:

return ExpressionInferenceProfile(sql_analysis_dialect=self.sql_analysis_dialect())

def _quote_sql_string(self, value: str) -> str:
"""Quote one string literal in this adapter's dialect; adapters override the escaping."""

return _quote_sql_string(value)

def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) -> str:
"""Render one generic cursor bound literal from a normalized string value."""

if cursor_type == CursorKind.INTEGER:
return value
if cursor_type == CursorKind.TIMESTAMP:
return f"TIMESTAMP '{value}'"
return f"'{value}'"
return f"TIMESTAMP {self._quote_sql_string(value)}"
return self._quote_sql_string(value)

def default_table_promotion_mode(self) -> TablePromotionMode:
"""Return staged as the generic default promotion mode."""
Expand Down
17 changes: 10 additions & 7 deletions src/sqlbuild/adapter/contract/classes/duckdb_backed_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,8 +1075,8 @@ def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) ->
if cursor_type == CursorKind.INTEGER:
return value
if cursor_type == CursorKind.TIMESTAMP:
return f"TIMESTAMP '{value}'"
return f"'{value}'"
return f"TIMESTAMP {self._quote_sql_string(value)}"
return self._quote_sql_string(value)

def connect(self, config: dict[str, Any]) -> Any:
"""Open DuckDB, waiting up to five seconds for another process's database file lock."""
Expand Down Expand Up @@ -1197,7 +1197,7 @@ def render_max_cursor_at_or_before(
) -> str:
quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand All @@ -1217,9 +1217,11 @@ def build_cursor_filter(

if cursor_column is None or start_cursor is None:
return ""
clauses: list[str] = [f"{cursor_column} >= '{start_cursor.value}'"]
clauses: list[str] = [
f"{cursor_column} >= {self._quote_sql_string(str(start_cursor.value))}"
]
if end_cursor is not None:
clauses.append(f"{cursor_column} < '{end_cursor.value}'")
clauses.append(f"{cursor_column} < {self._quote_sql_string(str(end_cursor.value))}")
return " AND ".join(clauses)

def duckdb_build_attach_sql(self, attach_entry: dict[str, object]) -> str:
Expand Down Expand Up @@ -1604,8 +1606,9 @@ def render_delete_insert_cursor(
) -> tuple[str, ...]:
delete_sql: str = (
f"DELETE FROM {destination} "
f"WHERE {self.render_identifier(cursor_column)} >= '{cursor_start}' "
f"AND {self.render_identifier(cursor_column)} < '{cursor_end}'"
f"WHERE {self.render_identifier(cursor_column)} >= "
f"{self._quote_sql_string(cursor_start)} "
f"AND {self.render_identifier(cursor_column)} < {self._quote_sql_string(cursor_end)}"
)
insert_stmts: tuple[str, ...] = self.render_append(
destination=destination, sql=sql, columns=columns
Expand Down
14 changes: 8 additions & 6 deletions src/sqlbuild/adapters/bigquery/classes/bigquery_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1691,8 +1691,8 @@ def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) ->
if cursor_type == CursorKind.INTEGER:
return value
if cursor_type == CursorKind.TIMESTAMP:
return f"TIMESTAMP '{value}'"
return f"'{value}'"
return f"TIMESTAMP {self._quote_sql_string(value)}"
return self._quote_sql_string(value)

def default_table_promotion_mode(self) -> TablePromotionMode:
return TablePromotionMode.STAGED
Expand Down Expand Up @@ -1759,7 +1759,9 @@ def render_create_function(
)
package_sql: str = ""
if packages:
package_values: str = ", ".join(f"'{package}'" for package in packages)
package_values: str = ", ".join(
self._quote_sql_string(package) for package in packages
)
package_sql = f",\n packages = [{package_values}]"
return (
"CREATE OR REPLACE FUNCTION "
Expand Down Expand Up @@ -2354,7 +2356,7 @@ def render_max_cursor_at_or_before(
) -> str:
quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand Down Expand Up @@ -3094,7 +3096,7 @@ def _to_bigquery_type(column_type: str) -> str:
def _render_cursor_value_literal(cursor_value: CursorValue) -> str:
if cursor_value.kind == CursorKind.INTEGER:
return str(cursor_value.value)
return f"TIMESTAMP '{cursor_value.value}'"
return f"TIMESTAMP '{str(cursor_value.value).translate(STRING_LITERAL_ESCAPES)}'"

@staticmethod
def _to_int(value: object) -> int:
Expand All @@ -3107,5 +3109,5 @@ def _render_cursor_bound_string(value: str) -> str:
try:
int(value)
except ValueError:
return f"TIMESTAMP '{value}'"
return f"TIMESTAMP '{value.translate(STRING_LITERAL_ESCAPES)}'"
return value
30 changes: 15 additions & 15 deletions src/sqlbuild/adapters/databricks/classes/databricks_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@
from datetime import date, datetime
from decimal import Decimal
from pathlib import Path
from typing import Any, ClassVar
from typing import Any, ClassVar, cast

from sqlbuild._native import (
SeedCsvSettings,
SourceFreshnessRecord,
SqlLexicalSyntax,
SqlValue,
SqlValueKind,
render_insert_source_freshness_records_sql,
)
from sqlbuild.adapter.contract.classes.base_adapter import (
BaseAdapter,
_encode_typed_json,
_render_ansi_typed_scalar,
_render_typed_value_list,
_typed_scalar_payload,
)
from sqlbuild.adapter.contract.classes.consumption import ConsumptionMixin
from sqlbuild.adapter.contract.classes.historical_check_snapshot_sql import (
Expand Down Expand Up @@ -132,6 +134,7 @@
DETAILED_TABLE_INFORMATION_MARKER,
NON_ROW_RESULT_COLUMN_NAMES,
RELATION_NOT_FOUND_ERROR_CLASSES,
STRING_LITERAL_ESCAPES,
TABLE_RELATION_METADATA_TYPES,
TEMPORARY_FUNCTION_PREFIX,
UNDEFINED_TABLE_SQLSTATE,
Expand Down Expand Up @@ -884,6 +887,8 @@ def render_loader_logical_type(self, type_name: LoaderLogicalType) -> str:
return "STRING"

def render_typed_scalar(self, *, value: SqlValue) -> str:
if value.kind == SqlValueKind.STRING:
return self._quote_sql_string(cast(str, _typed_scalar_payload(value)))
return _render_ansi_typed_scalar(value=value)

def render_typed_value_list(self, *, value: SqlValue) -> str:
Expand Down Expand Up @@ -961,7 +966,7 @@ def _loader_row_sql_type(self, column_type: str | None) -> str:
return self._to_databricks_type(column_type)

def _quote_sql_string(self, value: str) -> str:
return "'" + value.replace("'", "''") + "'"
return "'" + value.translate(STRING_LITERAL_ESCAPES) + "'"

def render_create_initial_snapshot_destination(
self,
Expand Down Expand Up @@ -1830,8 +1835,8 @@ def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) ->
if cursor_type == CursorKind.INTEGER:
return value
if cursor_type == CursorKind.TIMESTAMP:
return f"TIMESTAMP '{value}'"
return f"'{value}'"
return f"TIMESTAMP {self._quote_sql_string(value)}"
return self._quote_sql_string(value)

def render_drop(self, *, destination: str, if_exists: bool = True) -> tuple[str, ...]:
exists_clause: str = " IF EXISTS" if if_exists else ""
Expand Down Expand Up @@ -1960,13 +1965,9 @@ def read_view_definition(
connection=connection,
sql=(
f"SELECT view_definition FROM {views} WHERE lower(table_schema) = lower("
+ "'"
+ schema.replace("'", "''")
+ "'"
+ self._quote_sql_string(schema)
+ ") AND lower(table_name) = lower("
+ "'"
+ name.replace("'", "''")
+ "'"
+ self._quote_sql_string(name)
+ ")"
),
).fetchall()
Expand Down Expand Up @@ -2444,7 +2445,7 @@ def render_max_cursor_at_or_before(
) -> str:
quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand Down Expand Up @@ -3191,8 +3192,7 @@ def _normalize_session_value(value: object | None) -> str | None:

@staticmethod
def _string_literal(value: str) -> str:
escaped_value: str = value.replace("'", "''")
return f"'{escaped_value}'"
return "'" + value.translate(STRING_LITERAL_ESCAPES) + "'"

@staticmethod
def _information_schema(database: str) -> str:
Expand Down Expand Up @@ -3221,13 +3221,13 @@ def _to_databricks_type(column_type: str) -> str:
def _render_cursor_value_literal(cursor_value: CursorValue) -> str:
if cursor_value.kind == CursorKind.INTEGER:
return str(cursor_value.value)
return f"TIMESTAMP '{cursor_value.value}'"
return f"TIMESTAMP '{str(cursor_value.value).translate(STRING_LITERAL_ESCAPES)}'"

@staticmethod
def _render_cursor_bound_string(value: str) -> str:
if value.isdigit():
return value
return f"TIMESTAMP '{value}'"
return f"TIMESTAMP '{value.translate(STRING_LITERAL_ESCAPES)}'"

@staticmethod
def _to_int(value: object) -> int:
Expand Down
1 change: 1 addition & 0 deletions src/sqlbuild/adapters/databricks/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@
USAGE_UNAVAILABLE_SQLSTATES: frozenset[str] = frozenset({"42501", "42P01", "42704"})
LINEAGE_VIEW_SOURCE_TYPES: tuple[str, ...] = ("VIEW", "MATERIALIZED_VIEW", "METRIC_VIEW")
CURRENT_CATALOG_INFORMATION_SCHEMA: str = "information_schema"
STRING_LITERAL_ESCAPES: dict[int, str] = str.maketrans({"\\": "\\\\", "'": "\\'"})
17 changes: 10 additions & 7 deletions src/sqlbuild/adapters/postgres/classes/postgres_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def render_max_cursor_at_or_before(
) -> str:
quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand All @@ -320,9 +320,11 @@ def build_cursor_filter(

if cursor_column is None or start_cursor is None:
return ""
clauses: list[str] = [f"{cursor_column} >= '{start_cursor.value}'"]
clauses: list[str] = [
f"{cursor_column} >= {self._quote_sql_string(str(start_cursor.value))}"
]
if end_cursor is not None:
clauses.append(f"{cursor_column} < '{end_cursor.value}'")
clauses.append(f"{cursor_column} < {self._quote_sql_string(str(end_cursor.value))}")
return " AND ".join(clauses)

def schema_exists(
Expand Down Expand Up @@ -632,8 +634,9 @@ def render_delete_insert_cursor(
) -> tuple[str, ...]:
quoted_cursor_column: str = self.render_identifier(cursor_column)
delete_sql: str = (
f"DELETE FROM {destination} WHERE {quoted_cursor_column} >= '{cursor_start}' "
f"AND {quoted_cursor_column} < '{cursor_end}'"
f"DELETE FROM {destination} WHERE {quoted_cursor_column} >= "
f"{self._quote_sql_string(cursor_start)} "
f"AND {quoted_cursor_column} < {self._quote_sql_string(cursor_end)}"
)
insert_stmts: tuple[str, ...] = self.render_append(
destination=destination, sql=sql, columns=columns
Expand Down Expand Up @@ -1932,8 +1935,8 @@ def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) ->
if cursor_type == CursorKind.INTEGER:
return value
if cursor_type == CursorKind.TIMESTAMP:
return f"TIMESTAMP '{value}'"
return f"'{value}'"
return f"TIMESTAMP {self._quote_sql_string(value)}"
return self._quote_sql_string(value)

def default_table_promotion_mode(self) -> TablePromotionMode:
"""Return staged as the generic default promotion mode."""
Expand Down
18 changes: 9 additions & 9 deletions src/sqlbuild/adapters/snowflake/classes/snowflake_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,8 +1164,9 @@ def render_delete_insert_cursor(
) -> tuple[str, ...]:
quoted_cursor_column: str = self.render_identifier(cursor_column)
delete_sql: str = (
f"DELETE FROM {destination} WHERE {quoted_cursor_column} >= '{cursor_start}' "
f"AND {quoted_cursor_column} < '{cursor_end}'"
f"DELETE FROM {destination} WHERE {quoted_cursor_column} >= "
f"{self._quote_sql_string(cursor_start)} "
f"AND {quoted_cursor_column} < {self._quote_sql_string(cursor_end)}"
)
insert_stmts: tuple[str, ...] = self.render_append(
destination=destination, sql=sql, columns=columns
Expand Down Expand Up @@ -3079,10 +3080,7 @@ def render_create_temporary_function(
def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) -> str:
if cursor_type == CursorKind.INTEGER:
return value
if cursor_type == CursorKind.TIMESTAMP:
escaped_value: str = value.replace("'", "''")
return f"'{escaped_value}'"
return f"'{value}'"
return self._quote_sql_string(value)

def interrupt_connection(self, connection: Any) -> bool:
"""Abort the connection's running statements through Snowflake's cancel request."""
Expand Down Expand Up @@ -3462,7 +3460,7 @@ def render_max_cursor_at_or_before(
) -> str:
quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand Down Expand Up @@ -3975,9 +3973,11 @@ def build_cursor_filter(

if cursor_column is None or start_cursor is None:
return ""
clauses: list[str] = [f"{cursor_column} >= '{start_cursor.value}'"]
clauses: list[str] = [
f"{cursor_column} >= {self._quote_sql_string(str(start_cursor.value))}"
]
if end_cursor is not None:
clauses.append(f"{cursor_column} < '{end_cursor.value}'")
clauses.append(f"{cursor_column} < {self._quote_sql_string(str(end_cursor.value))}")
return " AND ".join(clauses)

def _build_information_schema_type(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ def render_max_cursor_at_or_before(
) -> str:
quoted_cursor: str = self.render_identifier(cursor_column)
maximum_literal: str = (
f"CAST('{maximum_allowed}' AS DATE)"
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
if is_date
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def test_given_builtin_adapter_when_rendering_identifier_then_uses_adapter_quote
adapter_name=BuiltinAdapter.DATABRICKS.value,
value={"source": "loader's api"},
logical_type=LoaderLogicalType.JSON,
expected_literal="'{\"source\": \"loader''s api\"}'",
expected_literal=r"""'{"source": "loader\'s api"}'""",
),
AdapterLoaderValueLiteralTestCase(
description="duckdb escapes string literals",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@
AdapterTypedSqlRenderingTestCase(
description="databricks",
adapter_factory=DatabricksAdapter,
expected_string="'O''Brien'",
expected_string="'O\\'Brien'",
expected_true="TRUE",
expected_false="FALSE",
expected_integer="-7",
expected_float="1.25",
expected_decimal="2.4700",
expected_null="NULL",
expected_value_list="('GB', 'FR')",
expected_object='parse_json(\'{"amount":2.4700,"label":"O\'\'Brien"}\')',
expected_object='parse_json(\'{"amount":2.4700,"label":"O\\\'Brien"}\')',
),
AdapterTypedSqlRenderingTestCase(
description="sqlserver",
Expand Down
Loading
Loading