|
| 1 | +# Design: DuckDB Settings/Pragmas Access for VGI Functions |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +VGI functions need access to DuckDB settings/pragmas to: |
| 6 | +1. Determine output schema during bind phase (e.g., timezone affects datetime output) |
| 7 | +2. Influence processing behavior (e.g., thread limits, memory settings) |
| 8 | +3. Maintain consistency with DuckDB's execution environment |
| 9 | + |
| 10 | +This design adds support for functions to declare required settings and access their values. |
| 11 | + |
| 12 | +## Design Decisions |
| 13 | + |
| 14 | +### 1. Settings Declaration in Meta Class |
| 15 | + |
| 16 | +Functions declare required settings in their `Meta` class: |
| 17 | + |
| 18 | +```python |
| 19 | +class TimezoneAwareFunction(TableInOutFunction): |
| 20 | + class Meta: |
| 21 | + required_settings = ["TimeZone", "Calendar"] # List of DuckDB setting names |
| 22 | + max_workers = 1 |
| 23 | + |
| 24 | + @property |
| 25 | + def output_schema(self) -> pa.Schema: |
| 26 | + # Settings available during bind - can influence output schema |
| 27 | + tz = self.get_setting("TimeZone", "UTC") |
| 28 | + return pa.schema([ |
| 29 | + ("timestamp", pa.timestamp("us", tz=tz)), |
| 30 | + ]) |
| 31 | +``` |
| 32 | + |
| 33 | +**Changes to `vgi/metadata.py`:** |
| 34 | +- Add `"required_settings"` to `_VALID_META_ATTRIBUTES` |
| 35 | +- Add `required_settings: list[str] = field(default_factory=list)` to `ResolvedMetadata` |
| 36 | +- Update `resolve_metadata()` to extract the field |
| 37 | +- Update Arrow serialization schema and methods |
| 38 | + |
| 39 | +### 2. Settings in Invocation |
| 40 | + |
| 41 | +Settings are passed as a dict in the `Invocation`: |
| 42 | + |
| 43 | +```python |
| 44 | +@dataclass(frozen=True, slots=True) |
| 45 | +class Invocation: |
| 46 | + # ... existing fields ... |
| 47 | + duckdb_settings: dict[str, str] | None = None # New field |
| 48 | +``` |
| 49 | + |
| 50 | +**Changes to `vgi/invocation.py`:** |
| 51 | +- Add `duckdb_settings: dict[str, str] | None = None` field |
| 52 | +- Serialize as `pa.map_(pa.utf8(), pa.utf8())` type in Arrow IPC |
| 53 | +- Deserialize with backward compatibility (None if field missing) |
| 54 | + |
| 55 | +**Serialization Format:** |
| 56 | +```python |
| 57 | +# In serialize(): |
| 58 | +pa.field("duckdb_settings", pa.map_(pa.utf8(), pa.utf8()), nullable=True) |
| 59 | + |
| 60 | +# Value encoding: |
| 61 | +"duckdb_settings": ( |
| 62 | + list(self.duckdb_settings.items()) if self.duckdb_settings else None |
| 63 | +) |
| 64 | +``` |
| 65 | + |
| 66 | +### 3. Settings Accessor API |
| 67 | + |
| 68 | +Functions access settings via the `Function` base class: |
| 69 | + |
| 70 | +```python |
| 71 | +class Function: |
| 72 | + @property |
| 73 | + def settings(self) -> dict[str, str]: |
| 74 | + """All DuckDB settings passed to this function.""" |
| 75 | + return dict(self.invocation.duckdb_settings or {}) |
| 76 | + |
| 77 | + def get_setting(self, name: str, default: str | None = None) -> str | None: |
| 78 | + """Get a specific DuckDB setting value. |
| 79 | +
|
| 80 | + Args: |
| 81 | + name: DuckDB setting name (e.g., "TimeZone", "threads") |
| 82 | + default: Value to return if setting not present |
| 83 | +
|
| 84 | + Returns: |
| 85 | + Setting value or default |
| 86 | + """ |
| 87 | + if self.invocation.duckdb_settings is None: |
| 88 | + return default |
| 89 | + return self.invocation.duckdb_settings.get(name, default) |
| 90 | +``` |
| 91 | + |
| 92 | +**Changes to `vgi/function.py`:** |
| 93 | +- Add `settings` property |
| 94 | +- Add `get_setting()` method |
| 95 | + |
| 96 | +### 4. Worker Validation |
| 97 | + |
| 98 | +The worker validates required settings during bind: |
| 99 | + |
| 100 | +```python |
| 101 | +# In Worker._validate_required_settings(): |
| 102 | +def _validate_required_settings( |
| 103 | + self, |
| 104 | + func_cls: type[Function], |
| 105 | + invocation: Invocation |
| 106 | +) -> None: |
| 107 | + """Validate that all required settings are present.""" |
| 108 | + meta = func_cls.get_metadata() |
| 109 | + required = set(meta.required_settings) |
| 110 | + |
| 111 | + if not required: |
| 112 | + return # No settings required |
| 113 | + |
| 114 | + provided = set(invocation.duckdb_settings.keys()) if invocation.duckdb_settings else set() |
| 115 | + missing = required - provided |
| 116 | + |
| 117 | + if missing: |
| 118 | + raise ValueError( |
| 119 | + f"Function '{meta.name}' requires settings {sorted(missing)} " |
| 120 | + f"but they were not provided. Provided: {sorted(provided)}" |
| 121 | + ) |
| 122 | +``` |
| 123 | + |
| 124 | +**Changes to `vgi/worker.py`:** |
| 125 | +- Add `_validate_required_settings()` method |
| 126 | +- Call it after function class resolution, before instantiation |
| 127 | + |
| 128 | +### 5. Client Support |
| 129 | + |
| 130 | +The client passes settings when creating invocations: |
| 131 | + |
| 132 | +```python |
| 133 | +# In Client methods: |
| 134 | +def invoke( |
| 135 | + self, |
| 136 | + function_name: str, |
| 137 | + ..., |
| 138 | + duckdb_settings: dict[str, str] | None = None, # New parameter |
| 139 | +) -> ...: |
| 140 | +``` |
| 141 | + |
| 142 | +**Changes to `vgi/client/client.py`:** |
| 143 | +- Add `duckdb_settings` parameter to `_initialize_stream_common()` and related methods |
| 144 | +- Include in `Invocation` creation |
| 145 | + |
| 146 | +## Protocol Flow |
| 147 | + |
| 148 | +``` |
| 149 | +Client Worker |
| 150 | + │ │ |
| 151 | + │ Invocation │ |
| 152 | + │ ├─ function_name: "timezone_func" │ |
| 153 | + │ ├─ arguments: {...} │ |
| 154 | + │ ├─ input_schema: {...} │ |
| 155 | + │ └─ duckdb_settings: { │ |
| 156 | + │ "TimeZone": "America/New_York", │ |
| 157 | + │ "threads": "4" │ |
| 158 | + │ } │ |
| 159 | + │─────────────────────────────────────────▶│ |
| 160 | + │ │ 1. Lookup function class |
| 161 | + │ │ 2. Validate required_settings |
| 162 | + │ │ 3. Instantiate function |
| 163 | + │ │ (settings available via self.settings) |
| 164 | + │ │ 4. Get output_schema (may use settings) |
| 165 | + │◀─────────────────────────────────────────│ |
| 166 | + │ OutputSpec │ |
| 167 | + │ └─ output_schema: {...} │ |
| 168 | + │ │ |
| 169 | +``` |
| 170 | + |
| 171 | +## Example Function |
| 172 | + |
| 173 | +```python |
| 174 | +from vgi import TableInOutFunction, Arg |
| 175 | +import pyarrow as pa |
| 176 | + |
| 177 | +class DebugOutputFunction(TableInOutFunction): |
| 178 | + """Function that optionally includes debug columns based on setting.""" |
| 179 | + |
| 180 | + class Meta: |
| 181 | + required_settings = ["vgi_debug_mode"] |
| 182 | + max_workers = 1 |
| 183 | + |
| 184 | + @property |
| 185 | + def output_schema(self) -> pa.Schema: |
| 186 | + # Base schema from input |
| 187 | + fields = list(self.input_schema) |
| 188 | + |
| 189 | + # Add debug column if debug mode enabled |
| 190 | + if self.get_setting("vgi_debug_mode") == "true": |
| 191 | + fields.append(pa.field("_debug_worker_pid", pa.int32())) |
| 192 | + |
| 193 | + return pa.schema(fields) |
| 194 | + |
| 195 | + def transform(self, batch: pa.RecordBatch) -> pa.RecordBatch: |
| 196 | + if self.get_setting("vgi_debug_mode") == "true": |
| 197 | + # Add debug column |
| 198 | + import os |
| 199 | + debug_col = pa.array([os.getpid()] * batch.num_rows, type=pa.int32()) |
| 200 | + return pa.RecordBatch.from_arrays( |
| 201 | + list(batch.columns) + [debug_col], |
| 202 | + schema=self.output_schema |
| 203 | + ) |
| 204 | + return batch |
| 205 | +``` |
| 206 | + |
| 207 | +## Files to Modify |
| 208 | + |
| 209 | +| File | Changes | |
| 210 | +|------|---------| |
| 211 | +| `vgi/metadata.py` | Add `required_settings` to Meta, ResolvedMetadata, Arrow schema | |
| 212 | +| `vgi/invocation.py` | Add `duckdb_settings` field, serialization | |
| 213 | +| `vgi/function.py` | Add `settings` property, `get_setting()` method | |
| 214 | +| `vgi/worker.py` | Add settings validation during bind | |
| 215 | +| `vgi/client/client.py` | Add `duckdb_settings` parameter | |
| 216 | +| `docs/protocol.md` | Document settings in protocol | |
| 217 | +| `docs/metadata.md` | Document `required_settings` | |
| 218 | +| `CLAUDE.md` | Add settings usage example | |
| 219 | + |
| 220 | +## Test Cases |
| 221 | + |
| 222 | +1. **Serialization roundtrip**: Invocation with settings serializes/deserializes correctly |
| 223 | +2. **Empty settings**: Function with no required_settings works without settings |
| 224 | +3. **Required settings present**: Function receives and can access settings |
| 225 | +4. **Missing required settings**: Worker rejects with clear error |
| 226 | +5. **Settings affect output schema**: Verify bind returns different schema based on setting |
| 227 | +6. **Settings affect processing**: Verify transform behavior changes based on setting |
| 228 | +7. **Backward compatibility**: Old clients without settings field work with new workers |
| 229 | + |
| 230 | +## Implementation Order |
| 231 | + |
| 232 | +1. `vgi/metadata.py` - Add required_settings to Meta |
| 233 | +2. `vgi/invocation.py` - Add duckdb_settings field |
| 234 | +3. `vgi/function.py` - Add settings accessor |
| 235 | +4. `vgi/worker.py` - Add validation |
| 236 | +5. `vgi/client/client.py` - Add settings parameter |
| 237 | +6. Example function |
| 238 | +7. Tests |
| 239 | +8. Documentation |
0 commit comments