vgi-python 0.43.1__py3-none-any.whl → 0.44.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -232,7 +232,9 @@ class _DynamicAggregateBase(AggregateFunction[DynamicState]):
232
232
  t = t.filter(null_mask)
233
233
  slices.append(t)
234
234
  if not slices:
235
- return pa.table({c: pa.array([], type=cols[i].type) for i, c in enumerate(col_names)})
235
+ # pa.nulls(0, ...), not pa.array([], ...): the latter has no
236
+ # builder for union or extension types.
237
+ return pa.table({c: pa.nulls(0, type=cols[i].type) for i, c in enumerate(col_names)})
236
238
  return pa.concat_tables(slices)
237
239
 
238
240
  @staticmethod
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import pyarrow as pa
8
8
 
9
- from vgi.exceptions import SchemaValidationError
9
+ from vgi.exceptions import ArgumentTypeError
10
10
 
11
11
 
12
12
  def _is_addable_type(dtype: pa.DataType) -> bool:
@@ -66,4 +66,4 @@ def _promote_for_addition(dtype: pa.DataType) -> pa.DataType:
66
66
  # accepts that values >= 5e37 will overflow at compute time.
67
67
  new_precision = min(dtype.precision + 1, 38)
68
68
  return pa.decimal128(new_precision, dtype.scale)
69
- raise SchemaValidationError(f"Unsupported numeric type for addition: {dtype}")
69
+ raise ArgumentTypeError(f"Unsupported numeric type for addition: {dtype}")
vgi/arguments.py CHANGED
@@ -19,10 +19,11 @@ import typing
19
19
  import warnings
20
20
  from collections.abc import Callable, Mapping, Sequence
21
21
  from dataclasses import dataclass
22
- from typing import TYPE_CHECKING, Any, Final, TypeVar, overload
22
+ from typing import TYPE_CHECKING, Any, ClassVar, Final, TypeVar, overload
23
23
 
24
24
  import pyarrow as pa
25
25
  from vgi_rpc import ArrowSerializableDataclass
26
+ from vgi_rpc.errors import Code
26
27
 
27
28
  if TYPE_CHECKING:
28
29
  from pyarrow import Scalar
@@ -638,6 +639,7 @@ class ArgumentValidationError(ValueError):
638
639
  valid_range: Human-readable description of valid values.
639
640
  default: Default value (if any) that could be used instead.
640
641
  choices: Valid choices, if the argument is constrained to a set.
642
+ error_code: ``INVALID_ARGUMENT`` -- the caller's input is wrong, not the worker.
641
643
 
642
644
  """
643
645
 
@@ -649,6 +651,7 @@ class ArgumentValidationError(ValueError):
649
651
  valid_range: str | None
650
652
  default: Any
651
653
  choices: Sequence[Any] | None
654
+ error_code: ClassVar[Code] = Code.INVALID_ARGUMENT
652
655
 
653
656
  def __init__(
654
657
  self,
@@ -1429,10 +1432,12 @@ class Arg[ArgT]:
1429
1432
  field_type: The Arrow type of the column to validate.
1430
1433
 
1431
1434
  Raises:
1432
- [`SchemaValidationError`][]: If the type bound is not satisfied.
1435
+ [`ArgumentTypeError`][vgi.exceptions.ArgumentTypeError]: If the type bound is
1436
+ not satisfied (a [`SchemaValidationError`][vgi.exceptions.SchemaValidationError]
1437
+ coded ``INVALID_ARGUMENT``).
1433
1438
 
1434
1439
  """
1435
- from vgi.exceptions import SchemaValidationError
1440
+ from vgi.exceptions import ArgumentTypeError
1436
1441
 
1437
1442
  if self.type_bound is None:
1438
1443
  return
@@ -1446,7 +1451,7 @@ class Arg[ArgT]:
1446
1451
  # OR logic: at least one predicate must pass
1447
1452
  if not any(predicate(field_type) for predicate in predicates):
1448
1453
  predicate_names = [getattr(p, "__name__", str(p)) for p in predicates]
