| name | schema-consistency-checker |
| description | Audits database schemas for naming conventions, type consistency, nullability patterns, and missing constraints. Provides violations report with recommended fixes. Use for "schema validation", "database linting", "schema standards", or "consistency checks". |
Schema Consistency Checker
Enforce schema consistency and best practices across your database.
Consistency Rules
1. Naming Conventions
export const NAMING_RULES = {
tables: {
pattern: /^[A-Z][a-zA-Z0-9]*$/,
examples: ["User", "OrderItem", "ProductCategory"],
},
columns: {
pattern: /^[a-z][a-zA-Z0-9]*$/,
examples: ["id", "firstName", "createdAt"],
},
indexes: {
pattern: /^idx_[a-z_]+$/,
examples: ["idx_users_email", "idx_orders_user_id"],
},
foreignKeys: {
pattern: /^fk_[a-z_]+$/,
examples: ["fk_orders_user_id", "fk_products_category_id"],
},
constraints: {
pattern: /^(chk|unq)_[a-z_]+$/,
examples: ["chk_age_positive", "unq_users_email"],
},
};
2. Type Consistency
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id TEXT
);
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email TEXT
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT REFERENCES users(id)
);
3. Nullability Patterns
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email TEXT,
name TEXT,
phone TEXT NULL,
created_at TIMESTAMP
);
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
phone TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
4. Missing Constraints
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status TEXT,
total DECIMAL(10,2),
created_at TIMESTAMP
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'delivered')),
total DECIMAL(10,2) NOT NULL CHECK (total >= 0),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Audit Script
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
interface Violation {
severity: "error" | "warning" | "info";
category: string;
table: string;
column?: string;
message: string;
recommendation: string;
}
async function auditSchema(): Promise<Violation[]> {
const violations: Violation[] = [];
const tables = await prisma.$queryRaw<any[]>`
SELECT
table_name,
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position
`;
tables.forEach((col) => {
if (!/^[A-Z][a-zA-Z0-9]*$/.test(col.table_name)) {
violations.({
: ,
: ,
: col.,
: ,
: ,
});
}
(!.(col.)) {
violations.({
: ,
: ,
: col.,
: col.,
: ,
: ,
});
}
});
criticalFields = [
,
,
,
,
,
];
tables.( {
(
criticalFields.( col..(f)) &&
col. ===
) {
violations.({
: ,
: ,
: col.,
: col.,
: ,
: ,
});
}
});
idTypes = <, >();
tables.( {
(col. === ) {
idTypes.(col., col.);
}
});
primaryIdType = .(idTypes.())[];
idTypes.( {
( !== primaryIdType) {
violations.({
: ,
: ,
table,
: ,
: ,
: ,
});
}
});
foreignKeys = prisma.<[]>;
indexes = prisma.<[]>;
foreignKeys.( {
hasIndex = indexes.(
idx. === fk. && idx..(fk.)
);
(!hasIndex) {
violations.({
: ,
: ,
: fk.,
: fk.,
: ,
: ,
});
}
});
tablesGrouped = tables.( {
(!acc[col.]) acc[col.] = [];
acc[col.].(col.);
acc;
}, {} <, []>);
.(tablesGrouped).( {
(!columns.()) {
violations.({
: ,
: ,
table,
: ,
: ,
});
}
(!columns.() && !columns.()) {
violations.({
: ,
: ,
table,
: ,
: ,
});
}
});
violations;
}
() {
violations = ();
.();
.();
grouped = violations.( {
(!acc[v.]) acc[v.] = [];
acc[v.].(v);
acc;
}, {} <, []>);
([, , ] ).( {
items = grouped[severity] || [];
(items. === ) ;
.(
);
items.( {
.(
);
.();
.();
});
});
process.(grouped.?. > ? : );
}
();
Recommended Schema Standards
// schema.prisma with best practices
model User {
// 1. ID: Consistent type (Int or String/cuid)
id Int @id @default(autoincrement())
// 2. Critical fields: NOT NULL
email String @unique
name String
// 3. Optional fields: Clearly nullable
phone String?
bio String?
// 4. Audit timestamps: Always include
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 5. Relations: Proper foreign keys
orders Order[]
// 6. Indexes: On frequently queried fields
@@index([email])
@@index([createdAt])
}
model Order {
id Int @id @default(autoincrement())
// Foreign key with clear naming
userId Int
user User @relation(fields: [userId], references: [id])
// Enum for status (type safety)
status OrderStatus @default(PENDING)
// Decimal for money
total Decimal @db.Decimal(10, 2)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Indexes on foreign keys
@@index([userId])
@@index([status])
@@index([createdAt])
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
Auto-fix Migrations
async function generateFixMigrations(violations: Violation[]) {
const migrations: string[] = [];
violations.forEach((v) => {
if (v.category === "nullability" && v.column) {
migrations.push(
`ALTER TABLE "${v.table}" ALTER COLUMN "${v.column}" SET NOT NULL;`
);
}
if (
v.category === "performance" &&
v.recommendation.startsWith("CREATE INDEX")
) {
migrations.push(v.recommendation + ";");
}
if (v.category === "audit" && v.message.includes("created_at")) {
migrations.push(
`ALTER TABLE "${v.table}" ADD COLUMN "created_at" TIMESTAMP NOT NULL DEFAULT NOW();`
);
}
});
console.log("-- Auto-generated fixes\n");
migrations.forEach((m) => console.log(m));
}
Best Practices
- Run regularly: Weekly schema audits
- Enforce in CI: Fail builds on errors
- Document standards: Team agreement on conventions
- Gradual adoption: Fix incrementally
- Use enums: For status fields
- Always timestamp: created_at and updated_at
- Index foreign keys: Performance best practice
Output Checklist