Skip to content

Commit af17b58

Browse files
docs: Add singleton tables documentation
- Add section 2.5 "Singleton Tables (Empty Primary Keys)" to table-declaration.md - Update university tutorial with CurrentTerm singleton example - Remove deprecated `___` separator mention (only `---` is supported) Related to datajoint/datajoint-python#1341 Closes datajoint/datajoint-python#113 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2727f63 commit af17b58

2 files changed

Lines changed: 59 additions & 77 deletions

File tree

src/reference/specs/table-declaration.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,9 @@ secondary_section
7474
---
7575
```
7676

77-
or equivalently:
78-
79-
```
80-
___
81-
```
82-
83-
- Three dashes or three underscores
84-
- Separates primary key attributes (above) from secondary attributes (below)
85-
- Required if table has secondary attributes
77+
- Three or more dashes
78+
- Separates primary key attributes (above) from dependent attributes (below)
79+
- Required if table has dependent attributes
8680

8781
### 2.4 Line Types
8882

@@ -92,6 +86,46 @@ Each non-empty, non-comment line is one of:
9286
2. **Foreign key reference**
9387
3. **Index declaration**
9488

89+
### 2.5 Singleton Tables (Empty Primary Keys)
90+
91+
A **singleton table** can hold at most one row. It is declared with no attributes in the primary key section:
92+
93+
```python
94+
@schema
95+
class Config(dj.Lookup):
96+
definition = """
97+
# Global configuration
98+
---
99+
setting1 : varchar(100)
100+
setting2 : int32
101+
"""
102+
```
103+
104+
**Behavior:**
105+
106+
| Operation | Result |
107+
|-----------|--------|
108+
| Insert | Works without specifying a key |
109+
| Second insert | Raises `DuplicateError` |
110+
| `fetch1()` | Returns the single row |
111+
| `heading.primary_key` | Returns `[]` (empty) |
112+
113+
**Use cases:**
114+
115+
- Global configuration settings
116+
- Pipeline parameters
117+
- Summary statistics
118+
- State tracking
119+
120+
**Implementation:**
121+
122+
Internally, singleton tables use a hidden `_singleton` attribute of type `bool` as the primary key. This attribute is:
123+
124+
- Automatically created and populated
125+
- Excluded from `heading.attributes`
126+
- Excluded from `fetch()` results
127+
- Excluded from join matching
128+
95129
---
96130

97131
## 3. Attribute Definition

src/tutorials/examples/university.ipynb

Lines changed: 16 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,7 @@
5353
"cell_type": "markdown",
5454
"id": "cell-schema-intro",
5555
"metadata": {},
56-
"source": [
57-
"## Schema Design\n",
58-
"\n",
59-
"Our university schema models:\n",
60-
"\n",
61-
"| Table | Purpose |\n",
62-
"|-------|--------|\n",
63-
"| `Student` | Student records with contact info |\n",
64-
"| `Department` | Academic departments |\n",
65-
"| `StudentMajor` | Student-declared majors |\n",
66-
"| `Course` | Course catalog |\n",
67-
"| `Term` | Academic terms (Spring/Summer/Fall) |\n",
68-
"| `Section` | Course offerings in specific terms |\n",
69-
"| `Enroll` | Student enrollments in sections |\n",
70-
"| `LetterGrade` | Grade scale (lookup) |\n",
71-
"| `Grade` | Assigned grades |"
72-
]
56+
"source": "## Schema Design\n\nOur university schema models:\n\n| Table | Purpose |\n|-------|--------|\n| `Student` | Student records with contact info |\n| `Department` | Academic departments |\n| `StudentMajor` | Student-declared majors |\n| `Course` | Course catalog |\n| `Term` | Academic terms (Spring/Summer/Fall) |\n| `CurrentTerm` | Active registration term (singleton) |\n| `Section` | Course offerings in specific terms |\n| `Enroll` | Student enrollments in sections |\n| `LetterGrade` | Grade scale (lookup) |\n| `Grade` | Assigned grades |"
7357
},
7458
{
7559
"cell_type": "code",
@@ -173,7 +157,7 @@
173157
},
174158
{
175159
"cell_type": "code",
176-
"execution_count": 6,
160+
"execution_count": null,
177161
"id": "cell-term",
178162
"metadata": {
179163
"execution": {
@@ -183,23 +167,8 @@
183167
"shell.execute_reply": "2026-01-17T06:50:04.029502Z"
184168
}
185169
},
186-
"outputs": [
187-
{
188-
"name": "stderr",
189-
"output_type": "stream",
190-
"text": [
191-
"[2026-01-17 00:50:04,016][WARNING]: Native type 'year' is used in attribute 'term_year'. Consider using a core DataJoint type for better portability.\n"
192-
]
193-
}
194-
],
195-
"source": [
196-
"@schema\n",
197-
"class Term(dj.Manual):\n",
198-
" definition = \"\"\"\n",
199-
" term_year : year\n",
200-
" term : enum('Spring', 'Summer', 'Fall')\n",
201-
" \"\"\""
202-
]
170+
"outputs": [],
171+
"source": "@schema\nclass Term(dj.Manual):\n definition = \"\"\"\n term_year : int16\n term : enum('Spring', 'Summer', 'Fall')\n \"\"\"\n\n\n@schema\nclass CurrentTerm(dj.Lookup):\n definition = \"\"\"\n # Active registration term (singleton)\n ---\n -> Term\n \"\"\""
203172
},
204173
{
205174
"cell_type": "code",
@@ -660,7 +629,7 @@
660629
},
661630
{
662631
"cell_type": "code",
663-
"execution_count": 16,
632+
"execution_count": null,
664633
"id": "cell-populate-terms",
665634
"metadata": {
666635
"execution": {
@@ -670,37 +639,8 @@
670639
"shell.execute_reply": "2026-01-17T06:50:05.113623Z"
671640
}
672641
},
673-
"outputs": [
674-
{
675-
"name": "stdout",
676-
"output_type": "stream",
677-
"text": [
678-
"339 sections created\n"
679-
]
680-
}
681-
],
682-
"source": [
683-
"# Academic terms 2020-2024\n",
684-
"Term.insert(\n",
685-
" {'term_year': year, 'term': term}\n",
686-
" for year in range(2020, 2025)\n",
687-
" for term in ['Spring', 'Summer', 'Fall']\n",
688-
")\n",
689-
"\n",
690-
"# Create sections for each course-term with 1-3 sections\n",
691-
"for course in Course.keys():\n",
692-
" for term in Term.keys():\n",
693-
" for sec in 'abc'[:random.randint(1, 3)]:\n",
694-
" if random.random() < 0.7: # Not every course every term\n",
695-
" Section.insert1({\n",
696-
" **course, **term,\n",
697-
" 'section': sec,\n",
698-
" 'auditorium': f\"{random.choice('ABCDEF')}\"\n",
699-
" f\"{random.randint(100, 400)}\"\n",
700-
" }, skip_duplicates=True)\n",
701-
"\n",
702-
"print(f\"{len(Section())} sections created\")"
703-
]
642+
"outputs": [],
643+
"source": "# Academic terms 2020-2024\nTerm.insert(\n {'term_year': year, 'term': term}\n for year in range(2020, 2025)\n for term in ['Spring', 'Summer', 'Fall']\n)\n\n# Set the current registration term (singleton - only one row allowed)\nCurrentTerm.insert1({'term_year': 2024, 'term': 'Fall'})\nprint(f\"Current term: {CurrentTerm.fetch1()}\")\n\n# Create sections for each course-term with 1-3 sections\nfor course in Course.keys():\n for term in Term.keys():\n for sec in 'abc'[:random.randint(1, 3)]:\n if random.random() < 0.7: # Not every course every term\n Section.insert1({\n **course, **term,\n 'section': sec,\n 'auditorium': f\"{random.choice('ABCDEF')}\"\n f\"{random.randint(100, 400)}\"\n }, skip_duplicates=True)\n\nprint(f\"{len(Section())} sections created\")"
704644
},
705645
{
706646
"cell_type": "code",
@@ -2369,6 +2309,14 @@
23692309
"all_a"
23702310
]
23712311
},
2312+
{
2313+
"cell_type": "code",
2314+
"id": "20yuzwslxgm",
2315+
"source": "# Sections available in the current term (using singleton)\nSection & CurrentTerm",
2316+
"metadata": {},
2317+
"execution_count": null,
2318+
"outputs": []
2319+
},
23722320
{
23732321
"cell_type": "markdown",
23742322
"id": "cell-proj-intro",
@@ -6548,4 +6496,4 @@
65486496
},
65496497
"nbformat": 4,
65506498
"nbformat_minor": 5
6551-
}
6499+
}

0 commit comments

Comments
 (0)