Findings
Errors (5)
F190Impersonations impersonatedById, impersonatedUserIdjunction-uniqueness Junction table allows duplicate links
Impersonations looks like a many-to-many link table but has no unique constraint across (impersonatedById, impersonatedUserId). The same pair can be inserted twice; every join through it will double-count.
Fix Use the FK pair as the primary key (or add a UNIQUE).
ALTER TABLE Impersonations ADD PRIMARY KEY (impersonatedById, impersonatedUserId);
No primary key
Without a primary key rows are not individually addressable: no safe UPDATE/DELETE of one row, no logical replication, ORMs misbehave, and duplicates are legal.
Fix Add a natural composite key or a surrogate id.
ALTER TABLE ManagedOrganization ADD PRIMARY KEY (...);
Monetary value stored as float
pricePerSeat is binary floating point. 0.1 + 0.2 ≠ 0.3; sums drift; reconciliation against the ledger will be off by cents.
Fix Use NUMERIC(p, s) or integer minor units.
ALTER TABLE OrganizationOnboarding ALTER COLUMN pricePerSeat TYPE numeric(14,2);
No primary key
Without a primary key rows are not individually addressable: no safe UPDATE/DELETE of one row, no logical replication, ORMs misbehave, and duplicates are legal.
Fix Add a natural composite key or a surrogate id.
ALTER TABLE UserPassword ADD PRIMARY KEY (...);
No primary key
Without a primary key rows are not individually addressable: no safe UPDATE/DELETE of one row, no logical replication, ORMs misbehave, and duplicates are legal.
Fix Add a natural composite key or a surrogate id.
ALTER TABLE avatars ADD PRIMARY KEY (...);
Warnings (78)
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on OAuthClient must scan AccessCode to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AccessCode maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often OAuthClient rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. clientId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AccessCode_clientId ON AccessCode(clientId) WHERE clientId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan AccessCode to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AccessCode maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. userId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AccessCode_userId ON AccessCode(userId) WHERE userId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan AccessCode to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AccessCode maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AccessCode_teamId ON AccessCode(teamId) WHERE teamId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan AccessToken to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AccessToken maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_AccessToken_userId ON AccessToken(userId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on PlatformOAuthClient must scan AccessToken to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AccessToken maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often PlatformOAuthClient rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_AccessToken_platformOAuthClientId ON AccessToken(platformOAuthClientId);
Enum-like column with no CHECK
type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE Account ADD CONSTRAINT Account_type_check CHECK (type IN (...));
F195Account token_typeundocumented-enum Enum-like column with no CHECK
token_type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE Account ADD CONSTRAINT Account_token_type_check CHECK (token_type IN (...));
Enum-like column with no CHECK
scope is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE Account ADD CONSTRAINT Account_scope_check CHECK (scope IN (...));
F197Account session_stateundocumented-enum Enum-like column with no CHECK
session_state is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE Account ADD CONSTRAINT Account_session_state_check CHECK (session_state IN (...));
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan ApiKey to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to ApiKey maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_ApiKey_teamId ON ApiKey(teamId) WHERE teamId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on App must scan ApiKey to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to ApiKey maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often App rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. appId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_ApiKey_appId ON ApiKey(appId) WHERE appId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan App_RoutingForms_Form to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to App_RoutingForms_Form maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. updatedById is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_App_RoutingForms_Form_updatedById ON App_RoutingForms_Form(updatedById) WHERE updatedById IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan App_RoutingForms_Form to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to App_RoutingForms_Form maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_App_RoutingForms_Form_teamId ON App_RoutingForms_Form(teamId) WHERE teamId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on App_RoutingForms_Form must scan App_RoutingForms_IncompleteBookingActions to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to App_RoutingForms_IncompleteBookingActions maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often App_RoutingForms_Form rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_App_RoutingForms_IncompleteBookingActions_formId ON App_RoutingForms_IncompleteBookingActions(formId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on App_RoutingForms_Form must scan App_RoutingForms_QueuedFormResponse to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to App_RoutingForms_QueuedFormResponse maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often App_RoutingForms_Form rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_App_RoutingForms_QueuedFormResponse_formId ON App_RoutingForms_QueuedFormResponse(formId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Attribute must scan AttributeOption to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeOption maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Attribute rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_AttributeOption_attributeId ON AttributeOption(attributeId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Attribute must scan AttributeSyncFieldMapping to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeSyncFieldMapping maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Attribute rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_AttributeSyncFieldMapping_attributeId ON AttributeSyncFieldMapping(attributeId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on AttributeOption must scan AttributeToUser to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeToUser maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often AttributeOption rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_AttributeToUser_attributeOptionId ON AttributeToUser(attributeOptionId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan AttributeToUser to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeToUser maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. createdById is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AttributeToUser_createdById ON AttributeToUser(createdById) WHERE createdById IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DSyncData must scan AttributeToUser to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeToUser maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DSyncData rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. createdByDSyncId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AttributeToUser_createdByDSyncId ON AttributeToUser(createdByDSyncId) WHERE createdByDSyncId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan AttributeToUser to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeToUser maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. updatedById is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AttributeToUser_updatedById ON AttributeToUser(updatedById) WHERE updatedById IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DSyncData must scan AttributeToUser to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to AttributeToUser maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DSyncData rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. updatedByDSyncId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_AttributeToUser_updatedByDSyncId ON AttributeToUser(updatedByDSyncId) WHERE updatedByDSyncId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan Booking to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Booking maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. reassignById is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Booking_reassignById ON Booking(reassignById) WHERE reassignById IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on InternalNotePreset must scan BookingInternalNote to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to BookingInternalNote maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often InternalNotePreset rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. notePresetId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_BookingInternalNote_notePresetId ON BookingInternalNote(notePresetId) WHERE notePresetId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan BookingInternalNote to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to BookingInternalNote maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_BookingInternalNote_createdById ON BookingInternalNote(createdById);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Credential must scan BookingReference to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to BookingReference maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Credential rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. credentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_BookingReference_credentialId ON BookingReference(credentialId) WHERE credentialId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DelegationCredential must scan BookingReference to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to BookingReference maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DelegationCredential rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. delegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_BookingReference_delegationCredentialId ON BookingReference(delegationCredentialId) WHERE delegationCredentialId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DomainWideDelegation must scan BookingReference to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to BookingReference maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DomainWideDelegation rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. domainWideDelegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_BookingReference_domainWideDelegationCredentialId ON BookingReference(domainWideDelegationCredentialId) WHERE domainWideDelegationCredentialId IS NOT NULL;
Enum-like column with no CHECK
type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE BookingReference ADD CONSTRAINT BookingReference_type_check CHECK (type IN (...));
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DelegationCredential must scan Credential to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Credential maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DelegationCredential rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. delegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Credential_delegationCredentialId ON Credential(delegationCredentialId) WHERE delegationCredentialId IS NOT NULL;
Enum-like column with no CHECK
type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE Credential ADD CONSTRAINT Credential_type_check CHECK (type IN (...));
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on CreditBalance must scan CreditExpenseLog to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to CreditExpenseLog maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often CreditBalance rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_CreditExpenseLog_creditBalanceId ON CreditExpenseLog(creditBalanceId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Booking must scan CreditExpenseLog to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to CreditExpenseLog maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Booking rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. bookingUid is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_CreditExpenseLog_bookingUid ON CreditExpenseLog(bookingUid) WHERE bookingUid IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on CreditBalance must scan CreditPurchaseLog to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to CreditPurchaseLog maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often CreditBalance rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_CreditPurchaseLog_creditBalanceId ON CreditPurchaseLog(creditBalanceId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DSyncData must scan DSyncTeamGroupMapping to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to DSyncTeamGroupMapping maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DSyncData rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_DSyncTeamGroupMapping_directoryId ON DSyncTeamGroupMapping(directoryId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on WorkspacePlatform must scan DelegationCredential to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to DelegationCredential maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often WorkspacePlatform rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_DelegationCredential_workspacePlatformId ON DelegationCredential(workspacePlatformId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DelegationCredential must scan DestinationCalendar to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to DestinationCalendar maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DelegationCredential rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. delegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_DestinationCalendar_delegationCredentialId ON DestinationCalendar(delegationCredentialId) WHERE delegationCredentialId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DomainWideDelegation must scan DestinationCalendar to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to DestinationCalendar maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DomainWideDelegation rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. domainWideDelegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_DestinationCalendar_domainWideDelegationCredentialId ON DestinationCalendar(domainWideDelegationCredentialId) WHERE domainWideDelegationCredentialId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on WorkspacePlatform must scan DomainWideDelegation to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to DomainWideDelegation maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often WorkspacePlatform rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_DomainWideDelegation_workspacePlatformId ON DomainWideDelegation(workspacePlatformId);
F005EventType instantMeetingScheduleIdfk-index Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Schedule must scan EventType to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to EventType maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Schedule rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. instantMeetingScheduleId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_EventType_instantMeetingScheduleId ON EventType(instantMeetingScheduleId) WHERE instantMeetingScheduleId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan EventTypeTranslation to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to EventTypeTranslation maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_EventTypeTranslation_createdBy ON EventTypeTranslation(createdBy);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan EventTypeTranslation to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to EventTypeTranslation maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. updatedBy is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_EventTypeTranslation_updatedBy ON EventTypeTranslation(updatedBy) WHERE updatedBy IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan FilterSegment to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to FilterSegment maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_FilterSegment_userId ON FilterSegment(userId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan FilterSegment to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to FilterSegment maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_FilterSegment_teamId ON FilterSegment(teamId) WHERE teamId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on HostGroup must scan Host to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Host maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often HostGroup rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. groupId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Host_groupId ON Host(groupId) WHERE groupId IS NOT NULL;
Enum-like column with no CHECK
type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE HostLocation ADD CONSTRAINT HostLocation_type_check CHECK (type IN (...));
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan InstantMeetingToken to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to InstantMeetingToken maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_InstantMeetingToken_teamId ON InstantMeetingToken(teamId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on TeamBilling must scan MonthlyProration to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to MonthlyProration maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often TeamBilling rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamBillingId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_MonthlyProration_teamBillingId ON MonthlyProration(teamBillingId) WHERE teamBillingId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on OrganizationBilling must scan MonthlyProration to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to MonthlyProration maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often OrganizationBilling rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. organizationBillingId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_MonthlyProration_organizationBillingId ON MonthlyProration(organizationBillingId) WHERE organizationBillingId IS NOT NULL;
Enum-like column with no CHECK
status is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE OrganizationBilling ADD CONSTRAINT OrganizationBilling_status_check CHECK (status IN (...));
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan OrganizationOnboarding to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to OrganizationOnboarding maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_OrganizationOnboarding_createdById ON OrganizationOnboarding(createdById);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on OutOfOfficeReason must scan OutOfOfficeEntry to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to OutOfOfficeEntry maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often OutOfOfficeReason rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. reasonId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_OutOfOfficeEntry_reasonId ON OutOfOfficeEntry(reasonId) WHERE reasonId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan OutOfOfficeReason to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to OutOfOfficeReason maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. userId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_OutOfOfficeReason_userId ON OutOfOfficeReason(userId) WHERE userId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on App must scan Payment to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Payment maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often App rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. appId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Payment_appId ON Payment(appId) WHERE appId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on PlatformOAuthClient must scan PlatformAuthorizationToken to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to PlatformAuthorizationToken maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often PlatformOAuthClient rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_PlatformAuthorizationToken_platformOAuthClientId ON PlatformAuthorizationToken(platformOAuthClientId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on PlatformBilling must scan PlatformBilling to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to PlatformBilling maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often PlatformBilling rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. managerBillingId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_PlatformBilling_managerBillingId ON PlatformBilling(managerBillingId) WHERE managerBillingId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan PlatformOAuthClient to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to PlatformOAuthClient maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_PlatformOAuthClient_organizationId ON PlatformOAuthClient(organizationId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan RefreshToken to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to RefreshToken maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_RefreshToken_userId ON RefreshToken(userId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on PlatformOAuthClient must scan RefreshToken to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to RefreshToken maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often PlatformOAuthClient rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_RefreshToken_platformOAuthClientId ON RefreshToken(platformOAuthClientId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on MonthlyProration must scan SeatChangeLog to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to SeatChangeLog maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often MonthlyProration rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. processedInProrationId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_SeatChangeLog_processedInProrationId ON SeatChangeLog(processedInProrationId) WHERE processedInProrationId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on TeamBilling must scan SeatChangeLog to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to SeatChangeLog maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often TeamBilling rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamBillingId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_SeatChangeLog_teamBillingId ON SeatChangeLog(teamBillingId) WHERE teamBillingId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on OrganizationBilling must scan SeatChangeLog to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to SeatChangeLog maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often OrganizationBilling rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. organizationBillingId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_SeatChangeLog_organizationBillingId ON SeatChangeLog(organizationBillingId) WHERE organizationBillingId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DelegationCredential must scan SelectedCalendar to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to SelectedCalendar maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DelegationCredential rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. delegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_SelectedCalendar_delegationCredentialId ON SelectedCalendar(delegationCredentialId) WHERE delegationCredentialId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on DomainWideDelegation must scan SelectedCalendar to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to SelectedCalendar maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often DomainWideDelegation rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. domainWideDelegationCredentialId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_SelectedCalendar_domainWideDelegationCredentialId ON SelectedCalendar(domainWideDelegationCredentialId) WHERE domainWideDelegationCredentialId IS NOT NULL;
F198Task typeundocumented-enum Enum-like column with no CHECK
type is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE Task ADD CONSTRAINT Task_type_check CHECK (type IN (...));
F011Team createdByOAuthClientIdfk-index Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on PlatformOAuthClient must scan Team to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Team maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often PlatformOAuthClient rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. createdByOAuthClientId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Team_createdByOAuthClientId ON Team(createdByOAuthClientId) WHERE createdByOAuthClientId IS NOT NULL;
Foreign-key cycle
Team → PlatformOAuthClient → Team. Rows must be inserted with a deferred constraint or a NULL-then-update dance; backups/restores and truncation have no valid order; ON DELETE CASCADE can loop.
Fix Break the cycle (move one FK to a link table) or mark one FK DEFERRABLE INITIALLY DEFERRED.
Enum-like column with no CHECK
status is a short string that clearly takes a fixed set of values, but the database accepts anything. Typos become new states, and nobody can list the legal values without reading application code.
Fix Add a CHECK constraint (cheap, easy to evolve) or a lookup table if values need metadata. Avoid native ENUM types unless the set is truly frozen.
ALTER TABLE TeamBilling ADD CONSTRAINT TeamBilling_status_check CHECK (status IN (...));
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Feature must scan TeamFeatures to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to TeamFeatures maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Feature rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_TeamFeatures_featureId ON TeamFeatures(featureId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on users must scan TravelSchedule to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to TravelSchedule maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often users rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_TravelSchedule_userId ON TravelSchedule(userId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Feature must scan UserFeatures to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to UserFeatures maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Feature rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add an index on the FK column(s).
CREATE INDEX CONCURRENTLY idx_UserFeatures_featureId ON UserFeatures(featureId);
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan Webhook to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Webhook maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. teamId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Webhook_teamId ON Webhook(teamId) WHERE teamId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on EventType must scan Webhook to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Webhook maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often EventType rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. eventTypeId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Webhook_eventTypeId ON Webhook(eventTypeId) WHERE eventTypeId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on App must scan Webhook to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to Webhook maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often App rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. appId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_Webhook_appId ON Webhook(appId) WHERE appId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Webhook must scan WebhookScheduledTriggers to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to WebhookScheduledTriggers maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Webhook rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. webhookId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_WebhookScheduledTriggers_webhookId ON WebhookScheduledTriggers(webhookId) WHERE webhookId IS NOT NULL;
Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Booking must scan WebhookScheduledTriggers to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to WebhookScheduledTriggers maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Booking rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. bookingId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_WebhookScheduledTriggers_bookingId ON WebhookScheduledTriggers(bookingId) WHERE bookingId IS NOT NULL;
F010users organizationIdfk-index Foreign key without index
PostgreSQL does not index FK columns automatically. Every DELETE/UPDATE on Team must scan users to check the constraint, and every join from the parent side is a sequential scan. The scan cost grows with the child table forever, so this gets worse on its own.
An index is not free. Every write to users maintains it — insert, update and delete alike — and it takes disk. Weigh that against how often Team rows are actually deleted or re-keyed — if the answer is never, the scan never happens and the index only costs.
Fix Add a partial index, scoped to the rows that have a value. organizationId is nullable, so the NULL rows are not indexed at all and the write cost is a fraction of a full index, while the constraint check still uses it.
CREATE INDEX CONCURRENTLY idx_users_organizationId ON users(organizationId) WHERE organizationId IS NOT NULL;
Foreign-key cycle
users → Profile → users. Rows must be inserted with a deferred constraint or a NULL-then-update dance; backups/restores and truncation have no valid order; ON DELETE CASCADE can loop.
Fix Break the cycle (move one FK to a link table) or mark one FK DEFERRABLE INITIALLY DEFERRED.
Notes (378)
Nullable foreign key
AccessCode.clientId may be NULL, so the relationship to OAuthClient is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
AccessCode.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
AccessCode.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F222AccessCode clientId, userId, teamIdexclusive-arc Either/or foreign keys without a CHECK
AccessCode has several nullable FKs (clientId, userId, teamId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE AccessCode ADD CONSTRAINT AccessCode_one_target CHECK ((clientId IS NOT NULL)::int + (userId IS NOT NULL)::int + (teamId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
expiresAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AccessCode ALTER COLUMN expiresAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AccessToken ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
expiresAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AccessToken ALTER COLUMN expiresAt TYPE timestamptz;
F172Agent userIdfk-nullable Nullable foreign key
Agent.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F173Agent teamIdfk-nullable Nullable foreign key
Agent.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F227Agent userId, teamIdexclusive-arc Either/or foreign keys without a CHECK
Agent has several nullable FKs (userId, teamId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Agent ADD CONSTRAINT Agent_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int = 1);
F397Agent createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Agent ALTER COLUMN createdAt TYPE timestamptz;
F398Agent updatedAttimestamp-tz TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Agent ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
ApiKey.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
ApiKey.appId may be NULL, so the relationship to App is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F214ApiKey teamId, appIdexclusive-arc Either/or foreign keys without a CHECK
ApiKey has several nullable FKs (teamId, appId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE ApiKey ADD CONSTRAINT ApiKey_one_target CHECK ((teamId IS NOT NULL)::int + (appId IS NOT NULL)::int = 1);
F288ApiKey createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ApiKey ALTER COLUMN createdAt TYPE timestamptz;
F289ApiKey expiresAttimestamp-tz TIMESTAMP without time zone
expiresAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ApiKey ALTER COLUMN expiresAt TYPE timestamptz;
F290ApiKey lastUsedAttimestamp-tz TIMESTAMP without time zone
lastUsedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ApiKey ALTER COLUMN lastUsedAt TYPE timestamptz;
F295App createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App ALTER COLUMN createdAt TYPE timestamptz;
F296App updatedAttimestamp-tz TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
App_RoutingForms_Form.updatedById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
App_RoutingForms_Form.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
App_RoutingForms_Form has several nullable FKs (updatedById, teamId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE App_RoutingForms_Form ADD CONSTRAINT App_RoutingForms_Form_one_target CHECK ((updatedById IS NOT NULL)::int + (teamId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App_RoutingForms_Form ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App_RoutingForms_Form ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
App_RoutingForms_FormResponse.routedToBookingUid may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App_RoutingForms_FormResponse ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App_RoutingForms_FormResponse ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
App_RoutingForms_QueuedFormResponse.actualResponseId may be NULL, so the relationship to App_RoutingForms_FormResponse is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App_RoutingForms_QueuedFormResponse ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE App_RoutingForms_QueuedFormResponse ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AssignmentReason ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
Attendee.bookingId may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Attribute ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Attribute ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AttributeSyncFieldMapping ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AttributeSyncFieldMapping ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AttributeSyncRule ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AttributeSyncRule ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
AttributeToUser.createdById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
AttributeToUser.createdByDSyncId may be NULL, so the relationship to DSyncData is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
AttributeToUser.updatedById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
AttributeToUser.updatedByDSyncId may be NULL, so the relationship to DSyncData is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F224AttributeToUser createdById, createdByDSyncId, updatedById, updatedByDSyncIdexclusive-arc Either/or foreign keys without a CHECK
AttributeToUser has several nullable FKs (createdById, createdByDSyncId, updatedById, updatedByDSyncId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE AttributeToUser ADD CONSTRAINT AttributeToUser_one_target CHECK ((createdById IS NOT NULL)::int + (createdByDSyncId IS NOT NULL)::int + (updatedById IS NOT NULL)::int + (updatedByDSyncId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AttributeToUser ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AttributeToUser ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE AuditActor ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
Availability.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Availability.eventTypeId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Availability.scheduleId may be NULL, so the relationship to Schedule is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F211Availability userId, eventTypeId, scheduleIdexclusive-arc Either/or foreign keys without a CHECK
Availability has several nullable FKs (userId, eventTypeId, scheduleId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Availability ADD CONSTRAINT Availability_one_target CHECK ((userId IS NOT NULL)::int + (eventTypeId IS NOT NULL)::int + (scheduleId IS NOT NULL)::int = 1);
Nullable foreign key
Booking.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Booking.eventTypeId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F110Booking destinationCalendarIdfk-nullable Nullable foreign key
Booking.destinationCalendarId may be NULL, so the relationship to DestinationCalendar is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F111Booking reassignByIdfk-nullable Nullable foreign key
Booking.reassignById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F210Booking userId, eventTypeId, destinationCalendarId, reassignByIdexclusive-arc Either/or foreign keys without a CHECK
Booking has several nullable FKs (userId, eventTypeId, destinationCalendarId, reassignById) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Booking ADD CONSTRAINT Booking_one_target CHECK ((userId IS NOT NULL)::int + (eventTypeId IS NOT NULL)::int + (destinationCalendarId IS NOT NULL)::int + (reassignById IS NOT NULL)::int = 1);
TIMESTAMP without time zone
startTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Booking ALTER COLUMN startTime TYPE timestamptz;
TIMESTAMP without time zone
endTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Booking ALTER COLUMN endTime TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Booking ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Booking ALTER COLUMN updatedAt TYPE timestamptz;
Hub table: referenced by 16 tables
Any change to its key, its delete semantics, or its partitioning touches every dependent. Migrations on hub tables need the longest lock windows and the most careful rollout.
Fix Treat schema changes here as breaking changes with a written rollout plan.
Wide table (40 columns)
Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
Fix Look for column groups that always change together and split them out.
TIMESTAMP without time zone
timestamp stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingAudit ALTER COLUMN timestamp TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingAudit ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingAudit ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
startTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingDenormalized ALTER COLUMN startTime TYPE timestamptz;
TIMESTAMP without time zone
endTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingDenormalized ALTER COLUMN endTime TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingDenormalized ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingDenormalized ALTER COLUMN updatedAt TYPE timestamptz;
Isolated table
BookingDenormalized references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
Nullable foreign key
BookingInternalNote.notePresetId may be NULL, so the relationship to InternalNotePreset is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingInternalNote ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
BookingReference.bookingId may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
BookingReference.credentialId may be NULL, so the relationship to Credential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
BookingReference.delegationCredentialId may be NULL, so the relationship to DelegationCredential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
BookingReference.domainWideDelegationCredentialId may be NULL, so the relationship to DomainWideDelegation is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F209BookingReference bookingId, credentialId, delegationCredentialId, domainWideDelegationCredentialIdexclusive-arc Either/or foreign keys without a CHECK
BookingReference has several nullable FKs (bookingId, credentialId, delegationCredentialId, domainWideDelegationCredentialId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE BookingReference ADD CONSTRAINT BookingReference_one_target CHECK ((bookingId IS NOT NULL)::int + (credentialId IS NOT NULL)::int + (delegationCredentialId IS NOT NULL)::int + (domainWideDelegationCredentialId IS NOT NULL)::int = 1);
Nullable foreign key
BookingReport.reportedById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
BookingReport.organizationId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
BookingReport.watchlistId may be NULL, so the relationship to Watchlist is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
BookingReport.globalWatchlistId may be NULL, so the relationship to Watchlist is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F225BookingReport reportedById, organizationId, watchlistId, globalWatchlistIdexclusive-arc Either/or foreign keys without a CHECK
BookingReport has several nullable FKs (reportedById, organizationId, watchlistId, globalWatchlistId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE BookingReport ADD CONSTRAINT BookingReport_one_target CHECK ((reportedById IS NOT NULL)::int + (organizationId IS NOT NULL)::int + (watchlistId IS NOT NULL)::int + (globalWatchlistId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingReport ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE BookingReport ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
CalAiPhoneNumber.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
CalAiPhoneNumber.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
CalAiPhoneNumber.inboundAgentId may be NULL, so the relationship to Agent is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
CalAiPhoneNumber.outboundAgentId may be NULL, so the relationship to Agent is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F228CalAiPhoneNumber userId, teamId, inboundAgentId, outboundAgentIdexclusive-arc Either/or foreign keys without a CHECK
CalAiPhoneNumber has several nullable FKs (userId, teamId, inboundAgentId, outboundAgentId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE CalAiPhoneNumber ADD CONSTRAINT CalAiPhoneNumber_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int + (inboundAgentId IS NOT NULL)::int + (outboundAgentId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalAiPhoneNumber ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalAiPhoneNumber ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalVideoSettings ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalVideoSettings ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
expiresAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCache ALTER COLUMN expiresAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCache ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
start stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN start TYPE timestamptz;
TIMESTAMP without time zone
end stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN end TYPE timestamptz;
TIMESTAMP without time zone
originalStartTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN originalStartTime TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
externalCreatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN externalCreatedAt TYPE timestamptz;
TIMESTAMP without time zone
externalUpdatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CalendarCacheEvent ALTER COLUMN externalUpdatedAt TYPE timestamptz;
Nullable foreign key
Credential.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Credential.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Credential.appId may be NULL, so the relationship to App is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Credential.delegationCredentialId may be NULL, so the relationship to DelegationCredential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F203Credential userId, teamId, appId, delegationCredentialIdexclusive-arc Either/or foreign keys without a CHECK
Credential has several nullable FKs (userId, teamId, appId, delegationCredentialId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Credential ADD CONSTRAINT Credential_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int + (appId IS NOT NULL)::int + (delegationCredentialId IS NOT NULL)::int = 1);
Nullable foreign key
CreditBalance.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
CreditBalance.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
CreditBalance has several nullable FKs (teamId, userId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE CreditBalance ADD CONSTRAINT CreditBalance_one_target CHECK ((teamId IS NOT NULL)::int + (userId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
limitReachedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CreditBalance ALTER COLUMN limitReachedAt TYPE timestamptz;
TIMESTAMP without time zone
warningSentAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CreditBalance ALTER COLUMN warningSentAt TYPE timestamptz;
Nullable foreign key
CreditExpenseLog.bookingUid may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
date stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CreditExpenseLog ALTER COLUMN date TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE CreditPurchaseLog ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
DSyncData.organizationId may be NULL, so the relationship to OrganizationSettings is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
lastEnabledAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DelegationCredential ALTER COLUMN lastEnabledAt TYPE timestamptz;
TIMESTAMP without time zone
lastDisabledAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DelegationCredential ALTER COLUMN lastDisabledAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DelegationCredential ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DelegationCredential ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
agreedLicenseAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Deployment ALTER COLUMN agreedLicenseAt TYPE timestamptz;
Isolated table
Deployment references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
Nullable foreign key
DestinationCalendar.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
DestinationCalendar.eventTypeId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
DestinationCalendar.credentialId may be NULL, so the relationship to Credential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
DestinationCalendar.delegationCredentialId may be NULL, so the relationship to DelegationCredential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
DestinationCalendar.domainWideDelegationCredentialId may be NULL, so the relationship to DomainWideDelegation is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F204DestinationCalendar userId, eventTypeId, credentialId, delegationCredentialId, domainWideDelegationCredentialIdexclusive-arc Either/or foreign keys without a CHECK
DestinationCalendar has several nullable FKs (userId, eventTypeId, credentialId, delegationCredentialId, domainWideDelegationCredentialId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE DestinationCalendar ADD CONSTRAINT DestinationCalendar_one_target CHECK ((userId IS NOT NULL)::int + (eventTypeId IS NOT NULL)::int + (credentialId IS NOT NULL)::int + (delegationCredentialId IS NOT NULL)::int + (domainWideDelegationCredentialId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DestinationCalendar ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DestinationCalendar ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DomainWideDelegation ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE DomainWideDelegation ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
EventType.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
EventType.profileId may be NULL, so the relationship to Profile is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
EventType.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
EventType.parentId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
EventType.scheduleId may be NULL, so the relationship to Schedule is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F080EventType instantMeetingScheduleIdfk-nullable Nullable foreign key
EventType.instantMeetingScheduleId may be NULL, so the relationship to Schedule is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
EventType.secondaryEmailId may be NULL, so the relationship to SecondaryEmail is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F082EventType restrictionScheduleIdfk-nullable Nullable foreign key
EventType.restrictionScheduleId may be NULL, so the relationship to Schedule is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F202EventType userId, profileId, teamId, parentId, scheduleId, instantMeetingScheduleId, secondaryEmailId, restrictionScheduleIdexclusive-arc Either/or foreign keys without a CHECK
EventType has several nullable FKs (userId, profileId, teamId, parentId, scheduleId, instantMeetingScheduleId, secondaryEmailId, restrictionScheduleId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE EventType ADD CONSTRAINT EventType_one_target CHECK ((userId IS NOT NULL)::int + (profileId IS NOT NULL)::int + (teamId IS NOT NULL)::int + (parentId IS NOT NULL)::int + (scheduleId IS NOT NULL)::int + (instantMeetingScheduleId IS NOT NULL)::int + (secondaryEmailId IS NOT NULL)::int + (restrictionScheduleId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
periodStartDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE EventType ALTER COLUMN periodStartDate TYPE timestamptz;
TIMESTAMP without time zone
periodEndDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE EventType ALTER COLUMN periodEndDate TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE EventType ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE EventType ALTER COLUMN updatedAt TYPE timestamptz;
Wide table (86 columns)
Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
Fix Look for column groups that always change together and split them out.
Nullable foreign key
EventTypeTranslation.updatedBy may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE EventTypeTranslation ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE EventTypeTranslation ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
lastUsedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Feature ALTER COLUMN lastUsedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Feature ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Feature ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
date stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Feedback ALTER COLUMN date TYPE timestamptz;
Nullable foreign key
FilterSegment.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE FilterSegment ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE FilterSegment ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
expiresAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HashedLink ALTER COLUMN expiresAt TYPE timestamptz;
TIMESTAMP without time zone
date stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HolidayCache ALTER COLUMN date TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HolidayCache ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HolidayCache ALTER COLUMN updatedAt TYPE timestamptz;
Isolated table
HolidayCache references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
F070Host scheduleIdfk-nullable Nullable foreign key
Host.scheduleId may be NULL, so the relationship to Schedule is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F071Host groupIdfk-nullable Nullable foreign key
Host.groupId may be NULL, so the relationship to HostGroup is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F072Host memberIdfk-nullable Nullable foreign key
Host.memberId may be NULL, so the relationship to Membership is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F201Host scheduleId, groupId, memberIdexclusive-arc Either/or foreign keys without a CHECK
Host has several nullable FKs (scheduleId, groupId, memberId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Host ADD CONSTRAINT Host_one_target CHECK ((scheduleId IS NOT NULL)::int + (groupId IS NOT NULL)::int + (memberId IS NOT NULL)::int = 1);
F233Host createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Host ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
HostGroup.eventTypeId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HostGroup ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HostGroup ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
HostLocation.credentialId may be NULL, so the relationship to Credential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HostLocation ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE HostLocation ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Impersonations ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
InstantMeetingToken.bookingId may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
expires stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE InstantMeetingToken ALTER COLUMN expires TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE InstantMeetingToken ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE InstantMeetingToken ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
IntegrationAttributeSync.credentialId may be NULL, so the relationship to Credential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE IntegrationAttributeSync ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE IntegrationAttributeSync ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE InternalNotePreset ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ManagedOrganization ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
Membership.customRoleId may be NULL, so the relationship to Role is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Membership ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Membership ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
MonthlyProration.teamBillingId may be NULL, so the relationship to TeamBilling is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
MonthlyProration.organizationBillingId may be NULL, so the relationship to OrganizationBilling is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
MonthlyProration has several nullable FKs (teamBillingId, organizationBillingId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE MonthlyProration ADD CONSTRAINT MonthlyProration_one_target CHECK ((teamBillingId IS NOT NULL)::int + (organizationBillingId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
periodStart stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN periodStart TYPE timestamptz;
TIMESTAMP without time zone
periodEnd stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN periodEnd TYPE timestamptz;
TIMESTAMP without time zone
subscriptionStart stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN subscriptionStart TYPE timestamptz;
TIMESTAMP without time zone
subscriptionEnd stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN subscriptionEnd TYPE timestamptz;
TIMESTAMP without time zone
chargedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN chargedAt TYPE timestamptz;
TIMESTAMP without time zone
failedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN failedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE MonthlyProration ALTER COLUMN updatedAt TYPE timestamptz;
Wide table (31 columns)
Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
Fix Look for column groups that always change together and split them out.
Nullable foreign key
OAuthClient.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OAuthClient ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
subscriptionStart stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationBilling ALTER COLUMN subscriptionStart TYPE timestamptz;
TIMESTAMP without time zone
subscriptionTrialEnd stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationBilling ALTER COLUMN subscriptionTrialEnd TYPE timestamptz;
TIMESTAMP without time zone
subscriptionEnd stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationBilling ALTER COLUMN subscriptionEnd TYPE timestamptz;
TIMESTAMP without time zone
highWaterMarkPeriodStart stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationBilling ALTER COLUMN highWaterMarkPeriodStart TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationBilling ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationBilling ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
OrganizationOnboarding.organizationId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationOnboarding ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OrganizationOnboarding ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
OutOfOfficeEntry.toUserId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
OutOfOfficeEntry.reasonId may be NULL, so the relationship to OutOfOfficeReason is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
OutOfOfficeEntry has several nullable FKs (toUserId, reasonId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE OutOfOfficeEntry ADD CONSTRAINT OutOfOfficeEntry_one_target CHECK ((toUserId IS NOT NULL)::int + (reasonId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
start stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OutOfOfficeEntry ALTER COLUMN start TYPE timestamptz;
TIMESTAMP without time zone
end stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OutOfOfficeEntry ALTER COLUMN end TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OutOfOfficeEntry ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE OutOfOfficeEntry ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
OutOfOfficeReason.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Payment.appId may be NULL, so the relationship to App is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
PendingRoutingTrace.formResponseId may be NULL, so the relationship to App_RoutingForms_FormResponse is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
PendingRoutingTrace.queuedFormResponseId may be NULL, so the relationship to App_RoutingForms_QueuedFormResponse is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
PendingRoutingTrace has several nullable FKs (formResponseId, queuedFormResponseId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE PendingRoutingTrace ADD CONSTRAINT PendingRoutingTrace_one_target CHECK ((formResponseId IS NOT NULL)::int + (queuedFormResponseId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE PendingRoutingTrace ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE PlatformAuthorizationToken ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
PlatformBilling.managerBillingId may be NULL, so the relationship to PlatformBilling is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE PlatformOAuthClient ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Profile ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Profile ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RateLimit ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RateLimit ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RefreshToken ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
expiresAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RefreshToken ALTER COLUMN expiresAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ReminderMail ALTER COLUMN createdAt TYPE timestamptz;
Isolated table
ReminderMail references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ResetPasswordRequest ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ResetPasswordRequest ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
expires stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE ResetPasswordRequest ALTER COLUMN expires TYPE timestamptz;
Isolated table
ResetPasswordRequest references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
F171Role teamIdfk-nullable Nullable foreign key
Role.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F390Role createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Role ALTER COLUMN createdAt TYPE timestamptz;
F391Role updatedAttimestamp-tz TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Role ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RolePermission ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
RoutingFormResponseDenormalized.bookingId may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
bookingCreatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RoutingFormResponseDenormalized ALTER COLUMN bookingCreatedAt TYPE timestamptz;
TIMESTAMP without time zone
bookingStartTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RoutingFormResponseDenormalized ALTER COLUMN bookingStartTime TYPE timestamptz;
TIMESTAMP without time zone
bookingEndTime stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RoutingFormResponseDenormalized ALTER COLUMN bookingEndTime TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RoutingFormResponseDenormalized ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
RoutingTrace.formResponseId may be NULL, so the relationship to App_RoutingForms_FormResponse is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
RoutingTrace.queuedFormResponseId may be NULL, so the relationship to App_RoutingForms_QueuedFormResponse is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
RoutingTrace.bookingUid may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
RoutingTrace.assignmentReasonId may be NULL, so the relationship to AssignmentReason is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F232RoutingTrace formResponseId, queuedFormResponseId, bookingUid, assignmentReasonIdexclusive-arc Either/or foreign keys without a CHECK
RoutingTrace has several nullable FKs (formResponseId, queuedFormResponseId, bookingUid, assignmentReasonId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE RoutingTrace ADD CONSTRAINT RoutingTrace_one_target CHECK ((formResponseId IS NOT NULL)::int + (queuedFormResponseId IS NOT NULL)::int + (bookingUid IS NOT NULL)::int + (assignmentReasonId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE RoutingTrace ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
SeatChangeLog.processedInProrationId may be NULL, so the relationship to MonthlyProration is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
SeatChangeLog.teamBillingId may be NULL, so the relationship to TeamBilling is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
SeatChangeLog.organizationBillingId may be NULL, so the relationship to OrganizationBilling is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F229SeatChangeLog processedInProrationId, teamBillingId, organizationBillingIdexclusive-arc Either/or foreign keys without a CHECK
SeatChangeLog has several nullable FKs (processedInProrationId, teamBillingId, organizationBillingId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE SeatChangeLog ADD CONSTRAINT SeatChangeLog_one_target CHECK ((processedInProrationId IS NOT NULL)::int + (teamBillingId IS NOT NULL)::int + (organizationBillingId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
changeDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SeatChangeLog ALTER COLUMN changeDate TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SeatChangeLog ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
emailVerified stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SecondaryEmail ALTER COLUMN emailVerified TYPE timestamptz;
Nullable foreign key
SelectedCalendar.credentialId may be NULL, so the relationship to Credential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
SelectedCalendar.delegationCredentialId may be NULL, so the relationship to DelegationCredential is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
SelectedCalendar.domainWideDelegationCredentialId may be NULL, so the relationship to DomainWideDelegation is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
SelectedCalendar.eventTypeId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F212SelectedCalendar credentialId, delegationCredentialId, domainWideDelegationCredentialId, eventTypeIdexclusive-arc Either/or foreign keys without a CHECK
SelectedCalendar has several nullable FKs (credentialId, delegationCredentialId, domainWideDelegationCredentialId, eventTypeId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE SelectedCalendar ADD CONSTRAINT SelectedCalendar_one_target CHECK ((credentialId IS NOT NULL)::int + (delegationCredentialId IS NOT NULL)::int + (domainWideDelegationCredentialId IS NOT NULL)::int + (eventTypeId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
channelExpiration stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN channelExpiration TYPE timestamptz;
TIMESTAMP without time zone
syncSubscribedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN syncSubscribedAt TYPE timestamptz;
TIMESTAMP without time zone
syncSubscribedErrorAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN syncSubscribedErrorAt TYPE timestamptz;
TIMESTAMP without time zone
syncedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN syncedAt TYPE timestamptz;
TIMESTAMP without time zone
syncErrorAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN syncErrorAt TYPE timestamptz;
TIMESTAMP without time zone
lastErrorAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedCalendar ALTER COLUMN lastErrorAt TYPE timestamptz;
Wide table (32 columns)
Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
Fix Look for column groups that always change together and split them out.
TIMESTAMP without time zone
slotUtcStartDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedSlots ALTER COLUMN slotUtcStartDate TYPE timestamptz;
TIMESTAMP without time zone
slotUtcEndDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedSlots ALTER COLUMN slotUtcEndDate TYPE timestamptz;
TIMESTAMP without time zone
releaseAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE SelectedSlots ALTER COLUMN releaseAt TYPE timestamptz;
Isolated table
SelectedSlots references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
TIMESTAMP without time zone
expires stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Session ALTER COLUMN expires TYPE timestamptz;
F349Task createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Task ALTER COLUMN createdAt TYPE timestamptz;
F350Task updatedAttimestamp-tz TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Task ALTER COLUMN updatedAt TYPE timestamptz;
F351Task scheduledAttimestamp-tz TIMESTAMP without time zone
scheduledAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Task ALTER COLUMN scheduledAt TYPE timestamptz;
F352Task succeededAttimestamp-tz TIMESTAMP without time zone
succeededAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Task ALTER COLUMN succeededAt TYPE timestamptz;
F353Task lastFailedAttemptAttimestamp-tz TIMESTAMP without time zone
lastFailedAttemptAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Task ALTER COLUMN lastFailedAttemptAt TYPE timestamptz;
Isolated table
Task references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
F094Team parentIdfk-nullable Nullable foreign key
Team.parentId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F095Team createdByOAuthClientIdfk-nullable Nullable foreign key
Team.createdByOAuthClientId may be NULL, so the relationship to PlatformOAuthClient is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F206Team parentId, createdByOAuthClientIdexclusive-arc Either/or foreign keys without a CHECK
Team has several nullable FKs (parentId, createdByOAuthClientId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Team ADD CONSTRAINT Team_one_target CHECK ((parentId IS NOT NULL)::int + (createdByOAuthClientId IS NOT NULL)::int = 1);
F257Team createdAttimestamp-tz TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Team ALTER COLUMN createdAt TYPE timestamptz;
Hub table: referenced by 40 tables
Any change to its key, its delete semantics, or its partitioning touches every dependent. Migrations on hub tables need the longest lock windows and the most careful rollout.
Fix Treat schema changes here as breaking changes with a written rollout plan.
Wide table (33 columns)
Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
Fix Look for column groups that always change together and split them out.
TIMESTAMP without time zone
subscriptionStart stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamBilling ALTER COLUMN subscriptionStart TYPE timestamptz;
TIMESTAMP without time zone
subscriptionTrialEnd stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamBilling ALTER COLUMN subscriptionTrialEnd TYPE timestamptz;
TIMESTAMP without time zone
subscriptionEnd stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamBilling ALTER COLUMN subscriptionEnd TYPE timestamptz;
TIMESTAMP without time zone
highWaterMarkPeriodStart stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamBilling ALTER COLUMN highWaterMarkPeriodStart TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamBilling ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamBilling ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
assignedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamFeatures ALTER COLUMN assignedAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TeamFeatures ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TempOrgRedirect ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TempOrgRedirect ALTER COLUMN updatedAt TYPE timestamptz;
Isolated table
TempOrgRedirect references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
TIMESTAMP without time zone
startDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TravelSchedule ALTER COLUMN startDate TYPE timestamptz;
TIMESTAMP without time zone
endDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE TravelSchedule ALTER COLUMN endDate TYPE timestamptz;
TIMESTAMP without time zone
assignedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE UserFeatures ALTER COLUMN assignedAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE UserFeatures ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
UserFilterSegmentPreference.segmentId may be NULL, so the relationship to FilterSegment is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE UserFilterSegmentPreference ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE UserFilterSegmentPreference ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE UserHolidaySettings ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE UserHolidaySettings ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
VerificationToken.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
VerificationToken.secondaryEmailId may be NULL, so the relationship to SecondaryEmail is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
VerificationToken has several nullable FKs (teamId, secondaryEmailId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE VerificationToken ADD CONSTRAINT VerificationToken_one_target CHECK ((teamId IS NOT NULL)::int + (secondaryEmailId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
expires stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE VerificationToken ALTER COLUMN expires TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE VerificationToken ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE VerificationToken ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
VerifiedEmail.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
VerifiedEmail.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
VerifiedEmail has several nullable FKs (userId, teamId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE VerifiedEmail ADD CONSTRAINT VerifiedEmail_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int = 1);
Nullable foreign key
VerifiedNumber.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
VerifiedNumber.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
VerifiedNumber has several nullable FKs (userId, teamId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE VerifiedNumber ADD CONSTRAINT VerifiedNumber_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
joinedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE VideoCallGuest ALTER COLUMN joinedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE VideoCallGuest ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE VideoCallGuest ALTER COLUMN updatedAt TYPE timestamptz;
Isolated table
VideoCallGuest references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
TIMESTAMP without time zone
lastUpdatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Watchlist ALTER COLUMN lastUpdatedAt TYPE timestamptz;
Nullable foreign key
WatchlistAudit.watchlistId may be NULL, so the relationship to Watchlist is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
TIMESTAMP without time zone
changedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WatchlistAudit ALTER COLUMN changedAt TYPE timestamptz;
TIMESTAMP without time zone
timestamp stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WatchlistEventAudit ALTER COLUMN timestamp TYPE timestamptz;
Isolated table
WatchlistEventAudit references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
Nullable foreign key
Webhook.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Webhook.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Webhook.eventTypeId may be NULL, so the relationship to EventType is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F123Webhook platformOAuthClientIdfk-nullable Nullable foreign key
Webhook.platformOAuthClientId may be NULL, so the relationship to PlatformOAuthClient is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Webhook.appId may be NULL, so the relationship to App is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F213Webhook userId, teamId, eventTypeId, platformOAuthClientId, appIdexclusive-arc Either/or foreign keys without a CHECK
Webhook has several nullable FKs (userId, teamId, eventTypeId, platformOAuthClientId, appId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Webhook ADD CONSTRAINT Webhook_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int + (eventTypeId IS NOT NULL)::int + (platformOAuthClientId IS NOT NULL)::int + (appId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE Webhook ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
WebhookScheduledTriggers.webhookId may be NULL, so the relationship to Webhook is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
WebhookScheduledTriggers.bookingId may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
WebhookScheduledTriggers has several nullable FKs (webhookId, bookingId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE WebhookScheduledTriggers ADD CONSTRAINT WebhookScheduledTriggers_one_target CHECK ((webhookId IS NOT NULL)::int + (bookingId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
startAfter stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WebhookScheduledTriggers ALTER COLUMN startAfter TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WebhookScheduledTriggers ALTER COLUMN createdAt TYPE timestamptz;
Nullable foreign key
Workflow.userId may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
Workflow.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F217Workflow userId, teamIdexclusive-arc Either/or foreign keys without a CHECK
Workflow has several nullable FKs (userId, teamId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE Workflow ADD CONSTRAINT Workflow_one_target CHECK ((userId IS NOT NULL)::int + (teamId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkflowOptOutContact ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkflowOptOutContact ALTER COLUMN updatedAt TYPE timestamptz;
Isolated table
WorkflowOptOutContact references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
Nullable foreign key
WorkflowReminder.bookingUid may be NULL, so the relationship to Booking is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
WorkflowReminder.workflowStepId may be NULL, so the relationship to WorkflowStep is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
WorkflowReminder has several nullable FKs (bookingUid, workflowStepId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE WorkflowReminder ADD CONSTRAINT WorkflowReminder_one_target CHECK ((bookingUid IS NOT NULL)::int + (workflowStepId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
scheduledDate stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkflowReminder ALTER COLUMN scheduledDate TYPE timestamptz;
Nullable foreign key
WorkflowStep.agentId may be NULL, so the relationship to Agent is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
WorkflowStep.inboundAgentId may be NULL, so the relationship to Agent is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
WorkflowStep has several nullable FKs (agentId, inboundAgentId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE WorkflowStep ADD CONSTRAINT WorkflowStep_one_target CHECK ((agentId IS NOT NULL)::int + (inboundAgentId IS NOT NULL)::int = 1);
TIMESTAMP without time zone
verifiedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkflowStep ALTER COLUMN verifiedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkflowStepTranslation ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkflowStepTranslation ALTER COLUMN updatedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkspacePlatform ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WorkspacePlatform ALTER COLUMN updatedAt TYPE timestamptz;
Nullable foreign key
WrongAssignmentReport.reportedById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
WrongAssignmentReport.teamId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
WrongAssignmentReport.routingFormId may be NULL, so the relationship to App_RoutingForms_Form is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Nullable foreign key
WrongAssignmentReport.reviewedById may be NULL, so the relationship to users is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
Either/or foreign keys without a CHECK
WrongAssignmentReport has several nullable FKs (reportedById, teamId, routingFormId, reviewedById) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE WrongAssignmentReport ADD CONSTRAINT WrongAssignmentReport_one_target CHECK ((reportedById IS NOT NULL)::int + (teamId IS NOT NULL)::int + (routingFormId IS NOT NULL)::int + (reviewedById IS NOT NULL)::int = 1);
TIMESTAMP without time zone
reviewedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WrongAssignmentReport ALTER COLUMN reviewedAt TYPE timestamptz;
TIMESTAMP without time zone
createdAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WrongAssignmentReport ALTER COLUMN createdAt TYPE timestamptz;
TIMESTAMP without time zone
updatedAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE WrongAssignmentReport ALTER COLUMN updatedAt TYPE timestamptz;
Isolated table
avatars references nothing and nothing references it. Either it is a staging/log table (fine, say so), or it is dead, or it is the seed of a second data model growing beside the first.
Fix Document its purpose or drop it.
F092users organizationIdfk-nullable Nullable foreign key
users.organizationId may be NULL, so the relationship to Team is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F093users movedToProfileIdfk-nullable Nullable foreign key
users.movedToProfileId may be NULL, so the relationship to Profile is optional. That is legitimate (e.g. approved_by before approval) but often a modelling shrug: a row with no owner, or two nullable FKs that are secretly an either/or.
Fix Confirm the optionality is a domain rule; document it in the column comment.
F205users organizationId, movedToProfileIdexclusive-arc Either/or foreign keys without a CHECK
users has several nullable FKs (organizationId, movedToProfileId) that look like an exclusive arc — a row should point at exactly one of them. Nothing stops zero or both.
Fix Add a CHECK that exactly one is non-null, or restructure with a supertype.
ALTER TABLE users ADD CONSTRAINT users_one_target CHECK ((organizationId IS NOT NULL)::int + (movedToProfileId IS NOT NULL)::int = 1);
F251users emailVerifiedtimestamp-tz TIMESTAMP without time zone
emailVerified stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE users ALTER COLUMN emailVerified TYPE timestamptz;
F252users createdtimestamp-tz TIMESTAMP without time zone
created stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE users ALTER COLUMN created TYPE timestamptz;
F253users trialEndsAttimestamp-tz TIMESTAMP without time zone
trialEndsAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE users ALTER COLUMN trialEndsAt TYPE timestamptz;
F254users lastActiveAttimestamp-tz TIMESTAMP without time zone
lastActiveAt stores wall-clock time with no zone. It reads back differently depending on the session's TimeZone, and DST transitions produce ambiguous values.
Fix Use TIMESTAMPTZ.
ALTER TABLE users ALTER COLUMN lastActiveAt TYPE timestamptz;
Hub table: referenced by 53 tables
Any change to its key, its delete semantics, or its partitioning touches every dependent. Migrations on hub tables need the longest lock windows and the most careful rollout.
Fix Treat schema changes here as breaking changes with a written rollout plan.
Wide table (47 columns)
Tables this wide usually hide several entities (or a JSON column that wants to be one). Every row update rewrites the whole tuple; TOAST kicks in; indexes bloat.
Fix Look for column groups that always change together and split them out.