Skip to content

Commit dd4a4ca

Browse files
Merge pull request #162 from datajoint/docs/hidden-attributes-platform-framing
docs: hidden attributes are platform-only; clarify users cannot declare them
2 parents c7eb390 + 20224ae commit dd4a4ca

1 file changed

Lines changed: 70 additions & 33 deletions

File tree

src/reference/specs/table-declaration.md

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -158,54 +158,91 @@ attribute_name [= default_value] : type [# comment]
158158

159159
### 3.4 Hidden Attributes
160160

161-
Attributes with names starting with underscore (`_`) are **hidden**:
161+
Attributes with names starting with an underscore (`_`) are **hidden**. The hidden-attribute mechanism is reserved for **platform-managed** columns — bookkeeping that DataJoint itself adds to support the data pipeline — and is intentionally not exposed for user-defined attributes. Attempting to declare an attribute name with a leading underscore raises:
162162

163-
```python
164-
definition = """
165-
session_id : int32
166-
---
167-
result : float64
168-
_job_start_time : datetime(3) # hidden
169-
_job_duration : float32 # hidden
170-
"""
163+
```text
164+
DataJointError: Attribute name in line "_hidden: bool" starts with an underscore.
165+
Names with leading underscore are reserved for platform-managed columns
166+
(e.g. _job_start_time, _singleton). Use a regular attribute name; if you
167+
need to control visibility at the call site, use proj().
171168
```
172169

173-
**Behavior:**
170+
**Platform-managed hidden attributes** are added automatically when DataJoint declares certain table types. Users do not write these in the definition; the framework injects them programmatically after parsing.
174171

175-
| Context | Hidden Attributes |
176-
|---------|-------------------|
177-
| `heading.attributes` | Excluded |
178-
| `heading._attributes` | Included |
179-
| Default table display | Excluded |
180-
| `to_dicts()` / `to_pandas()` | Excluded unless explicitly projected |
181-
| Join matching (namesakes) | Excluded |
182-
| Dict restrictions | Excluded (silently ignored) |
183-
| String restrictions | Included (passed to SQL) |
172+
| Hidden attribute | Added to | Purpose |
173+
|------------------|----------|---------|
174+
| `_job_start_time` | `Computed`, `Imported` | Wall-clock start of the populate call |
175+
| `_job_duration` | `Computed`, `Imported` | Elapsed seconds for the populate call |
176+
| `_job_version` | `Computed`, `Imported` | Library version that produced the row |
177+
| `_singleton` | Singleton tables | Implementation detail of the singleton pattern |
178+
179+
These columns are populated by DataJoint internals via raw SQL during the `populate()` lifecycle, not via `insert`/`update1`. They are filtered out of every public API surface so they don't clutter joins, fetches, or displays.
184180

185-
**Accessing hidden attributes:**
181+
**Behavior.** The filter is implemented in `Heading.attributes`, which all visible code paths consume; raw SQL strings bypass it.
182+
183+
| Context | Hidden attributes |
184+
|---------|-------------------|
185+
| `heading.attributes`, `heading.names`, `heading.primary_key` | Excluded |
186+
| `heading._attributes` (internal) | Included |
187+
| Table display / `repr` / `_repr_html_` | Excluded |
188+
| `fetch()`, `fetch1()`, `to_dicts()`, `to_pandas()` (default) | Excluded |
189+
| `fetch("_name")` / `fetch1("_name")` (explicit) | Rejected (`Attribute not found`) — use raw SQL via `conn.query(...)` |
190+
| `proj("_name")` (explicit) | Rejected (same reason) |
191+
| Natural-join namesake matching | Excluded |
192+
| Dict restriction `Table & {"_name": value}` | Silently ignored |
193+
| String restriction `Table & "_name = ..."` | Included (passes to SQL) |
194+
| `insert()`, `insert1()` | Rejected — ``KeyError("`_name` is not in the table heading")`` |
195+
| `update1()` | Rejected — ``DataJointError("Attribute `_name` not found.")`` |
196+
| `insert(..., ignore_extra_fields=True)` | Silently dropped (key not written) |
197+
| `describe()` / reverse-engineered definition | Excluded |
198+
| `unique index (..., _name)` | Allowed |
199+
200+
**Why users can't declare them.** Allowing user-defined hidden attributes would expose a feature with no public-API write path (`insert`/`update1` reject the keys; `ignore_extra_fields=True` drops them silently), no `describe()` round-trip (the regenerated definition would be missing the column), and silent filtering on dict restrictions. The cases users typically reach for hidden attributes — most commonly an index-backing derived column — are better served by a regular attribute.
201+
202+
**Inspecting platform-managed hidden columns:**
186203

187204
```python
188-
# Visible attributes only (default)
205+
# Default fetch — hidden columns excluded
189206
results = MyTable.to_dicts()
190207

191-
# Explicitly include hidden attributes
192-
results = MyTable.proj('result', '_job_start_time').to_dicts()
193-
194-
# Or with fetch1 for single row
195-
row = (MyTable & key).fetch1('result', '_job_start_time')
208+
# To inspect platform-managed hidden columns, query raw SQL.
209+
# The public API (fetch / proj) intentionally rejects them.
210+
conn = MyTable.connection
211+
rows = conn.query(
212+
f"SELECT _job_start_time, _job_duration, _job_version "
213+
f"FROM {MyTable.full_table_name}"
214+
).fetchall()
196215

197-
# String restriction works with hidden attributes
216+
# String restriction works (passes through to SQL)
198217
MyTable & "_job_start_time > '2024-01-01'"
199218

200-
# Dict restriction IGNORES hidden attributes
201-
MyTable & {'_job_start_time': some_date} # no effect
219+
# Dict restriction is silently dropped — does NOT filter
220+
MyTable & {'_job_start_time': some_date} # ⚠ ignored
202221
```
203222

204-
**Use cases:**
223+
**Use a regular attribute instead.** When you want a column that's part of the schema-level contract (backing an index, storing a derived value, etc.) but isn't featured in default displays, declare it as a regular attribute and use `proj()` at the call site if you want to omit it from a particular query result. For example, a hash column backing a unique index:
224+
225+
```python
226+
@schema
227+
class TaskParams(dj.Manual):
228+
definition = """
229+
task_id : int32
230+
---
231+
tool : varchar(32)
232+
params : json
233+
params_hash : varchar(32)
234+
unique index (tool, params_hash)
235+
"""
236+
237+
# Inserts work directly:
238+
TaskParams.insert1({'task_id': 1, 'tool': 't', 'params': {...}, 'params_hash': h})
205239

206-
- Job metadata (`_job_start_time`, `_job_duration`, `_job_version`)
207-
- Internal tracking fields
208-
- Attributes that should not participate in automatic joins
240+
# Dict restrictions work:
241+
TaskParams & {'params_hash': h}
242+
243+
# Hide from a specific result set with proj() if needed:
244+
TaskParams.proj('tool', 'params').fetch()
245+
```
209246

210247
### 3.5 Examples
211248

0 commit comments

Comments
 (0)