Skip to content

Commit aa1db4f

Browse files
Allow empty insert for tables with all defaults (#1280)
- When all attributes have defaults (autoincrement, nullable, or explicit default), allow inserting empty dicts: table.insert1({}) - Generates SQL: INSERT INTO table () VALUES () - For tables with required fields, raise clear error listing which attributes need values - Add tests for empty insert scenarios Closes #1280 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 13b73a0 commit aa1db4f

File tree

3 files changed

+87
-5
lines changed

3 files changed

+87
-5
lines changed

src/datajoint/table.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -864,10 +864,12 @@ def _insert_rows(self, rows, replace, skip_duplicates, ignore_extra_fields):
864864
rows = list(self.__make_row_to_insert(row, field_list, ignore_extra_fields) for row in rows)
865865
if rows:
866866
try:
867-
query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format(
867+
# Handle empty field_list (all-defaults insert)
868+
fields_clause = f"(`{'`,`'.join(field_list)}`)" if field_list else "()"
869+
query = "{command} INTO {destination}{fields} VALUES {placeholders}{duplicate}".format(
868870
command="REPLACE" if replace else "INSERT",
869871
destination=self.from_clause(),
870-
fields="`,`".join(field_list),
872+
fields=fields_clause,
871873
placeholders=",".join("(" + ",".join(row["placeholders"]) + ")" for row in rows),
872874
duplicate=(
873875
" ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`".format(pk=self.primary_key[0]) if skip_duplicates else ""
@@ -1457,8 +1459,19 @@ def check_fields(fields):
14571459
if ignore_extra_fields:
14581460
attributes = [a for a in attributes if a is not None]
14591461

1460-
assert len(attributes), "Empty tuple"
1461-
row_to_insert = dict(zip(("names", "placeholders", "values"), zip(*attributes)))
1462+
if not attributes:
1463+
# Check if empty insert is allowed (all attributes have defaults)
1464+
required_attrs = [
1465+
attr.name
1466+
for attr in self.heading.attributes.values()
1467+
if not (attr.autoincrement or attr.nullable or attr.default is not None)
1468+
]
1469+
if required_attrs:
1470+
raise DataJointError(f"Cannot insert empty row. The following attributes require values: {required_attrs}")
1471+
# All attributes have defaults - allow empty insert
1472+
row_to_insert = {"names": (), "placeholders": (), "values": ()}
1473+
else:
1474+
row_to_insert = dict(zip(("names", "placeholders", "values"), zip(*attributes)))
14621475
if not field_list:
14631476
# first row sets the composition of the field list
14641477
field_list.extend(row_to_insert["names"])

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.0.0a12"
4+
__version__ = "2.0.0a13"

tests/integration/test_insert.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,3 +438,72 @@ def test_validation_result_summary_truncated(self, schema_insert):
438438
result = dj.ValidationResult(is_valid=False, errors=errors, rows_checked=20)
439439
summary = result.summary()
440440
assert "and 10 more errors" in summary
441+
442+
443+
class AllDefaultsTable(dj.Manual):
444+
"""Table where all attributes have defaults."""
445+
446+
definition = """
447+
id : int auto_increment
448+
---
449+
timestamp=CURRENT_TIMESTAMP : datetime
450+
notes=null : varchar(200)
451+
"""
452+
453+
454+
class TestEmptyInsert:
455+
"""Tests for inserting empty dicts (GitHub issue #1280)."""
456+
457+
@pytest.fixture
458+
def schema_empty_insert(self, connection_test, prefix):
459+
schema = dj.Schema(
460+
prefix + "_empty_insert_test",
461+
context=dict(AllDefaultsTable=AllDefaultsTable, SimpleTable=SimpleTable),
462+
connection=connection_test,
463+
)
464+
schema(AllDefaultsTable)
465+
schema(SimpleTable)
466+
yield schema
467+
schema.drop()
468+
469+
def test_empty_insert_all_defaults(self, schema_empty_insert):
470+
"""Test that empty insert succeeds when all attributes have defaults."""
471+
table = AllDefaultsTable()
472+
assert len(table) == 0
473+
474+
# Insert empty dict - should use all defaults
475+
table.insert1({})
476+
assert len(table) == 1
477+
478+
# Check that values were populated with defaults
479+
row = table.fetch1()
480+
assert row["id"] == 1 # auto_increment starts at 1
481+
assert row["timestamp"] is not None # CURRENT_TIMESTAMP
482+
assert row["notes"] is None # nullable defaults to NULL
483+
484+
def test_empty_insert_multiple(self, schema_empty_insert):
485+
"""Test inserting multiple empty dicts."""
486+
table = AllDefaultsTable()
487+
488+
# Insert multiple empty dicts
489+
table.insert([{}, {}, {}])
490+
assert len(table) == 3
491+
492+
# Each should have unique auto_increment id
493+
ids = set(table.to_arrays("id"))
494+
assert ids == {1, 2, 3}
495+
496+
def test_empty_insert_required_fields_error(self, schema_empty_insert):
497+
"""Test that empty insert raises clear error when fields are required."""
498+
table = SimpleTable()
499+
500+
# SimpleTable has required fields (id, value)
501+
with pytest.raises(dj.DataJointError) as exc_info:
502+
table.insert1({})
503+
504+
error_msg = str(exc_info.value)
505+
assert "Cannot insert empty row" in error_msg
506+
assert "require values" in error_msg
507+
# Should list the required attributes
508+
assert "id" in error_msg
509+
assert "value" in error_msg

0 commit comments

Comments
 (0)