From a8ebc76077205b109ccf1b950ed5724af21c3b46 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Tue, 29 Sep 2026 14:05:57 -0500 Subject: [PATCH 1/3] docs: an empty primary key is legal, and there was never a DataJoint 1.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit definition-syntax.md asserted that a primary key must have at least one attribute, in the validation list and in the grammar itself (pk_section = attribute_line+). Singleton tables have had an empty primary key since 2.1, and table-declaration.md 2.5 documents them — so the reference page a reader consults while writing a definition contradicted the spec and never mentioned singletons at all. Corrects the grammar to attribute_line*, drops the false validation rule, and adds a section covering the declaration, the behavior, and the hidden _singleton attribute, linking to the full spec. fetch-api.md labelled its migration tables 'Old Pattern (1.x)'. No 1.x release exists: DataJoint went from 0.x to 2.x. Now pre-2.x. --- src/reference/definition-syntax.md | 28 ++++++++++++++++++++++++++-- src/reference/specs/fetch-api.md | 16 ++++++++-------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/reference/definition-syntax.md b/src/reference/definition-syntax.md index f4b67a24..62eb6e2f 100644 --- a/src/reference/definition-syntax.md +++ b/src/reference/definition-syntax.md @@ -21,7 +21,7 @@ class TableName(dj.Manual): ``` definition = [comment] pk_section "---" secondary_section -pk_section = attribute_line+ +pk_section = attribute_line* secondary_section = attribute_line* attribute_line = [foreign_key | attribute] @@ -36,6 +36,31 @@ core_type = int32 | float64 | varchar(n) | ... codec_type = "<" name ["@" [store]] ">" ``` +## A table with an empty primary key holds one row + +The primary key section may be empty. A table declared that way is a **singleton**: it holds at +most one row, which is what you want for global configuration, pipeline-wide parameters, or a +single summary. Start the definition with the `---` separator and declare only secondary +attributes. + +```python +@schema +class Config(dj.Lookup): + definition = """ + # Global configuration + --- + setting1 : varchar(100) + setting2 : int32 + """ +``` + +`insert1` takes no key, a second insert raises, `fetch1()` returns the row, and +`heading.primary_key` is `[]`. Internally the table carries a hidden `_singleton` attribute as +its key, which is excluded from the heading, from `fetch()` results, and from join matching. + +See [Table Declaration](specs/table-declaration.md#25-singleton-tables-empty-primary-keys) for +the full behavior. + ## Foreign Keys ```python @@ -149,7 +174,6 @@ class Session(dj.Manual): DataJoint validates definitions at declaration time: -- Primary key must have at least one attribute - Attribute names must be valid identifiers - Types must be recognized - Foreign key references must exist diff --git a/src/reference/specs/fetch-api.md b/src/reference/specs/fetch-api.md index 9b91d2cc..801aefc8 100644 --- a/src/reference/specs/fetch-api.md +++ b/src/reference/specs/fetch-api.md @@ -66,7 +66,7 @@ for row in table: ### Basic Fetch Operations -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch()` | `table.to_arrays()` or `table.to_dicts()` | | `table.fetch(format="array")` | `table.to_arrays()` | @@ -75,7 +75,7 @@ for row in table: ### Attribute Fetching -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch('a')` | `table.to_arrays('a')` | | `a, b = table.fetch('a', 'b')` | `a, b = table.to_arrays('a', 'b')` | @@ -83,7 +83,7 @@ for row in table: ### Primary Key Fetching -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch('KEY')` | `table.keys()` | | `table.fetch(dj.key)` | `table.keys()` | @@ -100,7 +100,7 @@ a = table.to_arrays('a') ### Ordering, Limiting, Offset -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch(order_by='name')` | `table.to_arrays(order_by='name')` | | `table.fetch(limit=10)` | `table.to_arrays(limit=10)` | @@ -108,7 +108,7 @@ a = table.to_arrays('a') ### Single Row Fetch (fetch1) -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch1()` | `table.fetch1()` (unchanged) | | `a, b = table.fetch1('a', 'b')` | `a, b = table.fetch1('a', 'b')` (unchanged) | @@ -116,14 +116,14 @@ a = table.to_arrays('a') ### Configuration -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `dj.config['fetch_format'] = 'frame'` | Use `.to_pandas()` explicitly | | `with dj.config.override(fetch_format='frame'):` | Use `.to_pandas()` in the block | ### Iteration -| Old Pattern (1.x) | New Pattern (2.0) | +| Old Pattern (pre-2.x) | New Pattern (2.0) | |-------------------|-------------------| | `for row in table:` | `for row in table:` (same syntax, now lazy!) | | `list(table)` | `table.to_dicts()` | @@ -254,7 +254,7 @@ table = Experiment().to_arrow() The new iteration is significantly more efficient: ```python -# Old (1.x): N+1 queries +# Old (pre-2.x): N+1 queries # 1. fetch("KEY") gets ALL keys # 2. fetch1() for EACH key From f94402bebe0aeac33a3664a5e0ef768e8467df24 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Wed, 30 Sep 2026 11:45:43 -0500 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20apply=20review=20=E2=80=94=20versio?= =?UTF-8?q?n=20marker,=20heading=20form,=20named=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds the version-added admonition. The section documents a 2.1 feature on the page a reader consults while writing a definition; someone on 2.0 would otherwise follow it and hit a failure the page never explains. Matches section 2.5, which it links to. - Heading becomes a noun phrase, matching every other H2 here and the heading of the spec section it points at. - Names DuplicateError rather than saying a second insert raises. --- src/reference/definition-syntax.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/reference/definition-syntax.md b/src/reference/definition-syntax.md index 62eb6e2f..1a89faa1 100644 --- a/src/reference/definition-syntax.md +++ b/src/reference/definition-syntax.md @@ -36,7 +36,11 @@ core_type = int32 | float64 | varchar(n) | ... codec_type = "<" name ["@" [store]] ">" ``` -## A table with an empty primary key holds one row +## Singleton Tables + +!!! version-added "New in 2.1" + + Singleton tables were introduced in DataJoint 2.1. The primary key section may be empty. A table declared that way is a **singleton**: it holds at most one row, which is what you want for global configuration, pipeline-wide parameters, or a @@ -54,7 +58,7 @@ class Config(dj.Lookup): """ ``` -`insert1` takes no key, a second insert raises, `fetch1()` returns the row, and +`insert1` takes no key, a second insert raises `DuplicateError`, `fetch1()` returns the row, and `heading.primary_key` is `[]`. Internally the table carries a hidden `_singleton` attribute as its key, which is excluded from the heading, from `fetch()` results, and from join matching. From a1b6defe7bf475d0f8e0fb11ed7ba386f8a5cc6d Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Wed, 30 Sep 2026 11:50:11 -0500 Subject: [PATCH 3/3] docs: pre-2.0, the form the docs already use pre-2.x appeared nowhere on main. pre-2.0 appears 45 times across five files, including migrate-to-v20.md and about/versioning.md, and pairs with the 2.0 already in each table's right-hand column. --- src/reference/specs/fetch-api.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/reference/specs/fetch-api.md b/src/reference/specs/fetch-api.md index 801aefc8..6ee38ea1 100644 --- a/src/reference/specs/fetch-api.md +++ b/src/reference/specs/fetch-api.md @@ -66,7 +66,7 @@ for row in table: ### Basic Fetch Operations -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch()` | `table.to_arrays()` or `table.to_dicts()` | | `table.fetch(format="array")` | `table.to_arrays()` | @@ -75,7 +75,7 @@ for row in table: ### Attribute Fetching -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch('a')` | `table.to_arrays('a')` | | `a, b = table.fetch('a', 'b')` | `a, b = table.to_arrays('a', 'b')` | @@ -83,7 +83,7 @@ for row in table: ### Primary Key Fetching -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch('KEY')` | `table.keys()` | | `table.fetch(dj.key)` | `table.keys()` | @@ -100,7 +100,7 @@ a = table.to_arrays('a') ### Ordering, Limiting, Offset -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch(order_by='name')` | `table.to_arrays(order_by='name')` | | `table.fetch(limit=10)` | `table.to_arrays(limit=10)` | @@ -108,7 +108,7 @@ a = table.to_arrays('a') ### Single Row Fetch (fetch1) -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `table.fetch1()` | `table.fetch1()` (unchanged) | | `a, b = table.fetch1('a', 'b')` | `a, b = table.fetch1('a', 'b')` (unchanged) | @@ -116,14 +116,14 @@ a = table.to_arrays('a') ### Configuration -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `dj.config['fetch_format'] = 'frame'` | Use `.to_pandas()` explicitly | | `with dj.config.override(fetch_format='frame'):` | Use `.to_pandas()` in the block | ### Iteration -| Old Pattern (pre-2.x) | New Pattern (2.0) | +| Old Pattern (pre-2.0) | New Pattern (2.0) | |-------------------|-------------------| | `for row in table:` | `for row in table:` (same syntax, now lazy!) | | `list(table)` | `table.to_dicts()` | @@ -254,7 +254,7 @@ table = Experiment().to_arrow() The new iteration is significantly more efficient: ```python -# Old (pre-2.x): N+1 queries +# Old (pre-2.0): N+1 queries # 1. fetch("KEY") gets ALL keys # 2. fetch1() for EACH key