# ABOUTME: Tests for the interval projections migration (0004-interval-projections.sql). # ABOUTME: Validates tables, constraints, and indexes for time-series interval tracking. import apsw import pytest class TestMigrationCreatesAllTables: """Tests that migration creates all interval tables.""" def test_animal_location_intervals_table_exists(self, migrated_db): """Migration creates animal_location_intervals table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='animal_location_intervals'" ).fetchone() assert result is not None assert result[0] == "animal_location_intervals" def test_animal_tag_intervals_table_exists(self, migrated_db): """Migration creates animal_tag_intervals table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='animal_tag_intervals'" ).fetchone() assert result is not None assert result[0] == "animal_tag_intervals" def test_animal_attr_intervals_table_exists(self, migrated_db): """Migration creates animal_attr_intervals table.""" result = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='animal_attr_intervals'" ).fetchone() assert result is not None assert result[0] == "animal_attr_intervals" class TestAnimalLocationIntervalsTable: """Tests for animal_location_intervals table schema and constraints.""" def test_insert_valid_interval(self, migrated_db): """Can insert valid location interval data.""" migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc, end_utc) VALUES (?, ?, ?, ?) """, ( "01ARZ3NDEKTSV4RRFFQ69G5FAA", "01ARZ3NDEKTSV4RRFFQ69G5FAV", 1704067200000, 1704153600000, ), ) result = migrated_db.execute( "SELECT animal_id, location_id, start_utc, end_utc FROM animal_location_intervals" ).fetchone() assert result == ( "01ARZ3NDEKTSV4RRFFQ69G5FAA", "01ARZ3NDEKTSV4RRFFQ69G5FAV", 1704067200000, 1704153600000, ) def test_null_end_utc_allowed(self, migrated_db): """Open interval with NULL end_utc is allowed.""" migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc) VALUES (?, ?, ?) """, ("01ARZ3NDEKTSV4RRFFQ69G5FAA", "01ARZ3NDEKTSV4RRFFQ69G5FAV", 1704067200000), ) result = migrated_db.execute("SELECT end_utc FROM animal_location_intervals").fetchone() assert result[0] is None def test_animal_id_length_check(self, migrated_db): """Animal ID must be exactly 26 characters.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc) VALUES ('short', '01ARZ3NDEKTSV4RRFFQ69G5FAV', 1704067200000) """ ) def test_location_id_length_check(self, migrated_db): """Location ID must be exactly 26 characters.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'short', 1704067200000) """ ) def test_end_utc_must_be_greater_than_start_utc(self, migrated_db): """end_utc must be greater than start_utc when not NULL.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc, end_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', '01ARZ3NDEKTSV4RRFFQ69G5FAV', 1704067200000, 1704067200000) """ ) def test_end_utc_less_than_start_utc_rejected(self, migrated_db): """end_utc less than start_utc is rejected.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc, end_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', '01ARZ3NDEKTSV4RRFFQ69G5FAV', 1704067200000, 1704000000000) """ ) def test_composite_primary_key(self, migrated_db): """Primary key is (animal_id, start_utc).""" # Insert first interval migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', '01ARZ3NDEKTSV4RRFFQ69G5FAV', 1704067200000) """ ) # Same animal, different start_utc should work migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', '01ARZ3NDEKTSV4RRFFQ69G5FAW', 1704153600000) """ ) # Same animal, same start_utc should fail with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_location_intervals (animal_id, location_id, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', '01ARZ3NDEKTSV4RRFFQ69G5FAX', 1704067200000) """ ) class TestAnimalTagIntervalsTable: """Tests for animal_tag_intervals table schema and constraints.""" def test_insert_valid_interval(self, migrated_db): """Can insert valid tag interval data.""" migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc, end_utc) VALUES (?, ?, ?, ?) """, ("01ARZ3NDEKTSV4RRFFQ69G5FAA", "friendly", 1704067200000, 1704153600000), ) result = migrated_db.execute( "SELECT animal_id, tag, start_utc, end_utc FROM animal_tag_intervals" ).fetchone() assert result == ("01ARZ3NDEKTSV4RRFFQ69G5FAA", "friendly", 1704067200000, 1704153600000) def test_null_end_utc_allowed(self, migrated_db): """Open interval with NULL end_utc is allowed.""" migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc) VALUES (?, ?, ?) """, ("01ARZ3NDEKTSV4RRFFQ69G5FAA", "leader", 1704067200000), ) result = migrated_db.execute("SELECT end_utc FROM animal_tag_intervals").fetchone() assert result[0] is None def test_animal_id_length_check(self, migrated_db): """Animal ID must be exactly 26 characters.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc) VALUES ('short', 'friendly', 1704067200000) """ ) def test_end_utc_must_be_greater_than_start_utc(self, migrated_db): """end_utc must be greater than start_utc when not NULL.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc, end_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'friendly', 1704067200000, 1704067200000) """ ) def test_composite_primary_key(self, migrated_db): """Primary key is (animal_id, tag, start_utc).""" # Insert first interval migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'friendly', 1704067200000) """ ) # Same animal, same tag, different start_utc should work migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'friendly', 1704153600000) """ ) # Same animal, different tag, same start_utc should work migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'leader', 1704067200000) """ ) # Same animal, same tag, same start_utc should fail with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_tag_intervals (animal_id, tag, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'friendly', 1704067200000) """ ) class TestAnimalAttrIntervalsTable: """Tests for animal_attr_intervals table schema and constraints.""" def test_insert_valid_interval(self, migrated_db): """Can insert valid attr interval data.""" migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc, end_utc) VALUES (?, ?, ?, ?, ?) """, ("01ARZ3NDEKTSV4RRFFQ69G5FAA", "sex", "female", 1704067200000, 1704153600000), ) result = migrated_db.execute( "SELECT animal_id, attr, value, start_utc, end_utc FROM animal_attr_intervals" ).fetchone() assert result == ( "01ARZ3NDEKTSV4RRFFQ69G5FAA", "sex", "female", 1704067200000, 1704153600000, ) def test_null_end_utc_allowed(self, migrated_db): """Open interval with NULL end_utc is allowed.""" migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES (?, ?, ?, ?) """, ("01ARZ3NDEKTSV4RRFFQ69G5FAA", "status", "alive", 1704067200000), ) result = migrated_db.execute("SELECT end_utc FROM animal_attr_intervals").fetchone() assert result[0] is None def test_animal_id_length_check(self, migrated_db): """Animal ID must be exactly 26 characters.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('short', 'sex', 'female', 1704067200000) """ ) def test_attr_check_constraint_sex(self, migrated_db): """attr='sex' is allowed.""" migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'sex', 'female', 1704067200000) """ ) count = migrated_db.execute("SELECT COUNT(*) FROM animal_attr_intervals").fetchone()[0] assert count == 1 def test_attr_check_constraint_life_stage(self, migrated_db): """attr='life_stage' is allowed.""" migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'life_stage', 'adult', 1704067200000) """ ) count = migrated_db.execute("SELECT COUNT(*) FROM animal_attr_intervals").fetchone()[0] assert count == 1 def test_attr_check_constraint_repro_status(self, migrated_db): """attr='repro_status' is allowed.""" migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'repro_status', 'intact', 1704067200000) """ ) count = migrated_db.execute("SELECT COUNT(*) FROM animal_attr_intervals").fetchone()[0] assert count == 1 def test_attr_check_constraint_status(self, migrated_db): """attr='status' is allowed.""" migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'status', 'alive', 1704067200000) """ ) count = migrated_db.execute("SELECT COUNT(*) FROM animal_attr_intervals").fetchone()[0] assert count == 1 def test_attr_check_constraint_invalid_rejected(self, migrated_db): """Invalid attr value is rejected.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'invalid_attr', 'value', 1704067200000) """ ) def test_end_utc_must_be_greater_than_start_utc(self, migrated_db): """end_utc must be greater than start_utc when not NULL.""" with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc, end_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'sex', 'female', 1704067200000, 1704067200000) """ ) def test_composite_primary_key(self, migrated_db): """Primary key is (animal_id, attr, start_utc).""" # Insert first interval migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'sex', 'female', 1704067200000) """ ) # Same animal, same attr, different start_utc should work migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'sex', 'male', 1704153600000) """ ) # Same animal, different attr, same start_utc should work migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'status', 'alive', 1704067200000) """ ) # Same animal, same attr, same start_utc should fail with pytest.raises(apsw.ConstraintError): migrated_db.execute( """ INSERT INTO animal_attr_intervals (animal_id, attr, value, start_utc) VALUES ('01ARZ3NDEKTSV4RRFFQ69G5FAA', 'sex', 'unknown', 1704067200000) """ ) class TestIndexes: """Tests that indexes are created correctly.""" def test_location_intervals_indexes_exist(self, migrated_db): """animal_location_intervals table has required indexes.""" indexes = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='animal_location_intervals'" ).fetchall() index_names = {row[0] for row in indexes} assert "idx_ali_loc_time" in index_names assert "idx_ali_animal_time" in index_names def test_tag_intervals_indexes_exist(self, migrated_db): """animal_tag_intervals table has required indexes.""" indexes = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='animal_tag_intervals'" ).fetchall() index_names = {row[0] for row in indexes} assert "idx_ati_tag_time" in index_names def test_attr_intervals_indexes_exist(self, migrated_db): """animal_attr_intervals table has required indexes.""" indexes = migrated_db.execute( "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='animal_attr_intervals'" ).fetchall() index_names = {row[0] for row in indexes} assert "idx_aai_attr_time" in index_names