Files
drizzle-team--drizzle-orm/drizzle-kit/tests/introspect/pg.test.ts
T
2026-07-13 12:10:05 +08:00

929 lines
23 KiB
TypeScript

import { PGlite } from '@electric-sql/pglite';
import { SQL, sql } from 'drizzle-orm';
import {
bigint,
bigserial,
boolean,
char,
check,
cidr,
date,
doublePrecision,
index,
inet,
integer,
interval,
json,
jsonb,
macaddr,
macaddr8,
numeric,
pgEnum,
pgMaterializedView,
pgPolicy,
pgRole,
pgSchema,
pgTable,
pgView,
real,
serial,
smallint,
smallserial,
text,
time,
timestamp,
uuid,
varchar,
} from 'drizzle-orm/pg-core';
import fs from 'fs';
import { introspectPgToFile } from 'tests/schemaDiffer';
import { expect, test } from 'vitest';
if (!fs.existsSync('tests/introspect/postgres')) {
fs.mkdirSync('tests/introspect/postgres');
}
test('basic introspect test', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').notNull(),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic identity always test', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedAlwaysAsIdentity(),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-identity-always-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic identity by default test', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedByDefaultAsIdentity(),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-identity-default-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic index test', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
firstName: text('first_name'),
lastName: text('last_name'),
data: jsonb('data'),
}, (table) => ({
singleColumn: index('single_column').on(table.firstName),
multiColumn: index('multi_column').on(table.firstName, table.lastName),
singleExpression: index('single_expression').on(sql`lower(${table.firstName})`),
multiExpression: index('multi_expression').on(sql`lower(${table.firstName})`, sql`lower(${table.lastName})`),
expressionWithComma: index('expression_with_comma').on(
sql`(lower(${table.firstName}) || ', '::text || lower(${table.lastName}))`,
),
expressionWithDoubleQuote: index('expression_with_double_quote').on(sql`('"'::text || ${table.firstName})`),
expressionWithJsonbOperator: index('expression_with_jsonb_operator').on(
sql`(${table.data} #>> '{a,b,1}'::text[])`,
),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-index-introspect',
);
expect(statements.length).toBe(10);
expect(sqlStatements.length).toBe(10);
});
test('identity always test: few params', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedAlwaysAsIdentity({
startWith: 100,
name: 'custom_name',
}),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'identity-always-few-params-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('identity by default test: few params', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedByDefaultAsIdentity({
maxValue: 10000,
name: 'custom_name',
}),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'identity-default-few-params-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('identity always test: all params', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedAlwaysAsIdentity({
startWith: 10,
increment: 4,
minValue: 10,
maxValue: 10000,
cache: 100,
cycle: true,
}),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'identity-always-all-params-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('identity by default test: all params', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedByDefaultAsIdentity({
startWith: 10,
increment: 4,
minValue: 10,
maxValue: 10000,
cache: 100,
cycle: true,
}),
email: text('email'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'identity-default-all-params-introspect',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('generated column: link to another column', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').generatedAlwaysAsIdentity(),
email: text('email'),
generatedEmail: text('generatedEmail').generatedAlwaysAs(
(): SQL => sql`email`,
),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'generated-link-column',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('instrospect all column types', async () => {
const client = new PGlite();
const myEnum = pgEnum('my_enum', ['a', 'b', 'c']);
const schema = {
enum_: myEnum,
// NOTE: Types from extensions aren't tested due to PGlite not supporting at the moment
columns: pgTable('columns', {
enum: myEnum('my_enum').default('a'),
smallint: smallint('smallint').default(10),
integer: integer('integer').default(10),
numeric: numeric('numeric', { precision: 3, scale: 1 }).default('99.9'),
numeric2: numeric('numeric2', { precision: 1, scale: 1 }).default('99.9'),
numeric3: numeric('numeric3').default('99.9'),
bigint: bigint('bigint', { mode: 'number' }).default(100),
boolean: boolean('boolean').default(true),
text: text('test').default('abc'),
varchar: varchar('varchar', { length: 25 }).default('abc'),
char: char('char', { length: 3 }).default('abc'),
serial: serial('serial'),
bigserial: bigserial('bigserial', { mode: 'number' }),
smallserial: smallserial('smallserial'),
doublePrecision: doublePrecision('doublePrecision').default(100),
real: real('real').default(100),
json: json('json').$type<{ attr: string }>().default({ attr: 'value' }),
jsonb: jsonb('jsonb').$type<{ attr: string }>().default({ attr: 'value' }),
time1: time('time1').default('00:00:00'),
time2: time('time2').defaultNow(),
timestamp1: timestamp('timestamp1', { withTimezone: true, precision: 6 }).default(new Date()),
timestamp2: timestamp('timestamp2', { withTimezone: true, precision: 6 }).defaultNow(),
timestamp3: timestamp('timestamp3', { withTimezone: true, precision: 6 }).default(
sql`timezone('utc'::text, now())`,
),
date1: date('date1').default('2024-01-01'),
date2: date('date2').defaultNow(),
date3: date('date3').default(sql`CURRENT_TIMESTAMP`),
uuid1: uuid('uuid1').default('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'),
uuid2: uuid('uuid2').defaultRandom(),
inet: inet('inet').default('127.0.0.1'),
cidr: cidr('cidr').default('127.0.0.1/32'),
macaddr: macaddr('macaddr').default('00:00:00:00:00:00'),
macaddr8: macaddr8('macaddr8').default('00:00:00:ff:fe:00:00:00'),
interval: interval('interval').default('1 day 01:00:00'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-all-columns-types',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('instrospect all column array types', async () => {
const client = new PGlite();
const myEnum = pgEnum('my_enum', ['a', 'b', 'c']);
const schema = {
enum_: myEnum,
// NOTE: Types from extensions aren't tested due to PGlite not supporting at the moment
columns: pgTable('columns', {
enum: myEnum('my_enum').array().default(['a', 'b']),
smallint: smallint('smallint').array().default([10, 20]),
integer: integer('integer').array().default([10, 20]),
numeric: numeric('numeric', { precision: 3, scale: 1 }).array().default(['99.9', '88.8']),
bigint: bigint('bigint', { mode: 'number' }).array().default([100, 200]),
boolean: boolean('boolean').array().default([true, false]),
text: text('test').array().default(['abc', 'def']),
varchar: varchar('varchar', { length: 25 }).array().default(['abc', 'def']),
char: char('char', { length: 3 }).array().default(['abc', 'def']),
doublePrecision: doublePrecision('doublePrecision').array().default([100, 200]),
real: real('real').array().default([100, 200]),
json: json('json').$type<{ attr: string }>().array().default([{ attr: 'value1' }, { attr: 'value2' }]),
jsonb: jsonb('jsonb').$type<{ attr: string }>().array().default([{ attr: 'value1' }, { attr: 'value2' }]),
time: time('time').array().default(['00:00:00', '01:00:00']),
timestamp: timestamp('timestamp', { withTimezone: true, precision: 6 })
.array()
.default([new Date(), new Date()]),
date: date('date').array().default(['2024-01-01', '2024-01-02']),
uuid: uuid('uuid').array().default([
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12',
]),
inet: inet('inet').array().default(['127.0.0.1', '127.0.0.2']),
cidr: cidr('cidr').array().default(['127.0.0.1/32', '127.0.0.2/32']),
macaddr: macaddr('macaddr').array().default(['00:00:00:00:00:00', '00:00:00:00:00:01']),
macaddr8: macaddr8('macaddr8').array().default(['00:00:00:ff:fe:00:00:00', '00:00:00:ff:fe:00:00:01']),
interval: interval('interval').array().default(['1 day 01:00:00', '1 day 02:00:00']),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-all-columns-array-types',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect columns with name with non-alphanumeric characters', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
'not:allowed': integer('not:allowed'),
'nuh--uh': integer('nuh-uh'),
'1_nope': integer('1_nope'),
valid: integer('valid'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-column-with-name-with-non-alphanumeric-characters',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect enum from different schema', async () => {
const client = new PGlite();
const schema2 = pgSchema('schema2');
const myEnumInSchema2 = schema2.enum('my_enum', ['a', 'b', 'c']);
const schema = {
schema2,
myEnumInSchema2,
users: pgTable('users', {
col: myEnumInSchema2('col'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-enum-from-different-schema',
['public', 'schema2'],
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect enum with same names across different schema', async () => {
const client = new PGlite();
const schema2 = pgSchema('schema2');
const myEnumInSchema2 = schema2.enum('my_enum', ['a', 'b', 'c']);
const myEnum = pgEnum('my_enum', ['a', 'b', 'c']);
const schema = {
schema2,
myEnumInSchema2,
myEnum,
users: pgTable('users', {
col1: myEnumInSchema2('col1'),
col2: myEnum('col2'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-enum-with-same-names-across-different-schema',
['public', 'schema2'],
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect enum with similar name to native type', async () => {
const client = new PGlite();
const timeLeft = pgEnum('time_left', ['short', 'medium', 'long']);
const schema = {
timeLeft,
auction: pgTable('auction', {
col: timeLeft('col1'),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-enum-with-similar-name-to-native-type',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('instrospect strings with single quotes', async () => {
const client = new PGlite();
const myEnum = pgEnum('my_enum', ['escape\'s quotes " ']);
const schema = {
enum_: myEnum,
columns: pgTable('columns', {
enum: myEnum('my_enum').default('escape\'s quotes " '),
text: text('text').default('escape\'s quotes " '),
varchar: varchar('varchar').default('escape\'s quotes " '),
}),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-strings-with-single-quotes',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect checks', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: serial('id'),
name: varchar('name'),
age: integer('age'),
}, (table) => ({
someCheck: check('some_check', sql`${table.age} > 21`),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-checks',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect checks from different schemas with same names', async () => {
const client = new PGlite();
const mySchema = pgSchema('schema2');
const schema = {
mySchema,
users: pgTable('users', {
id: serial('id'),
age: integer('age'),
}, (table) => ({
someCheck: check('some_check', sql`${table.age} > 21`),
})),
usersInMySchema: mySchema.table('users', {
id: serial('id'),
age: integer('age'),
}, (table) => ({
someCheck: check('some_check', sql`${table.age} < 1`),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-checks-diff-schema-same-names',
['public', 'schema2'],
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect view #1', async () => {
const client = new PGlite();
const users = pgTable('users', {
id: serial('id').primaryKey().notNull(),
name: varchar('users'),
});
const view = pgView('some_view').as((qb) => qb.select().from(users));
const schema = {
view,
users,
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-view',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect view #2', async () => {
const client = new PGlite();
const users = pgTable('users', {
id: serial('id').primaryKey().notNull(),
name: varchar('users'),
});
const view = pgView('some_view', { id: integer('asd') }).with({ checkOption: 'cascaded' }).as(
sql`SELECT * FROM ${users}`,
);
const schema = {
view,
users,
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-view-2',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect view in other schema', async () => {
const client = new PGlite();
const newSchema = pgSchema('new_schema');
const users = pgTable('users', {
id: serial('id').primaryKey().notNull(),
name: varchar('users'),
});
const view = newSchema.view('some_view', { id: integer('asd') }).with({ checkOption: 'cascaded' }).as(
sql`SELECT * FROM ${users}`,
);
const schema = {
view,
users,
newSchema,
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-view-in-other-schema',
['new_schema'],
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect materialized view in other schema', async () => {
const client = new PGlite();
const newSchema = pgSchema('new_schema');
const users = pgTable('users', {
id: serial('id').primaryKey().notNull(),
name: varchar('users'),
});
const view = newSchema.materializedView('some_view', { id: integer('asd') }).with({ autovacuumEnabled: true }).as(
sql`SELECT * FROM ${users}`,
);
const schema = {
view,
users,
newSchema,
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-mat-view-in-other-schema',
['new_schema'],
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect materialized view #1', async () => {
const client = new PGlite();
const users = pgTable('users', {
id: serial('id').primaryKey().notNull(),
name: varchar('users'),
});
const view = pgMaterializedView('some_view').using('heap').withNoData().as((qb) => qb.select().from(users));
const schema = {
view,
users,
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-materialized-view',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('introspect materialized view #2', async () => {
const client = new PGlite();
const users = pgTable('users', {
id: serial('id').primaryKey().notNull(),
name: varchar('users'),
});
const view = pgMaterializedView('some_view', { id: integer('asd') }).with({ autovacuumFreezeMinAge: 1 }).as(
sql`SELECT * FROM ${users}`,
);
const schema = {
view,
users,
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-materialized-view-2',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic policy', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test'),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-policy',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic policy with "as"', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { as: 'permissive' }),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-policy-as',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test.todo('basic policy with CURRENT_USER role', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { to: 'current_user' }),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-policy',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic policy with all fields except "using" and "with"', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { as: 'permissive', for: 'all', to: ['postgres'] }),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-policy-all-fields',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic policy with "using" and "with"', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { using: sql`true`, withCheck: sql`true` }),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-policy-using-withcheck',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('multiple policies', async () => {
const client = new PGlite();
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { using: sql`true`, withCheck: sql`true` }),
rlsPolicy: pgPolicy('newRls'),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'multiple-policies',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('multiple policies with roles', async () => {
const client = new PGlite();
client.query(`CREATE ROLE manager;`);
const schema = {
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { using: sql`true`, withCheck: sql`true` }),
rlsPolicy: pgPolicy('newRls', { to: ['postgres', 'manager'] }),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'multiple-policies-with-roles',
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('basic roles', async () => {
const client = new PGlite();
const schema = {
usersRole: pgRole('user'),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'basic-roles',
['public'],
{ roles: { include: ['user'] } },
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('role with properties', async () => {
const client = new PGlite();
const schema = {
usersRole: pgRole('user', { inherit: false, createDb: true, createRole: true }),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'roles-with-properties',
['public'],
{ roles: { include: ['user'] } },
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('role with a few properties', async () => {
const client = new PGlite();
const schema = {
usersRole: pgRole('user', { inherit: false, createRole: true }),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'roles-with-few-properties',
['public'],
{ roles: { include: ['user'] } },
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});
test('multiple policies with roles from schema', async () => {
const client = new PGlite();
const usersRole = pgRole('user_role', { inherit: false, createRole: true });
const schema = {
usersRole,
users: pgTable('users', {
id: integer('id').primaryKey(),
}, () => ({
rls: pgPolicy('test', { using: sql`true`, withCheck: sql`true` }),
rlsPolicy: pgPolicy('newRls', { to: ['postgres', usersRole] }),
})),
};
const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'multiple-policies-with-roles-from-schema',
['public'],
{ roles: { include: ['user_role'] } },
);
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});