1449
- raise SchemaValidationError(
1454
+ raise ArgumentTypeError(
1450
1455
  self.format_error(f"column type {field_type} does not match any of: {', '.join(predicate_names)}")
1451
1456
  )
1452
1457
 
@@ -23,6 +23,7 @@ from dataclasses import dataclass
23
23
  from typing import Any, ClassVar
24
24
 
25
25
  import pyarrow as pa
26
+ from vgi_rpc.errors import Code
26
27
 
27
28
  from vgi.catalog._descriptor_spec import _DescriptorBase, _extract_specs, _SpecBase
28
29
 
@@ -156,10 +157,12 @@ class MissingAttachOptionsError(ValueError):
156
157
 
157
158
  Attributes:
158
159
  missing: Names of the required options that were not supplied.
160
+ error_code: ``INVALID_ARGUMENT``.
159
161
 
160
162
  """
161
163
 
162
164
  missing: list[str]
165
+ error_code: ClassVar[Code] = Code.INVALID_ARGUMENT
163
166
 
164
167
  def __init__(self, catalog_name: str, missing: list[str]) -> None:
165
168
  """Build the error from the catalog name and the missing option names."""
@@ -38,7 +38,7 @@ from vgi_rpc import ArrowSerializableDataclass, ArrowType
38
38
  from vgi_rpc.utils import deserialize_record_batch, serialize_record_batch_bytes
39
39
 
40
40
  from vgi.arguments import Arguments, SecretLookupEntry
41
- from vgi.exceptions import CatalogReadOnlyError
41
+ from vgi.exceptions import CatalogReadOnlyError, InvalidArgumentError, NotFoundError, UnsupportedOperationError
42
42
  from vgi.metadata import (
43
43
  ArgumentMonotonicity,
44
44
  DistinctDependence,
@@ -100,7 +100,7 @@ __all__ = [
100
100
  def _validate_at_params(at_unit: str | None, at_value: str | None) -> None:
101
101
  """Validate that at_unit and at_value are both provided or both absent."""
102
102
  if bool(at_unit) != bool(at_value):
103
- raise ValueError("at_unit and at_value must both be provided or both be None")
103
+ raise InvalidArgumentError("at_unit and at_value must both be provided or both be None")
104
104
 
105
105
 
106
106
  @dataclass(frozen=True)
@@ -1810,12 +1810,12 @@ class CatalogInterface(ABC):
1810
1810
  If on_conflict is ERROR and the catalog already exists, raise an error.
1811
1811
 
1812
1812
  """
1813
- raise NotImplementedError("Catalog create not implemented.")
1813
+ raise UnsupportedOperationError("Catalog create not implemented.")
1814
1814
 
1815
1815
  # Drop a catalog
1816
1816
  def catalog_drop(self, *, name: str) -> None:
1817
1817
  """Drop the catalog with the given name."""
1818
- raise NotImplementedError("Catalog drop not implemented.")
1818
+ raise UnsupportedOperationError("Catalog drop not implemented.")
1819
1819
 
1820
1820
  # Transactions are initiated and driven by DuckDB it is rare for CatalogInterface
1821
1821
  # implementors to implement them, but I want to support them.
@@ -1830,7 +1830,7 @@ class CatalogInterface(ABC):
1830
1830
 
1831
1831
  If the implementation does not support transactions, it can return None.
1832
1832
  """
1833
- raise NotImplementedError("Catalog transactions not implemented.")
1833
+ raise UnsupportedOperationError("Catalog transactions not implemented.")
1834
1834
 
1835
1835
  def catalog_transaction_commit(
1836
1836
  self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData
@@ -1839,7 +1839,7 @@ class CatalogInterface(ABC):
1839
1839
 
1840
1840
  If the transaction cannot be committed, an exception should be raised.
1841
1841
  """
1842
- raise NotImplementedError("Catalog transactions not implemented.")
1842
+ raise UnsupportedOperationError("Catalog transactions not implemented.")
1843
1843
 
1844
1844
  def catalog_transaction_rollback(
1845
1845
  self, *, attach_opaque_data: AttachOpaqueData, transaction_opaque_data: TransactionOpaqueData
@@ -1848,7 +1848,7 @@ class CatalogInterface(ABC):
1848
1848
 
1849
1849
  If the transaction cannot be rolled back, an exception should be raised.
1850
1850
  """
1851
- raise NotImplementedError("Catalog transactions not implemented.")
1851
+ raise UnsupportedOperationError("Catalog transactions not implemented.")
1852
1852
 
1853
1853
  @abstractmethod
1854
1854
  def catalog_attach(
@@ -1936,7 +1936,7 @@ class CatalogInterface(ABC):
1936
1936
  tags: dict[str, str],
1937
1937
  ) -> None:
1938
1938
  """Create a new schema with the given path, comment, and tags."""
1939
- raise NotImplementedError("Schema create not implemented.")
1939
+ raise UnsupportedOperationError("Schema create not implemented.")
1940
1940
 
1941
1941
  def schema_drop(
1942
1942
  self,
@@ -1948,7 +1948,7 @@ class CatalogInterface(ABC):
1948
1948
  cascade: bool,
1949
1949
  ) -> None:
1950
1950
  """Drop the schema with the given path."""
1951
- raise NotImplementedError("Schema drop not implemented.")
1951
+ raise UnsupportedOperationError("Schema drop not implemented.")
1952
1952
 
1953
1953
  @overload
1954
1954
  def schema_contents(
@@ -2034,7 +2034,7 @@ class CatalogInterface(ABC):
2034
2034
  depending on the type parameter.
2035
2035
 
2036
2036
  """
2037
- raise NotImplementedError("Schema contents not implemented.")
2037
+ raise UnsupportedOperationError("Schema contents not implemented.")
2038
2038
 
2039
2039
  def catalog_contents(
2040
2040
  self,
@@ -2159,7 +2159,7 @@ class CatalogInterface(ABC):
2159
2159
 
2160
2160
  Comments and tags are not supported on table creation.
2161
2161
  """
2162
- raise NotImplementedError("Table create not implemented.")
2162
+ raise UnsupportedOperationError("Table create not implemented.")
2163
2163
 
2164
2164
  def table_drop(
2165
2165
  self,
@@ -2172,7 +2172,7 @@ class CatalogInterface(ABC):
2172
2172
  cascade: bool = False,
2173
2173
  ) -> None:
2174
2174
  """Drop the table with the given name."""
2175
- raise NotImplementedError("Table drop not implemented.")
2175
+ raise UnsupportedOperationError("Table drop not implemented.")
2176
2176
 
2177
2177
  def table_comment_set(
2178
2178
  self,
@@ -2185,7 +2185,7 @@ class CatalogInterface(ABC):
2185
2185
  ignore_not_found: bool,
2186
2186
  ) -> None:
2187
2187
  """Set the comment for the table with the given name."""
2188
- raise NotImplementedError("Table comment set not implemented.")
2188
+ raise UnsupportedOperationError("Table comment set not implemented.")
2189
2189
 
2190
2190
  def table_column_comment_set(
2191
2191
  self,
@@ -2199,7 +2199,7 @@ class CatalogInterface(ABC):
2199
2199
  ignore_not_found: bool,
2200
2200
  ) -> None:
2201
2201
  """Set the comment for a column in the table."""
2202
- raise NotImplementedError("Table column comment set not implemented.")
2202
+ raise UnsupportedOperationError("Table column comment set not implemented.")
2203
2203
 
2204
2204
  def table_rename(
2205
2205
  self,
@@ -2212,7 +2212,7 @@ class CatalogInterface(ABC):
2212
2212
  ignore_not_found: bool,
2213
2213
  ) -> None:
2214
2214
  """Rename the table with the given name to the new name."""
2215
- raise NotImplementedError("Table rename not implemented.")
2215
+ raise UnsupportedOperationError("Table rename not implemented.")
2216
2216
 
2217
2217
  def table_column_add(
2218
2218
  self,
@@ -2228,7 +2228,7 @@ class CatalogInterface(ABC):
2228
2228
  if_column_not_exists: bool,
2229
2229
  ) -> None:
2230
2230
  """Add a column to the table with the given name."""
2231
- raise NotImplementedError("Table column add not implemented.")
2231
+ raise UnsupportedOperationError("Table column add not implemented.")
2232
2232
 
2233
2233
  def table_column_drop(
2234
2234
  self,
@@ -2243,7 +2243,7 @@ class CatalogInterface(ABC):
2243
2243
  cascade: bool,
2244
2244
  ) -> None:
2245
2245
  """Drop the column from the table with the given name."""
2246
- raise NotImplementedError("Table column drop not implemented.")
2246
+ raise UnsupportedOperationError("Table column drop not implemented.")
2247
2247
 
2248
2248
  def table_column_rename(
2249
2249
  self,
@@ -2257,7 +2257,7 @@ class CatalogInterface(ABC):
2257
2257
  ignore_not_found: bool,
2258
2258
  ) -> None:
2259
2259
  """Rename the column in the table with the given name."""
2260
- raise NotImplementedError("Table column rename not implemented.")
2260
+ raise UnsupportedOperationError("Table column rename not implemented.")
2261
2261
 
2262
2262
  def table_column_default_set(
2263
2263
  self,
@@ -2271,7 +2271,7 @@ class CatalogInterface(ABC):
2271
2271
  ignore_not_found: bool,
2272
2272
  ) -> None:
2273
2273
  """Set the default expression for the column."""
2274
- raise NotImplementedError("Table column default set not implemented.")
2274
+ raise UnsupportedOperationError("Table column default set not implemented.")
2275
2275
 
2276
2276
  def table_column_default_drop(
2277
2277
  self,
@@ -2284,7 +2284,7 @@ class CatalogInterface(ABC):
2284
2284
  ignore_not_found: bool,
2285
2285
  ) -> None:
2286
2286
  """Drop the default expression for the column."""
2287
- raise NotImplementedError("Table column default drop not implemented.")
2287
+ raise UnsupportedOperationError("Table column default drop not implemented.")
2288
2288
 
2289
2289
  def table_column_type_change(
2290
2290
  self,
@@ -2303,7 +2303,7 @@ class CatalogInterface(ABC):
2303
2303
 
2304
2304
  The name of the column to change is taken from the field in the provided schema.
2305
2305
  """
2306
- raise NotImplementedError("Table column type change not implemented.")
2306
+ raise UnsupportedOperationError("Table column type change not implemented.")
2307
2307
 
2308
2308
  def table_not_null_drop(
2309
2309
  self,
@@ -2316,7 +2316,7 @@ class CatalogInterface(ABC):
2316
2316
  ignore_not_found: bool,
2317
2317
  ) -> None:
2318
2318
  """Drop the NOT NULL constraint from the column."""
2319
- raise NotImplementedError("Table NOT NULL drop not implemented.")
2319
+ raise UnsupportedOperationError("Table NOT NULL drop not implemented.")
2320
2320
 
2321
2321
  def table_not_null_set(
2322
2322
  self,
@@ -2329,7 +2329,7 @@ class CatalogInterface(ABC):
2329
2329
  ignore_not_found: bool,
2330
2330
  ) -> None:
2331
2331
  """Set the NOT NULL constraint on the column."""
2332
- raise NotImplementedError("Table NOT NULL set not implemented.")
2332
+ raise UnsupportedOperationError("Table NOT NULL set not implemented.")
2333
2333
 
2334
2334
  def table_scan_function_get(
2335
2335
  self,
@@ -2347,7 +2347,7 @@ class CatalogInterface(ABC):
2347
2347
  Returns information about the VGI table function to call when scanning
2348
2348
  this table. The at_unit and at_value support time travel queries.
2349
2349
  """
2350
- raise NotImplementedError("Table scan function get not implemented.")
2350
+ raise UnsupportedOperationError("Table scan function get not implemented.")
2351
2351
 
2352
2352
  def table_scan_branches_get(
2353
2353
  self,
@@ -2466,7 +2466,7 @@ class CatalogInterface(ABC):
2466
2466
  For single-branch tables this is ``None`` (or unset for legacy
2467
2467
  overrides).
2468
2468
  """
2469
- raise NotImplementedError("Table insert not supported.")
2469
+ raise UnsupportedOperationError("Table insert not supported.")
2470
2470
 
2471
2471
  def table_update_function_get(
2472
2472
  self,
@@ -2482,7 +2482,7 @@ class CatalogInterface(ABC):
2482
2482
  to call for updating rows in this table. Input batches will include a
2483
2483
  rowid column plus the columns being updated.
2484
2484
  """
2485
- raise NotImplementedError("Table update not supported.")
2485
+ raise UnsupportedOperationError("Table update not supported.")
2486
2486
 
2487
2487
  def table_delete_function_get(
2488
2488
  self,
@@ -2498,7 +2498,7 @@ class CatalogInterface(ABC):
2498
2498
  to call for deleting rows from this table. Input batches will contain
2499
2499
  a rowid column identifying the rows to delete.
2500
2500
  """
2501
- raise NotImplementedError("Table delete not supported.")
2501
+ raise UnsupportedOperationError("Table delete not supported.")
2502
2502
 
2503
2503
  def view_create(
2504
2504
  self,
@@ -2511,7 +2511,7 @@ class CatalogInterface(ABC):
2511
2511
  on_conflict: OnConflict,
2512
2512
  ) -> None:
2513
2513
  """Create a new view with the given definition."""
2514
- raise NotImplementedError("View create not implemented.")
2514
+ raise UnsupportedOperationError("View create not implemented.")
2515
2515
 
2516
2516
  def view_drop(
2517
2517
  self,
@@ -2524,7 +2524,7 @@ class CatalogInterface(ABC):
2524
2524
  cascade: bool = False,
2525
2525
  ) -> None:
2526
2526
  """Drop the view with the given name."""
2527
- raise NotImplementedError("View drop not implemented.")
2527
+ raise UnsupportedOperationError("View drop not implemented.")
2528
2528
 
2529
2529
  def view_rename(
2530
2530
  self,
@@ -2537,7 +2537,7 @@ class CatalogInterface(ABC):
2537
2537
  ignore_not_found: bool,
2538
2538
  ) -> None:
2539
2539
  """Rename the view to the new name."""
2540
- raise NotImplementedError("View rename not implemented.")
2540
+ raise UnsupportedOperationError("View rename not implemented.")
2541
2541
 
2542
2542
  @abstractmethod
2543
2543
  def view_get(
@@ -2564,7 +2564,7 @@ class CatalogInterface(ABC):
2564
2564
  ignore_not_found: bool,
2565
2565
  ) -> None:
2566
2566
  """Set the comment for the view with the given name."""
2567
- raise NotImplementedError("View comment set not implemented.")
2567
+ raise UnsupportedOperationError("View comment set not implemented.")
2568
2568
 
2569
2569
  # ---- Macros ----
2570
2570
 
@@ -2613,7 +2613,7 @@ class CatalogInterface(ABC):
2613
2613
  descriptions via the ``vgi_doc`` field metadata key. ``None`` when
2614
2614
  no per-parameter docs are supplied.
2615
2615
  """
2616
- raise NotImplementedError("Macro create not implemented.")
2616
+ raise UnsupportedOperationError("Macro create not implemented.")
2617
2617
 
2618
2618
  def macro_drop(
2619
2619
  self,
@@ -2625,7 +2625,7 @@ class CatalogInterface(ABC):
2625
2625
  ignore_not_found: bool,
2626
2626
  ) -> None:
2627
2627
  """Drop the macro with the given name."""
2628
- raise NotImplementedError("Macro drop not implemented.")
2628
+ raise UnsupportedOperationError("Macro drop not implemented.")
2629
2629
 
2630
2630
  # ---- Indexes ----
2631
2631
 
@@ -2659,7 +2659,7 @@ class CatalogInterface(ABC):
2659
2659
  options: dict[str, str] | None = None,
2660
2660
  ) -> None:
2661
2661
  """Create a new index on the specified table."""
2662
- raise NotImplementedError("Index create not implemented.")
2662
+ raise UnsupportedOperationError("Index create not implemented.")
2663
2663
 
2664
2664
  def index_drop(
2665
2665
  self,
@@ -2672,7 +2672,7 @@ class CatalogInterface(ABC):
2672
2672
  cascade: bool = False,
2673
2673
  ) -> None:
2674
2674
  """Drop the index with the given name."""
2675
- raise NotImplementedError("Index drop not implemented.")
2675
+ raise UnsupportedOperationError("Index drop not implemented.")
2676
2676
 
2677
2677
  def copy_from_formats(
2678
2678
  self,
@@ -3015,7 +3015,7 @@ class ReadOnlyCatalogInterface(CatalogInterface):
3015
3015
  del data_version_spec, implementation_version, ctx
3016
3016
  effective_name = self._effective_catalog_name
3017
3017
  if name != effective_name:
3018
- raise ValueError(f"Unknown catalog: {name!r}. Available: {effective_name}")
3018
+ raise NotFoundError(f"Unknown catalog: {name!r}. Available: {effective_name}")
3019
3019
  validate_required_attach_options(effective_name, self.attach_option_specs, options)
3020
3020
 
3021
3021
  # Serialize settings and secret types for the attach result
@@ -3276,7 +3276,7 @@ class ReadOnlyCatalogInterface(CatalogInterface):
3276
3276
 
3277
3277
  table = self._table_registry.get((schema_path_key(schema_path), name.lower()))
3278
3278
  if table is None:
3279
- raise NotImplementedError(f"Table {schema_path_display(schema_path)}::{name!r} not found in catalog.")
3279
+ raise NotFoundError(f"Table {schema_path_display(schema_path)}::{name!r} not found in catalog.")
3280
3280
 
3281
3281
  write_func = getattr(table, attr_name, None)
3282
3282
  if write_func is None:
vgi/client/aggregate.py CHANGED
@@ -50,6 +50,7 @@ from typing import Any
50
50
 
51
51
  import pyarrow as pa
52
52
  from vgi_rpc.rpc import RpcError
53
+ from vgi_rpc.utils import empty_batch
53
54
 
54
55
  from vgi.aggregate_function import GROUP_COLUMN_NAME
55
56
  from vgi.arguments import Arguments
@@ -783,12 +784,9 @@ class AggregateClientMixin:
783
784
  batch = next(batches, None)
784
785
 
785
786
  if not group_keys:
786
- results = [
787
- pa.RecordBatch.from_arrays(
788
- [pa.array([], type=field.type) for field in session.output_schema],
789
- schema=session.output_schema,
790
- )
791
- ]
787
+ # empty_batch, not pa.array([], type=...): that has no builder
788
+ # for union or extension types.
789
+ results = [empty_batch(session.output_schema)]
792
790
  else:
793
791
  results = [
794
792
  session.finalize(range(start, min(start + finalize_chunk_size, len(group_keys))))
vgi/exceptions.py CHANGED
@@ -14,15 +14,21 @@ from __future__ import annotations
14
14
 
15
15
  from typing import TYPE_CHECKING, ClassVar
16
16
 
17
+ from vgi_rpc.errors import Code
18
+
17
19
  if TYPE_CHECKING:
18
20
  import pyarrow as pa
19
21
 
20
22
  __all__ = [
23
+ "ArgumentTypeError",
21
24
  "BindStateNotFoundError",
22
25
  "CatalogReadOnlyError",
23
26
  "ExecutionIdentifierError",
27
+ "InvalidArgumentError",
28
+ "NotFoundError",
24
29
  "OpaqueDataNotRecognizedError",
25
30
  "SchemaValidationError",
31
+ "UnsupportedOperationError",
26
32
  ]
27
33
 
28
34
 
@@ -55,8 +61,14 @@ class CatalogReadOnlyError(Exception):
55
61
  - table_get(), view_get() - get table/view info
56
62
  - table_scan_function_get() - get scan function for tables
57
63
 
64
+ Also raised when a write targets a table that has no write function.
65
+ ``FAILED_PRECONDITION``: the request is well-formed, the catalog is just
66
+ not in a state that permits it.
67
+
58
68
  """
59
69
 
70
+ error_code: ClassVar[Code] = Code.FAILED_PRECONDITION
71
+
60
72
 
61
73
  class OpaqueDataNotRecognizedError(ValueError):
62
74
  """An opaque value (``attach_opaque_data`` / ``transaction_opaque_data``) failed to open.
@@ -72,7 +84,7 @@ class OpaqueDataNotRecognizedError(ValueError):
72
84
  working.
73
85
  """
74
86
 
75
- error_code: ClassVar[str] = "INVALID_ARGUMENT"
87
+ error_code: ClassVar[Code] = Code.INVALID_ARGUMENT
76
88
  error_kind: ClassVar[str] = "opaque_data_not_recognized"
77
89
 
78
90
  def __init__(self, field: str) -> None:
@@ -230,3 +242,52 @@ class SchemaValidationError(Exception):
230
242
  lines.append(f" {field.name}: {field.type}{nullable}")
231
243
 
232
244
  return "\n".join(lines)
245
+
246
+
247
+ class ArgumentTypeError(SchemaValidationError):
248
+ """An argument or input column has a type the function does not accept.
249
+
250
+ Raised when a value fails an ``Arg[AnyArrow]`` ``type_bound`` -- e.g.
251
+ ``double('abc')`` against a numeric-only bound. Unlike its base class,
252
+ which reports a worker's *own* output violating its declared schema (a
253
+ bug, sent as ``UNKNOWN``), this is the caller's input being wrong, so it is
254
+ ``INVALID_ARGUMENT``. Subclasses [`SchemaValidationError`][] so existing
255
+ handlers keep working.
256
+ """
257
+
258
+ error_code: ClassVar[Code] = Code.INVALID_ARGUMENT
259
+
260
+
261
+ class InvalidArgumentError(ValueError):
262
+ """A request's arguments cannot be accepted (``INVALID_ARGUMENT``).
263
+
264
+ For argument problems that are not a single argument's constraint (those
265
+ raise [`ArgumentValidationError`][vgi.arguments.ArgumentValidationError]):
266
+ no overload matching the supplied argument types, an ambiguous call, or a
267
+ call shape the function does not take. Subclasses ``ValueError`` so
268
+ existing handlers keep working.
269
+ """
270
+
271
+ error_code: ClassVar[Code] = Code.INVALID_ARGUMENT
272
+
273
+
274
+ class NotFoundError(ValueError):
275
+ """A named function, table, schema, or catalog does not exist (``NOT_FOUND``).
276
+
277
+ Subclasses ``ValueError`` so existing handlers keep working.
278
+ """
279
+
280
+ error_code: ClassVar[Code] = Code.NOT_FOUND
281
+
282
+
283
+ class UnsupportedOperationError(NotImplementedError):
284
+ """An operation the SDK or catalog explicitly does not support (``UNIMPLEMENTED``).
285
+
286
+ Raised by the [`CatalogInterface`][vgi.catalog.CatalogInterface] defaults
287
+ for DDL/DML a catalog has not implemented, and by framework paths that are
288
+ not supported yet. Subclasses ``NotImplementedError`` so existing handlers
289
+ -- including the ones that treat an unimplemented optional hook as
290
+ "absent" -- keep working.
291
+ """
292
+
293
+ error_code: ClassVar[Code] = Code.UNIMPLEMENTED
vgi/filter_v2.py CHANGED
@@ -16,6 +16,7 @@ from typing import Any
16
16
  from typing import Literal as TypingLiteral
17
17
 
18
18
  import pyarrow as pa
19
+ from vgi_rpc.utils import empty_batch
19
20
 
20
21
  FILTER_ENCODING = "vgi.filters.v2"
21
22
  FILTER_VERSION = "2"
@@ -765,10 +766,9 @@ class _Parser:
765
766
  and not isinstance(expression, RuntimeFilter)
766
767
  and not _REPLAYING_ACCEPTED_STATE.get()
767
768
  ):
768
- empty = pa.RecordBatch.from_arrays(
769
- [pa.array([], type=field.type) for field in self.output_schema],
770
- schema=self.output_schema,
771
- )
769
+ # empty_batch, not pa.array([], type=...): that has no builder
770
+ # for union or extension types.
771
+ empty = empty_batch(self.output_schema)
772
772
  try:
773
773
  _evaluate_expression(expression, empty, self.context)
774
774
  except Exception as exc:
vgi/function.py CHANGED
@@ -147,6 +147,20 @@ class Function(ABC, MetadataMixin):
147
147
  # Cache for resolved metadata
148
148
  _metadata_cache: ClassVar[ResolvedMetadata | None] = None
149
149
 
150
+ @classmethod
151
+ def __subclasshook__(cls, subclass: type) -> bool:
152
+ """Answer ``issubclass`` from the MRO alone.
153
+
154
+ Without this, a *failed* ``issubclass(X, SomeFunctionBase)`` makes
155
+ ``ABCMeta`` call ``issubclass(X, S)`` for every subclass ``S`` of the
156
+ base, looking for a virtual subclass. A worker that generates one class
157
+ per function has thousands of them, and the framework asks this about
158
+ every class, so building a catalog went quadratic in its function count.
159
+ Function classes are real subclasses, never ``ABC.register``-ed ones,
160
+ so the MRO is the whole answer.
161
+ """
162
+ return cls in getattr(subclass, "__mro__", ())
163
+
150
164
  def __init_subclass__(cls, **kwargs: Any) -> None:
151
165
  """Extract [`Arg`][] descriptors from Annotated type hints.
152
166