Skip to content

Commit 864b258

Browse files
feat: Add singleton tables (empty primary keys)
Implements support for singleton tables - tables with empty primary keys that can hold at most one row. This feature was described in the 2018 DataJoint paper and proposed in issue #113. Syntax: ```python @Schema class Config(dj.Lookup): definition = """ --- setting : varchar(100) """ ``` Implementation uses a hidden `_singleton` attribute of type `bool` as the primary key. This attribute is automatically created and excluded from user-facing operations (heading.attributes, fetch, join matching). Closes #113 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9775d0a commit 864b258

File tree

3 files changed

+109
-2
lines changed

3 files changed

+109
-2
lines changed

src/datajoint/declare.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,19 @@ def declare(
473473
attribute_sql.extend(job_metadata_sql)
474474

475475
if not primary_key:
476-
raise DataJointError("Table must have a primary key")
476+
# Singleton table: add hidden sentinel attribute
477+
primary_key = ["_singleton"]
478+
singleton_comment = ":bool:singleton primary key"
479+
sql_type = adapter.core_type_to_sql("bool")
480+
singleton_sql = adapter.format_column_definition(
481+
name="_singleton",
482+
sql_type=sql_type,
483+
nullable=False,
484+
default="NOT NULL DEFAULT 1",
485+
comment=singleton_comment,
486+
)
487+
attribute_sql.insert(0, singleton_sql)
488+
column_comments["_singleton"] = singleton_comment
477489

478490
pre_ddl = [] # DDL to run BEFORE CREATE TABLE (e.g., CREATE TYPE for enums)
479491
post_ddl = [] # DDL to run AFTER CREATE TABLE (e.g., COMMENT ON)

src/datajoint/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# version bump auto managed by Github Actions:
22
# label_prs.yaml(prep), release.yaml(bump), post_release.yaml(edit)
33
# manually set this version will be eventually overwritten by the above actions
4-
__version__ = "2.1.0a2"
4+
__version__ = "2.1.0a3"

tests/integration/test_declare.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,98 @@ class Table_With_Underscores(dj.Manual):
368368
schema_any(TableNoUnderscores)
369369
with pytest.raises(dj.DataJointError, match="must be alphanumeric in CamelCase"):
370370
schema_any(Table_With_Underscores)
371+
372+
373+
class TestSingletonTables:
374+
"""Tests for singleton tables (empty primary keys)."""
375+
376+
def test_singleton_declaration(self, schema_any):
377+
"""Singleton table creates correctly with hidden _singleton attribute."""
378+
379+
@schema_any
380+
class Config(dj.Lookup):
381+
definition = """
382+
# Global configuration
383+
---
384+
setting : varchar(100)
385+
"""
386+
387+
# Access attributes first to trigger lazy loading from database
388+
visible_attrs = Config.heading.attributes
389+
all_attrs = Config.heading._attributes
390+
391+
# Table should exist and have _singleton as hidden PK
392+
assert "_singleton" in all_attrs
393+
assert "_singleton" not in visible_attrs
394+
assert Config.heading.primary_key == [] # Visible PK is empty for singleton
395+
396+
def test_singleton_insert_and_fetch(self, schema_any):
397+
"""Insert and fetch work without specifying _singleton."""
398+
399+
@schema_any
400+
class Settings(dj.Lookup):
401+
definition = """
402+
---
403+
value : int32
404+
"""
405+
406+
# Insert without specifying _singleton
407+
Settings.insert1({"value": 42})
408+
409+
# Fetch should work
410+
result = Settings.fetch1()
411+
assert result["value"] == 42
412+
assert "_singleton" not in result # Hidden attribute excluded
413+
414+
def test_singleton_uniqueness(self, schema_any):
415+
"""Second insert raises DuplicateError."""
416+
417+
@schema_any
418+
class SingleValue(dj.Lookup):
419+
definition = """
420+
---
421+
data : varchar(50)
422+
"""
423+
424+
SingleValue.insert1({"data": "first"})
425+
426+
# Second insert should fail
427+
with pytest.raises(dj.errors.DuplicateError):
428+
SingleValue.insert1({"data": "second"})
429+
430+
def test_singleton_with_multiple_attributes(self, schema_any):
431+
"""Singleton table with multiple secondary attributes."""
432+
433+
@schema_any
434+
class PipelineConfig(dj.Lookup):
435+
definition = """
436+
# Pipeline configuration singleton
437+
---
438+
version : varchar(20)
439+
max_workers : int32
440+
debug_mode : bool
441+
"""
442+
443+
PipelineConfig.insert1(
444+
{"version": "1.0.0", "max_workers": 4, "debug_mode": False}
445+
)
446+
447+
result = PipelineConfig.fetch1()
448+
assert result["version"] == "1.0.0"
449+
assert result["max_workers"] == 4
450+
assert result["debug_mode"] == 0 # bool stored as tinyint
451+
452+
def test_singleton_describe(self, schema_any):
453+
"""Describe should show the singleton nature."""
454+
455+
@schema_any
456+
class Metadata(dj.Lookup):
457+
definition = """
458+
---
459+
info : varchar(255)
460+
"""
461+
462+
description = Metadata.describe()
463+
# Description should show just the secondary attribute
464+
assert "info" in description
465+
# _singleton is hidden, implementation detail

0 commit comments

Comments
 (0)