Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions drizzle-kit/src/cli/commands/libSqlPushUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ export const libSqlLogSuggestionsAndReturn = async (
const columnsToRemove: string[] = [];
const tablesToTruncate: string[] = [];

// Track tables that have been recreated to avoid duplicate index creation
const recreatedTables = new Set<string>();

for (const statement of statements) {
if (statement.type === 'drop_table') {
const res = await connection.query<{ count: string }>(
Expand Down Expand Up @@ -231,6 +234,8 @@ export const libSqlLogSuggestionsAndReturn = async (
);
} else if (statement.type === 'recreate_table') {
const tableName = statement.tableName;
// Mark table as recreated to skip duplicate index creation later
recreatedTables.add(tableName);

let dataLoss = false;

Expand Down Expand Up @@ -336,6 +341,9 @@ export const libSqlLogSuggestionsAndReturn = async (
statementsToExecute.push(
...(Array.isArray(fromJsonStatement) ? fromJsonStatement : [fromJsonStatement]),
);
} else if (statement.type === 'create_index' && recreatedTables.has(statement.tableName)) {
// Skip create_index for recreated tables - indexes are already created in _moveDataStatements
continue;
} else {
const fromJsonStatement = fromJson([statement], 'turso', 'push', json2);
statementsToExecute.push(
Expand Down
8 changes: 8 additions & 0 deletions drizzle-kit/src/cli/commands/sqlitePushUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export const logSuggestionsAndReturn = async (
const schemasToRemove: string[] = [];
const tablesToTruncate: string[] = [];

// Track tables that have been recreated to avoid duplicate index creation
const recreatedTables = new Set<string>();

for (const statement of statements) {
if (statement.type === 'drop_table') {
const res = await connection.query<{ count: string }>(
Expand Down Expand Up @@ -217,6 +220,8 @@ export const logSuggestionsAndReturn = async (
);
} else if (statement.type === 'recreate_table') {
const tableName = statement.tableName;
// Mark table as recreated to skip duplicate index creation later
recreatedTables.add(tableName);
const oldTableName = getOldTableName(tableName, meta);

let dataLoss = false;
Expand Down Expand Up @@ -302,6 +307,9 @@ export const logSuggestionsAndReturn = async (
if (pragmaState) {
statementsToExecute.push(`PRAGMA foreign_keys=ON;`);
}
} else if (statement.type === 'create_index' && recreatedTables.has(statement.tableName)) {
// Skip create_index for recreated tables - indexes are already created in _moveDataStatements
continue;
} else {
const fromJsonStatement = fromJson([statement], 'sqlite', 'push');
statementsToExecute.push(
Expand Down
76 changes: 76 additions & 0 deletions drizzle-kit/tests/push/libsql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
int,
integer,
numeric,
primaryKey,
real,
sqliteTable,
sqliteView,
Expand Down Expand Up @@ -1398,3 +1399,78 @@ test('alter view ".as"', async () => {
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});

// Regression test for duplicate CREATE INDEX on table recreation
// Bug: When a recreate_table occurs, CREATE INDEX statements were emitted twice:
// CREATE UNIQUE INDEX `UserSlug_userSlug` ...
// PRAGMA foreign_keys=ON;
// CREATE UNIQUE INDEX `UserSlug_userSlug` ... <-- duplicate!
test('recreate table with composite pk, unique index and regular index should not duplicate CREATE INDEX', async (t) => {
// Schema with composite PK, uniqueIndex, and regular index (matches UserSlugBindings)
const schema = {
UserSlugBindings: sqliteTable(
'UserSlugBindings',
{
userId: text('userId').notNull(),
userSlug: text('userSlug').notNull(),
created: text('created').notNull(),
},
(table) => [
primaryKey({ columns: [table.userSlug, table.userId] }),
uniqueIndex('UserSlug_userSlug').on(table.userSlug),
index('UserSlug_created').on(table.created),
],
),
};

const turso = createClient({ url: ':memory:' });

// ==================== FIRST PUSH: empty DB -> schema ====================
// This simulates the initial `drizzle-kit push` to an empty database
const {
sqlStatements: firstPushStatements,
} = await diffTestSchemasPushLibSQL(turso, {}, schema, [], false);

// First push should have clean CREATE statements, no ALTER/recreate
expect(firstPushStatements.some((s) => s.includes('CREATE TABLE `UserSlugBindings`'))).toBe(true);
expect(firstPushStatements.some((s) => s.includes('CREATE UNIQUE INDEX `UserSlug_userSlug`'))).toBe(true);
expect(firstPushStatements.some((s) => s.includes('CREATE INDEX `UserSlug_created`'))).toBe(true);

// No ALTER TABLE or __new_ (recreate) statements on first push
expect(firstPushStatements.some((s) => s.includes('ALTER TABLE'))).toBe(false);
expect(firstPushStatements.some((s) => s.includes('__new_'))).toBe(false);

// Each index should appear exactly once
const firstPushUniqueIndexCount = firstPushStatements.filter(
(s) => s.includes('CREATE UNIQUE INDEX') && s.includes('UserSlug_userSlug'),
).length;
const firstPushRegularIndexCount = firstPushStatements.filter(
(s) => s.includes('CREATE INDEX') && s.includes('UserSlug_created'),
).length;
expect(firstPushUniqueIndexCount).toBe(1);
expect(firstPushRegularIndexCount).toBe(1);

// ==================== SECOND PUSH: schema -> schema ====================
// This simulates running `drizzle-kit push` again on the same DB
const {
sqlStatements: secondPushStatements,
columnsToRemove,
tablesToRemove,
} = await diffTestSchemasPushLibSQL(turso, schema, schema, [], false);

// Count CREATE INDEX occurrences in second push output
const secondPushUniqueIndexCount = secondPushStatements.filter(
(s) => s.includes('CREATE UNIQUE INDEX') && s.includes('UserSlug_userSlug'),
).length;
const secondPushRegularIndexCount = secondPushStatements.filter(
(s) => s.includes('CREATE INDEX') && s.includes('UserSlug_created'),
).length;

// CRITICAL: Each index should appear at most ONCE, not twice
// Bug was: _moveDataStatements created indexes, then create_index statements added duplicates
expect(secondPushUniqueIndexCount).toBeLessThanOrEqual(1);
expect(secondPushRegularIndexCount).toBeLessThanOrEqual(1);

expect(columnsToRemove!.length).toBe(0);
expect(tablesToRemove!.length).toBe(0);
});
Loading