Skip to content

Commit a25cc5b

Browse files
Merge branch 'main' into window_null_skip_refactor
2 parents 1c036bf + bc33c98 commit a25cc5b

File tree

23 files changed

+495
-108
lines changed

23 files changed

+495
-108
lines changed

bigframes/bigquery/_operations/ai.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def generate(
8888
or pandas Series.
8989
connection_id (str, optional):
9090
Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`.
91-
If not provided, the connection from the current session will be used.
91+
If not provided, the query uses your end-user credential.
9292
endpoint (str, optional):
9393
Specifies the Vertex AI endpoint to use for the model. For example `"gemini-2.5-flash"`. You can specify any
9494
generally available or preview Gemini model. If you specify the model name, BigQuery ML automatically identifies and
@@ -131,7 +131,7 @@ def generate(
131131

132132
operator = ai_ops.AIGenerate(
133133
prompt_context=tuple(prompt_context),
134-
connection_id=_resolve_connection_id(series_list[0], connection_id),
134+
connection_id=connection_id,
135135
endpoint=endpoint,
136136
request_type=request_type,
137137
model_params=json.dumps(model_params) if model_params else None,
@@ -186,7 +186,7 @@ def generate_bool(
186186
or pandas Series.
187187
connection_id (str, optional):
188188
Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`.
189-
If not provided, the connection from the current session will be used.
189+
If not provided, the query uses your end-user credential.
190190
endpoint (str, optional):
191191
Specifies the Vertex AI endpoint to use for the model. For example `"gemini-2.5-flash"`. You can specify any
192192
generally available or preview Gemini model. If you specify the model name, BigQuery ML automatically identifies and
@@ -216,7 +216,7 @@ def generate_bool(
216216

217217
operator = ai_ops.AIGenerateBool(
218218
prompt_context=tuple(prompt_context),
219-
connection_id=_resolve_connection_id(series_list[0], connection_id),
219+
connection_id=connection_id,
220220
endpoint=endpoint,
221221
request_type=request_type,
222222
model_params=json.dumps(model_params) if model_params else None,
@@ -267,7 +267,7 @@ def generate_int(
267267
or pandas Series.
268268
connection_id (str, optional):
269269
Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`.
270-
If not provided, the connection from the current session will be used.
270+
If not provided, the query uses your end-user credential.
271271
endpoint (str, optional):
272272
Specifies the Vertex AI endpoint to use for the model. For example `"gemini-2.5-flash"`. You can specify any
273273
generally available or preview Gemini model. If you specify the model name, BigQuery ML automatically identifies and
@@ -297,7 +297,7 @@ def generate_int(
297297

298298
operator = ai_ops.AIGenerateInt(
299299
prompt_context=tuple(prompt_context),
300-
connection_id=_resolve_connection_id(series_list[0], connection_id),
300+
connection_id=connection_id,
301301
endpoint=endpoint,
302302
request_type=request_type,
303303
model_params=json.dumps(model_params) if model_params else None,
@@ -348,7 +348,7 @@ def generate_double(
348348
or pandas Series.
349349
connection_id (str, optional):
350350
Specifies the connection to use to communicate with the model. For example, `myproject.us.myconnection`.
351-
If not provided, the connection from the current session will be used.
351+
If not provided, the query uses your end-user credential.
352352
endpoint (str, optional):
353353
Specifies the Vertex AI endpoint to use for the model. For example `"gemini-2.5-flash"`. You can specify any
354354
generally available or preview Gemini model. If you specify the model name, BigQuery ML automatically identifies and
@@ -378,7 +378,7 @@ def generate_double(
378378

379379
operator = ai_ops.AIGenerateDouble(
380380
prompt_context=tuple(prompt_context),
381-
connection_id=_resolve_connection_id(series_list[0], connection_id),
381+
connection_id=connection_id,
382382
endpoint=endpoint,
383383
request_type=request_type,
384384
model_params=json.dumps(model_params) if model_params else None,

bigframes/core/compile/sqlglot/expressions/ai_ops.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,13 @@ def _construct_named_args(op: ops.NaryOp) -> list[sge.Kwarg]:
104104

105105
op_args = asdict(op)
106106

107-
connection_id = op_args["connection_id"]
108-
args.append(
109-
sge.Kwarg(this="connection_id", expression=sge.Literal.string(connection_id))
110-
)
107+
connection_id = op_args.get("connection_id", None)
108+
if connection_id is not None:
109+
args.append(
110+
sge.Kwarg(
111+
this="connection_id", expression=sge.Literal.string(connection_id)
112+
)
113+
)
111114

112115
endpoit = op_args.get("endpoint", None)
113116
if endpoit is not None:

bigframes/display/anywidget.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ class TableWidget(WIDGET_BASE):
5555

5656
page = traitlets.Int(0).tag(sync=True)
5757
page_size = traitlets.Int(0).tag(sync=True)
58-
row_count = traitlets.Int(0).tag(sync=True)
58+
row_count = traitlets.Union(
59+
[traitlets.Int(), traitlets.Instance(type(None))],
60+
default_value=None,
61+
allow_none=True,
62+
).tag(sync=True)
5963
table_html = traitlets.Unicode().tag(sync=True)
6064
_initial_load_complete = traitlets.Bool(False).tag(sync=True)
6165
_batches: Optional[blocks.PandasBatches] = None
@@ -94,12 +98,17 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
9498
# SELECT COUNT(*) query. It is a must have however.
9599
# TODO(b/428238610): Start iterating over the result of `to_pandas_batches()`
96100
# before we get here so that the count might already be cached.
97-
# TODO(b/452747934): Allow row_count to be None and check to see if
98-
# there are multiple pages and show "page 1 of many" in this case
99101
self._reset_batches_for_new_page_size()
100-
if self._batches is None or self._batches.total_rows is None:
101-
self._error_message = "Could not determine total row count. Data might be unavailable or an error occurred."
102-
self.row_count = 0
102+
103+
if self._batches is None:
104+
self._error_message = "Could not retrieve data batches. Data might be unavailable or an error occurred."
105+
self.row_count = None
106+
elif self._batches.total_rows is None:
107+
# Total rows is unknown, this is an expected state.
108+
# TODO(b/461536343): Cheaply discover if we have exactly 1 page.
109+
# There are cases where total rows is not set, but there are no additional
110+
# pages. We could disable the "next" button in these cases.
111+
self.row_count = None
103112
else:
104113
self.row_count = self._batches.total_rows
105114

@@ -131,11 +140,22 @@ def _validate_page(self, proposal: Dict[str, Any]) -> int:
131140
Returns:
132141
The validated and clamped page number as an integer.
133142
"""
134-
135143
value = proposal["value"]
144+
145+
if value < 0:
146+
raise ValueError("Page number cannot be negative.")
147+
148+
# If truly empty or invalid page size, stay on page 0.
149+
# This handles cases where row_count is 0 or page_size is 0, preventing
150+
# division by zero or nonsensical pagination, regardless of row_count being None.
136151
if self.row_count == 0 or self.page_size == 0:
137152
return 0
138153

154+
# If row count is unknown, allow any non-negative page. The previous check
155+
# ensures that invalid page_size (0) is already handled.
156+
if self.row_count is None:
157+
return value
158+
139159
# Calculate the zero-indexed maximum page number.
140160
max_page = max(0, math.ceil(self.row_count / self.page_size) - 1)
141161

@@ -229,6 +249,23 @@ def _set_table_html(self) -> None:
229249
# Get the data for the current page
230250
page_data = cached_data.iloc[start:end]
231251

252+
# Handle case where user navigated beyond available data with unknown row count
253+
is_unknown_count = self.row_count is None
254+
is_beyond_data = self._all_data_loaded and len(page_data) == 0 and self.page > 0
255+
if is_unknown_count and is_beyond_data:
256+
# Calculate the last valid page (zero-indexed)
257+
total_rows = len(cached_data)
258+
if total_rows > 0:
259+
last_valid_page = max(0, math.ceil(total_rows / self.page_size) - 1)
260+
# Navigate back to the last valid page
261+
self.page = last_valid_page
262+
# Recursively call to display the correct page
263+
return self._set_table_html()
264+
else:
265+
# If no data at all, stay on page 0 with empty display
266+
self.page = 0
267+
return self._set_table_html()
268+
232269
# Generate HTML table
233270
self.table_html = bigframes.display.html.render_html(
234271
dataframe=page_data,

bigframes/display/table_widget.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,21 @@ function render({ model, el }) {
8585
const rowCount = model.get(ModelProperty.ROW_COUNT);
8686
const pageSize = model.get(ModelProperty.PAGE_SIZE);
8787
const currentPage = model.get(ModelProperty.PAGE);
88-
const totalPages = Math.ceil(rowCount / pageSize);
89-
90-
rowCountLabel.textContent = `${rowCount.toLocaleString()} total rows`;
91-
paginationLabel.textContent = `Page ${(
92-
currentPage + 1
93-
).toLocaleString()} of ${(totalPages || 1).toLocaleString()}`;
94-
prevPage.disabled = currentPage === 0;
95-
nextPage.disabled = currentPage >= totalPages - 1;
88+
89+
if (rowCount === null) {
90+
// Unknown total rows
91+
rowCountLabel.textContent = "Total rows unknown";
92+
paginationLabel.textContent = `Page ${(currentPage + 1).toLocaleString()} of many`;
93+
prevPage.disabled = currentPage === 0;
94+
nextPage.disabled = false; // Allow navigation until we hit the end
95+
} else {
96+
// Known total rows
97+
const totalPages = Math.ceil(rowCount / pageSize);
98+
rowCountLabel.textContent = `${rowCount.toLocaleString()} total rows`;
99+
paginationLabel.textContent = `Page ${(currentPage + 1).toLocaleString()} of ${rowCount.toLocaleString()}`;
100+
prevPage.disabled = currentPage === 0;
101+
nextPage.disabled = currentPage >= totalPages - 1;
102+
}
96103
pageSizeSelect.value = pageSize;
97104
}
98105

bigframes/operations/ai_ops.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class AIGenerate(base_ops.NaryOp):
2929
name: ClassVar[str] = "ai_generate"
3030

3131
prompt_context: Tuple[str | None, ...]
32-
connection_id: str
32+
connection_id: str | None
3333
endpoint: str | None
3434
request_type: Literal["dedicated", "shared", "unspecified"]
3535
model_params: str | None
@@ -57,7 +57,7 @@ class AIGenerateBool(base_ops.NaryOp):
5757
name: ClassVar[str] = "ai_generate_bool"
5858

5959
prompt_context: Tuple[str | None, ...]
60-
connection_id: str
60+
connection_id: str | None
6161
endpoint: str | None
6262
request_type: Literal["dedicated", "shared", "unspecified"]
6363
model_params: str | None
@@ -79,7 +79,7 @@ class AIGenerateInt(base_ops.NaryOp):
7979
name: ClassVar[str] = "ai_generate_int"
8080

8181
prompt_context: Tuple[str | None, ...]
82-
connection_id: str
82+
connection_id: str | None
8383
endpoint: str | None
8484
request_type: Literal["dedicated", "shared", "unspecified"]
8585
model_params: str | None
@@ -101,7 +101,7 @@ class AIGenerateDouble(base_ops.NaryOp):
101101
name: ClassVar[str] = "ai_generate_double"
102102

103103
prompt_context: Tuple[str | None, ...]
104-
connection_id: str
104+
connection_id: str | None
105105
endpoint: str | None
106106
request_type: Literal["dedicated", "shared", "unspecified"]
107107
model_params: str | None

0 commit comments

Comments
 (0)