Skip to content

Commit

Permalink
making names for check constraints optional
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonyalayo committed Feb 4, 2025
1 parent 82aa92e commit 702acb1
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 42 deletions.
9 changes: 7 additions & 2 deletions drizzle-kit/src/jsonStatements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,11 @@ export const prepareDomainJson = (
type: 'create' | 'alter' | 'drop',
action?: JsonAlterDomainStatement['action'],
): JsonDomainStatement => {
let checkConstraints;
if (domain.checkConstraints) {
checkConstraints = Object.values(domain.checkConstraints);
}

if (type === 'create') {
return {
type: 'create_domain',
Expand All @@ -1061,7 +1066,7 @@ export const prepareDomainJson = (
notNull: domain.notNull,
defaultValue: domain.defaultValue,
baseType: domain.baseType,
checkConstraints: Object.values(domain.checkConstraints),
checkConstraints,
};
} else if (type === 'drop') {
return {
Expand All @@ -1078,7 +1083,7 @@ export const prepareDomainJson = (
name: domain.name,
schema: domain.schema,
defaultValue: domain.defaultValue,
checkConstraints: Object.values(domain.checkConstraints),
checkConstraints,
};
};

Expand Down
8 changes: 4 additions & 4 deletions drizzle-kit/src/serializer/pgSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const tableV2 = object({
}).strict();

const checkConstraint = object({
name: string(),
name: string().optional(),
value: string(),
}).strict();

Expand All @@ -39,7 +39,7 @@ const domainSchema = object({
baseType: string(),
notNull: boolean().optional(),
defaultValue: string().optional(),
checkConstraints: record(string(), checkConstraint).default({}),
checkConstraints: record(string(), checkConstraint).optional(),
}).strict();

const enumSchemaV1 = object({
Expand Down Expand Up @@ -473,7 +473,7 @@ const domainSquashed = object({
baseType: string(),
notNull: boolean().optional(),
defaultValue: string().optional(),
checkConstraints: record(string(), string()),
checkConstraints: record(string(), string()).optional(),
}).strict();

const tableSquashed = object({
Expand Down Expand Up @@ -895,7 +895,7 @@ export const squashPgScheme = (

const mappedDomains = mapValues(json.domains, (domain) => {
const squashedDomainChecks = mapValues(
domain.checkConstraints,
domain.checkConstraints ?? {},
(check) => PgSquasher.squashCheck(check),
);

Expand Down
47 changes: 27 additions & 20 deletions drizzle-kit/src/serializer/pgSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,18 @@ export const generatePgSnapshot = (
};
});

checks.forEach((check) => {
const checkName = check.name;
checks.forEach((check, index) => {
const tableKey = `"${schema ?? 'public'}"."${tableName}"`;

// you can have multiple unnamed checks per table (using the default above)
let defaultCheckName = `${tableName}_check`
let checkName = check.name ?? defaultCheckName;
if (checksInTable[tableKey]?.includes(checkName) && checkName == defaultCheckName) {
checkName += `_${index}`;
}

if (typeof checksInTable[`"${schema ?? 'public'}"."${tableName}"`] !== 'undefined') {
if (checksInTable[`"${schema ?? 'public'}"."${tableName}"`].includes(check.name)) {
if (checksInTable[`"${schema ?? 'public'}"."${tableName}"`].includes(checkName)) {
console.log(
`\n${
withStyle.errorWarning(
Expand All @@ -564,11 +571,11 @@ export const generatePgSnapshot = (
}
checksInTable[`"${schema ?? 'public'}"."${tableName}"`].push(checkName);
} else {
checksInTable[`"${schema ?? 'public'}"."${tableName}"`] = [check.name];
checksInTable[`"${schema ?? 'public'}"."${tableName}"`] = [checkName];
}

checksObject[checkName] = {
name: checkName,
name: check.name ?? '', // don't squash and include the default name
value: dialect.sqlToQuery(check.value).sql,
};
});
Expand Down Expand Up @@ -875,24 +882,24 @@ export const generatePgSnapshot = (
// Process check constraints similar to tables
const checksObject: Record<string, CheckConstraint> = {};

obj.checkConstraints?.forEach((checkConstraint) => {
const checkName = checkConstraint.name;
const checkValue = dialect.sqlToQuery(checkConstraint.value).sql;

obj.checkConstraints?.forEach((checkConstraint, index) => {
// Validate unique constraint names within domain
const domainKey = `"${obj.schema ?? 'public'}"."${obj.domainName}"`;
if (checksInTable[domainKey]?.includes(checkName)) {
console.error(`Duplicate check constraint name ${checkName} in domain ${domainKey}`);
process.exit(1);

// you can have multiple unnamed checks per domain (using the default above)
let defaultCheckName = `${obj.domainName}_check`
let checkName = checkConstraint.name ?? defaultCheckName;
if (checksInTable[domainKey]?.includes(checkName) && checkName == defaultCheckName) {
checkName += `_${index}`;
}

checksInTable[domainKey] = checksInTable[domainKey]
? [...checksInTable[domainKey], checkName]
: [checkName];

checksObject[checkName] = {
name: checkName,
value: checkValue,
name: checkConstraint.name ?? '', // don't squash and include the default name
value: dialect.sqlToQuery(checkConstraint.value).sql,
};
});

Expand Down Expand Up @@ -1138,20 +1145,22 @@ WHERE
const schemaName = domain.domain_schema || 'public';
const key = `${schemaName}.${domain.domain_name}`;

// Initialize the domain if it doesn't exist
if (!domainsToReturn[key]) {
domainsToReturn[key] = {
name: domain.domain_name,
schema: schemaName,
baseType: domain.base_type,
notNull: domain.not_null,
defaultValue: domain.default_value,
checkConstraints: {}, // Now using checkConstraints (plural) as a Record
};
}

// Add the check constraint if present in this row
if (domain.constraint_name && domain.domain_constraint) {
if (!domainsToReturn[key].checkConstraints) {
domainsToReturn[key].checkConstraints = {};
}

domainsToReturn[key].checkConstraints[domain.constraint_name] = {
name: domain.constraint_name,
value: domain.domain_constraint,
Expand Down Expand Up @@ -1330,7 +1339,7 @@ WHERE
const tableChecks = await db.query(`SELECT
tc.constraint_name,
tc.constraint_type,
pg_get_constraintdef(con.oid) AS constraint_definition
pg_get_expr(con.conbin, con.conrelid) AS check_expression
FROM
information_schema.table_constraints AS tc
JOIN pg_constraint AS con
Expand Down Expand Up @@ -1457,11 +1466,9 @@ WHERE
for (const checks of tableChecks) {
// CHECK (((email)::text <> '[email protected]'::text))
// Where (email) is column in table
let checkValue: string = checks.constraint_definition;
const checkValue: string = checks.check_expression;
const constraintName: string = checks.constraint_name;

checkValue = checkValue.replace(/^CHECK\s*\(\(/, '').replace(/\)\)\s*$/, '');

checkConstraints[constraintName] = {
name: constraintName,
value: checkValue,
Expand Down
4 changes: 2 additions & 2 deletions drizzle-kit/src/snapshotsDiffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ const domainSchema = object({
baseType: string(),
notNull: boolean().optional(),
defaultValue: string().optional(),
checkConstraints: record(string(), string()).default({}),
checkConstraints: record(string(), string()).optional(),
}).strict();

const changedDomainSchema = object({
Expand All @@ -259,7 +259,7 @@ const changedDomainSchema = object({
baseType: string(),
notNull: boolean().optional(),
defaultValue: string().optional(),
checkConstraints: record(string(), string()).default({}),
checkConstraints: record(string(), string()).optional(),
}).strict();

const enumSchema = object({
Expand Down
21 changes: 18 additions & 3 deletions drizzle-kit/src/sqlgenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,12 @@ class PgCreateTableConvertor extends Convertor {
for (const checkConstraint of checkConstraints) {
statement += ',\n';
const unsquashedCheck = PgSquasher.unsquashCheck(checkConstraint);
statement += `\tCONSTRAINT "${unsquashedCheck.name}" CHECK (${unsquashedCheck.value})`;

if(unsquashedCheck.name) {
statement += `\tCONSTRAINT "${unsquashedCheck.name}" CHECK (${unsquashedCheck.value})`;
} else {
statement += `\tCHECK (${unsquashedCheck.value})`;
}
}
}

Expand Down Expand Up @@ -1398,7 +1403,12 @@ class CreateDomainConvertor extends DomainConvertor {
if (checkConstraints && checkConstraints.length > 0) {
for (const checkConstraint of checkConstraints) {
const unsquashedCheck = PgSquasher.unsquashCheck(checkConstraint);
statement += ` CONSTRAINT ${unsquashedCheck.name} CHECK (${unsquashedCheck.value})`;

if (unsquashedCheck.name) {
statement += ` CONSTRAINT ${unsquashedCheck.name} CHECK (${unsquashedCheck.value})`;
} else {
statement += ` CHECK (${unsquashedCheck.value})`;
}
}
}

Expand All @@ -1422,7 +1432,12 @@ class AlterDomainConvertor extends DomainConvertor {
if (checkConstraints && checkConstraints.length > 0) {
for (const checkConstraint of checkConstraints) {
const unsquashedCheck = PgSquasher.unsquashCheck(checkConstraint);
statement += ` ADD CONSTRAINT ${unsquashedCheck.name} CHECK (${unsquashedCheck.value})`;

if (unsquashedCheck.name) {
statement += ` ADD CONSTRAINT ${unsquashedCheck.name} CHECK (${unsquashedCheck.value})`;
} else {
statement += ` ADD CHECK (${unsquashedCheck.value})`;
}
}
}
break;
Expand Down
47 changes: 47 additions & 0 deletions drizzle-kit/tests/pg-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,50 @@ test('create checks with same names', async (t) => {

await expect(diffTestSchemas({}, to, [])).rejects.toThrowError();
});

test('create unnamed checks', async (t) => {
const to = {
users: pgTable('users', {
id: serial('id').primaryKey(),
age: integer('age'),
}, (table) => ({
checkConstraint: check(sql`${table.age} > 21`),
})),
};

const { sqlStatements, statements } = await diffTestSchemas({}, to, []);

expect(statements.length).toBe(1);
expect(statements[0]).toStrictEqual({
type: 'create_table',
tableName: 'users',
schema: '',
columns: [
{
name: 'id',
type: 'serial',
notNull: true,
primaryKey: true,
},
{
name: 'age',
type: 'integer',
notNull: false,
primaryKey: false,
},
],
compositePKs: [],
checkConstraints: [';"users"."age" > 21'],
compositePkName: '',
uniqueConstraints: [],
isRLSEnabled: false,
policies: [],
} as JsonCreateTableStatement);

expect(sqlStatements.length).toBe(1);
expect(sqlStatements[0]).toBe(`CREATE TABLE "users" (
\t"id" serial PRIMARY KEY NOT NULL,
\t"age" integer,
\tCHECK ("users"."age" > 21)
);\n`);
});
27 changes: 27 additions & 0 deletions drizzle-kit/tests/pg-domains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,30 @@ test('domains #11 alter domain to drop default value', async () => {
defaultValue: undefined,
});
});

test('domains #12 create domain with unnamed constraint', async () => {
const to = {
domain: pgDomain('domain', 'text', {
checkConstraints: [check(sql`VALUE ~ '^[A-Za-z]+$'`)],
}),
};

const { statements, sqlStatements } = await diffTestSchemas({}, to, []);

expect(sqlStatements.length).toBe(1);
expect(sqlStatements[0]).toBe(
`CREATE DOMAIN "public"."domain" AS text CHECK (VALUE ~ '^[A-Za-z]+$');`,
);
expect(statements.length).toBe(1);
expect(statements[0]).toStrictEqual({
type: 'create_domain',
name: 'domain',
schema: 'public',
baseType: 'text',
notNull: false,
defaultValue: undefined,
checkConstraints: [
";VALUE ~ '^[A-Za-z]+$'",
],
});
});
14 changes: 10 additions & 4 deletions drizzle-orm/src/pg-core/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class CheckBuilder {

protected brand!: 'PgConstraintBuilder';

constructor(public name: string, public value: SQL) {}
constructor(public name: string | undefined, public value: SQL) {}

/** @internal */
build(table: PgTable): Check {
Expand All @@ -18,7 +18,7 @@ export class CheckBuilder {
export class Check {
static readonly [entityKind]: string = 'PgCheck';

readonly name: string;
readonly name?: string;
readonly value: SQL;

constructor(public table: PgTable, builder: CheckBuilder) {
Expand All @@ -27,6 +27,12 @@ export class Check {
}
}

export function check(name: string, value: SQL): CheckBuilder {
return new CheckBuilder(name, value);
export function check(value: SQL): CheckBuilder;
export function check(name: string, value: SQL): CheckBuilder;
export function check(nameOrValue: string | SQL, maybeValue?: SQL): CheckBuilder {
if (maybeValue === undefined) {
// Only one argument: treat it as the SQL value.
return new CheckBuilder(undefined, nameOrValue as SQL);
}
return new CheckBuilder(nameOrValue as string, maybeValue);
}
14 changes: 7 additions & 7 deletions drizzle-zod/tests/pg-checks.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {type Equal, sql} from 'drizzle-orm';
import {check, integer, pgDomain, pgTable, serial, text,} from 'drizzle-orm/pg-core';
import {test} from 'vitest';
import {z} from 'zod';
import {CONSTANTS} from '~/constants.ts';
import {createSelectSchema} from '../src';
import {Expect, expectSchemaShape} from './utils.ts';
import { type Equal, sql } from 'drizzle-orm';
import { check, integer, pgDomain, pgTable, serial, text } from 'drizzle-orm/pg-core';
import { test } from 'vitest';
import { z } from 'zod';
import { CONSTANTS } from '~/constants.ts';
import { createSelectSchema } from '../src';
import { Expect, expectSchemaShape } from './utils.ts';

// TODO think about what to do with the existing filters being added when check constraints are involved
const integerSchema = z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int();
Expand Down

0 comments on commit 702acb1

Please sign in to comment.