Skip to content

Commit b367cfc

Browse files
authored
Merge branch 'main' into build/smaller-wheels
2 parents 86c537a + d3fc0e0 commit b367cfc

31 files changed

Lines changed: 582 additions & 99 deletions

File tree

‎.release-please-manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.195.0"
2+
".": "0.196.1"
33
}

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [0.196.1](https://github.com/sqlstack-io/sqlbuild/compare/v0.196.0...v0.196.1) (2026-11-07)
4+
5+
6+
### Bug Fixes
7+
8+
* **bigquery:** render GoogleSQL column type changes and escapes ([#1341](https://github.com/sqlstack-io/sqlbuild/issues/1341)) ([18f2ac3](https://github.com/sqlstack-io/sqlbuild/commit/18f2ac3ed0edd93b599794640371227c6c324bfd))
9+
10+
## [0.196.0](https://github.com/sqlstack-io/sqlbuild/compare/v0.195.0...v0.196.0) (2026-11-07)
11+
12+
13+
### Features
14+
15+
* **ui:** wire the checks pages to the live control API ([#1338](https://github.com/sqlstack-io/sqlbuild/issues/1338)) ([d7ebacb](https://github.com/sqlstack-io/sqlbuild/commit/d7ebacbccfa6b7ab4b9145471fc88ffecff0042a))
16+
317
## [0.195.0](https://github.com/sqlstack-io/sqlbuild/compare/v0.194.0...v0.195.0) (2026-11-07)
418

519

‎Cargo.lock‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/sqlbuild-cli/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlbuild-cli"
3-
version = "0.195.0"
3+
version = "0.196.1"
44
description = "The native `sqb` executable: reused compiles and Python-free commands served without Python, every other command handed to the Python CLI."
55
edition.workspace = true
66
license-file = "LICENSE"

‎crates/sqlbuild-python/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlbuild-python"
3-
version = "0.195.0"
3+
version = "0.196.1"
44
description = "The sqlbuild._native Python extension module."
55
edition.workspace = true
66
license.workspace = true

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "maturin"
77

88
[project]
99
name = "sqlbuild"
10-
version = "0.195.0"
10+
version = "0.196.1"
1111
description = "Typed, test-first SQL pipelines with local E2E testing"
1212
readme = "README.md"
1313
requires-python = ">=3.12,<3.15"
@@ -18,7 +18,7 @@ dependencies = [
1818
"polyglot-sql-chio==0.13.10",
1919
"pydantic-settings>=2,<3",
2020
"PyYAML>=6.0.2",
21-
"sqlbuild-cli==0.195.0", # x-release-please-version
21+
"sqlbuild-cli==0.196.1", # x-release-please-version
2222
]
2323

2424
[project.optional-dependencies]

‎src/sqlbuild/adapter/contract/classes/base_adapter.py‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ def render_max_cursor_at_or_before(
405405

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

426426
if cursor_column is None or start_cursor is None:
427427
return ""
428-
clauses: list[str] = [f"{cursor_column} >= '{start_cursor.value}'"]
428+
clauses: list[str] = [
429+
f"{cursor_column} >= {self._quote_sql_string(str(start_cursor.value))}"
430+
]
429431
if end_cursor is not None:
430-
clauses.append(f"{cursor_column} < '{end_cursor.value}'")
432+
clauses.append(f"{cursor_column} < {self._quote_sql_string(str(end_cursor.value))}")
431433
return " AND ".join(clauses)
432434

433435
def schema_exists(
@@ -710,8 +712,9 @@ def render_delete_insert_cursor(
710712
) -> tuple[str, ...]:
711713
delete_sql: str = (
712714
f"DELETE FROM {destination} "
713-
f"WHERE {self.render_identifier(cursor_column)} >= '{cursor_start}' "
714-
f"AND {self.render_identifier(cursor_column)} < '{cursor_end}'"
715+
f"WHERE {self.render_identifier(cursor_column)} >= "
716+
f"{self._quote_sql_string(cursor_start)} "
717+
f"AND {self.render_identifier(cursor_column)} < {self._quote_sql_string(cursor_end)}"
715718
)
716719
insert_stmts: tuple[str, ...] = self.render_append(
717720
destination=destination, sql=sql, columns=columns
@@ -2482,14 +2485,19 @@ def expression_inference_profile(self) -> ExpressionInferenceProfile:
24822485

24832486
return ExpressionInferenceProfile(sql_analysis_dialect=self.sql_analysis_dialect())
24842487

2488+
def _quote_sql_string(self, value: str) -> str:
2489+
"""Quote one string literal in this adapter's dialect; adapters override the escaping."""
2490+
2491+
return _quote_sql_string(value)
2492+
24852493
def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) -> str:
24862494
"""Render one generic cursor bound literal from a normalized string value."""
24872495

24882496
if cursor_type == CursorKind.INTEGER:
24892497
return value
24902498
if cursor_type == CursorKind.TIMESTAMP:
2491-
return f"TIMESTAMP '{value}'"
2492-
return f"'{value}'"
2499+
return f"TIMESTAMP {self._quote_sql_string(value)}"
2500+
return self._quote_sql_string(value)
24932501

24942502
def default_table_promotion_mode(self) -> TablePromotionMode:
24952503
"""Return staged as the generic default promotion mode."""

‎src/sqlbuild/adapter/contract/classes/duckdb_backed_adapter.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,8 +1075,8 @@ def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) ->
10751075
if cursor_type == CursorKind.INTEGER:
10761076
return value
10771077
if cursor_type == CursorKind.TIMESTAMP:
1078-
return f"TIMESTAMP '{value}'"
1079-
return f"'{value}'"
1078+
return f"TIMESTAMP {self._quote_sql_string(value)}"
1079+
return self._quote_sql_string(value)
10801080

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

12181218
if cursor_column is None or start_cursor is None:
12191219
return ""
1220-
clauses: list[str] = [f"{cursor_column} >= '{start_cursor.value}'"]
1220+
clauses: list[str] = [
1221+
f"{cursor_column} >= {self._quote_sql_string(str(start_cursor.value))}"
1222+
]
12211223
if end_cursor is not None:
1222-
clauses.append(f"{cursor_column} < '{end_cursor.value}'")
1224+
clauses.append(f"{cursor_column} < {self._quote_sql_string(str(end_cursor.value))}")
12231225
return " AND ".join(clauses)
12241226

12251227
def duckdb_build_attach_sql(self, attach_entry: dict[str, object]) -> str:
@@ -1604,8 +1606,9 @@ def render_delete_insert_cursor(
16041606
) -> tuple[str, ...]:
16051607
delete_sql: str = (
16061608
f"DELETE FROM {destination} "
1607-
f"WHERE {self.render_identifier(cursor_column)} >= '{cursor_start}' "
1608-
f"AND {self.render_identifier(cursor_column)} < '{cursor_end}'"
1609+
f"WHERE {self.render_identifier(cursor_column)} >= "
1610+
f"{self._quote_sql_string(cursor_start)} "
1611+
f"AND {self.render_identifier(cursor_column)} < {self._quote_sql_string(cursor_end)}"
16091612
)
16101613
insert_stmts: tuple[str, ...] = self.render_append(
16111614
destination=destination, sql=sql, columns=columns

‎src/sqlbuild/adapters/bigquery/classes/bigquery_adapter.py‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,14 @@
134134
DECIMAL_METADATA_TYPE_NAMES,
135135
FLOAT_METADATA_TYPE_NAME,
136136
FLOAT_WIRE_TYPE_NAME,
137+
IDENTIFIER_ESCAPES,
137138
INTEGER_METADATA_TYPE_NAME,
138139
INTEGER_TYPE_TOKEN,
139140
INTEGER_WIRE_TYPE_NAME,
140141
NATIVE_TABLE_TYPE,
141142
NOT_FOUND_ERROR_CLASS_NAME,
142143
SELECT_STATEMENT_TYPE,
144+
STRING_LITERAL_ESCAPES,
143145
TABLE_NAME_WILDCARD,
144146
TIMESTAMP_TYPE_TOKEN,
145147
)
@@ -1026,7 +1028,7 @@ def render_alter_column_types(
10261028
) -> tuple[str, ...]:
10271029
return tuple(
10281030
f"ALTER TABLE {destination} ALTER COLUMN "
1029-
f"{self.render_identifier(col.name)} TYPE {col.type}"
1031+
f"{self.render_identifier(col.name)} SET DATA TYPE {col.type}"
10301032
for col in columns
10311033
)
10321034

@@ -1089,6 +1091,8 @@ def render_loader_logical_type(self, type_name: LoaderLogicalType) -> str:
10891091
def render_typed_scalar(self, *, value: SqlValue) -> str:
10901092
if value.kind == SqlValueKind.DECIMAL:
10911093
return f"NUMERIC {self._quote_sql_string(str(_typed_scalar_payload(value)))}"
1094+
if value.kind == SqlValueKind.STRING:
1095+
return self._quote_sql_string(cast(str, _typed_scalar_payload(value)))
10921096
return _render_ansi_typed_scalar(value=value)
10931097

10941098
def render_typed_value_list(self, *, value: SqlValue) -> str:
@@ -1173,13 +1177,13 @@ def _bigquery_loader_rows_projection_sql(
11731177
return f"CAST({literal} AS {self._loader_row_sql_type(sql_type)}) AS {quoted_column}"
11741178

11751179
def render_identifier(self, name: str) -> str:
1176-
return "`" + name.replace("`", "``") + "`"
1180+
return "`" + name.translate(IDENTIFIER_ESCAPES) + "`"
11771181

11781182
def render_exact_identifier(self, name: str) -> str:
11791183
return self.render_identifier(name)
11801184

11811185
def _quote_sql_string(self, value: str) -> str:
1182-
return "'" + value.replace("'", "''") + "'"
1186+
return "'" + self._escape_sql_string(value) + "'"
11831187

11841188
def render_create_initial_snapshot_destination(
11851189
self,
@@ -1687,8 +1691,8 @@ def render_cursor_bound_literal(self, *, value: str, cursor_type: str | None) ->
16871691
if cursor_type == CursorKind.INTEGER:
16881692
return value
16891693
if cursor_type == CursorKind.TIMESTAMP:
1690-
return f"TIMESTAMP '{value}'"
1691-
return f"'{value}'"
1694+
return f"TIMESTAMP {self._quote_sql_string(value)}"
1695+
return self._quote_sql_string(value)
16921696

16931697
def default_table_promotion_mode(self) -> TablePromotionMode:
16941698
return TablePromotionMode.STAGED
@@ -1755,7 +1759,9 @@ def render_create_function(
17551759
)
17561760
package_sql: str = ""
17571761
if packages:
1758-
package_values: str = ", ".join(f"'{package}'" for package in packages)
1762+
package_values: str = ", ".join(
1763+
self._quote_sql_string(package) for package in packages
1764+
)
17591765
package_sql = f",\n packages = [{package_values}]"
17601766
return (
17611767
"CREATE OR REPLACE FUNCTION "
@@ -2350,7 +2356,7 @@ def render_max_cursor_at_or_before(
23502356
) -> str:
23512357
quoted_cursor: str = self.render_identifier(cursor_column)
23522358
maximum_literal: str = (
2353-
f"CAST('{maximum_allowed}' AS DATE)"
2359+
f"CAST({self._quote_sql_string(maximum_allowed)} AS DATE)"
23542360
if is_date
23552361
else self.render_cursor_bound_literal(value=maximum_allowed, cursor_type=cursor_type)
23562362
)
@@ -3046,7 +3052,7 @@ def _table_storage_information_schema(cls, *, database: str | None, location: st
30463052

30473053
@staticmethod
30483054
def _escape_sql_string(value: str) -> str:
3049-
return value.replace("'", "''")
3055+
return value.translate(STRING_LITERAL_ESCAPES)
30503056

30513057
@staticmethod
30523058
def _strip_identifier_quotes(value: str) -> str:
@@ -3090,7 +3096,7 @@ def _to_bigquery_type(column_type: str) -> str:
30903096
def _render_cursor_value_literal(cursor_value: CursorValue) -> str:
30913097
if cursor_value.kind == CursorKind.INTEGER:
30923098
return str(cursor_value.value)
3093-
return f"TIMESTAMP '{cursor_value.value}'"
3099+
return f"TIMESTAMP '{str(cursor_value.value).translate(STRING_LITERAL_ESCAPES)}'"
30943100

30953101
@staticmethod
30963102
def _to_int(value: object) -> int:
@@ -3103,5 +3109,5 @@ def _render_cursor_bound_string(value: str) -> str:
31033109
try:
31043110
int(value)
31053111
except ValueError:
3106-
return f"TIMESTAMP '{value}'"
3112+
return f"TIMESTAMP '{value.translate(STRING_LITERAL_ESCAPES)}'"
31073113
return value

‎src/sqlbuild/adapters/bigquery/constants.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,10 @@
5252
+ _CLONE_NAME
5353
+ r"([^A-Za-z0-9_`-]|$)"
5454
)
55+
56+
STRING_LITERAL_ESCAPES: dict[int, str] = str.maketrans(
57+
{"\\": "\\\\", "'": "\\'", "\n": "\\n", "\r": "\\r"}
58+
)
59+
IDENTIFIER_ESCAPES: dict[int, str] = str.maketrans(
60+
{"\\": "\\\\", "`": "\\`", "\n": "\\n", "\r": "\\r"}
61+
)

0 commit comments

Comments
 (0)