Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add renameColumn migration step #1654

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG-Unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Changes unlikely to cause issues:
### New features

- New `@experimentalFailsafe` decorator you can apply before `@relation/@immutableRelation` so that if relation points to a record that does not exist, `.fetch()/.observe()` yield `undefined` instead of throwing an error
- Added renameColumn migration step.

### Fixes

Expand Down
22 changes: 21 additions & 1 deletion src/Schema/migrations/getSyncChanges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export type MigrationSyncChanges = $Exact<{
table: TableName<any>,
columns: ColumnName[],
}>[],
+renamedColumns: $Exact<{
table: TableName<any>,
columns: $Exact<{
from: ColumnName,
to: ColumnName,
}>[],
}>[],
}> | null

export default function getSyncChanges(
Expand All @@ -34,7 +41,7 @@ export default function getSyncChanges(

steps.forEach((step) => {
invariant(
['create_table', 'add_columns', 'sql'].includes(step.type),
['create_table', 'add_columns', 'sql', 'rename_column'].includes(step.type),
`Unknown migration step type ${step.type}. Can not perform migration sync. This most likely means your migrations are defined incorrectly. It could also be a WatermelonDB bug.`,
)
})
Expand Down Expand Up @@ -63,9 +70,22 @@ export default function getSyncChanges(
columns: unique(columnDefs.map(({ name }) => name)),
}))

const allRenamedColumns = steps.filter(
(step) => step.type === 'rename_column' && !createdTables.includes(step.table),
)

const renamedColumns = pipe(
groupBy(({ table }) => table),
toPairs,
)(allRenamedColumns).map(([table, columns]) => ({
table: tableName(table),
columns: columns.map(({ from, to }) => ({ from, to })),
}))

return {
from: fromVersion,
tables: unique(createdTables),
columns: addedColumns,
renamedColumns,
}
}
55 changes: 50 additions & 5 deletions src/Schema/migrations/getSyncChanges/test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import getSyncChanges from './index'
import { schemaMigrations, createTable, addColumns, unsafeExecuteSql } from '../index'
import { schemaMigrations, createTable, addColumns, unsafeExecuteSql, renameColumn } from '../index'

const createCommentsTable = createTable({
name: 'comments',
Expand All @@ -18,10 +18,15 @@ describe('getSyncChanges', () => {
expect(test([{ toVersion: 2, steps: [createCommentsTable] }], 2, 2)).toEqual(null)
})
it('returns empty changes for empty steps', () => {
expect(testSteps([])).toEqual({ from: 1, tables: [], columns: [] })
expect(testSteps([])).toEqual({ from: 1, tables: [], columns: [], renamedColumns: [] })
})
it('returns created tables', () => {
expect(testSteps([createCommentsTable])).toEqual({ from: 1, tables: ['comments'], columns: [] })
expect(testSteps([createCommentsTable])).toEqual({
from: 1,
tables: ['comments'],
columns: [],
renamedColumns: [],
})
})
it('returns added columns', () => {
expect(
Expand All @@ -38,6 +43,36 @@ describe('getSyncChanges', () => {
from: 1,
tables: [],
columns: [{ table: 'posts', columns: ['subtitle', 'is_pinned'] }],
renamedColumns: [],
})
})
it('returns renamed columns', () => {
expect(testSteps([renameColumn({ table: 'posts', from: 'body', to: 'text' })])).toEqual({
from: 1,
tables: [],
columns: [],
renamedColumns: [{ table: 'posts', columns: [{ from: 'body', to: 'text' }] }],
})
})
it('combines renamed columns from multiple migration steps', () => {
expect(
testSteps([
renameColumn({ table: 'posts', from: 'body', to: 'text' }),
renameColumn({ table: 'posts', from: 'favorite', to: 'saved' }),
]),
).toEqual({
from: 1,
tables: [],
columns: [],
renamedColumns: [
{
table: 'posts',
columns: [
{ from: 'body', to: 'text' },
{ from: 'favorite', to: 'saved' },
],
},
],
})
})
it('combines added columns from multiple migration steps', () => {
Expand All @@ -60,6 +95,7 @@ describe('getSyncChanges', () => {
from: 1,
tables: [],
columns: [{ table: 'posts', columns: ['subtitle', 'is_pinned', 'author_id'] }],
renamedColumns: [],
})
})
it('skips added columns for a table if it is also added', () => {
Expand All @@ -75,6 +111,7 @@ describe('getSyncChanges', () => {
from: 1,
tables: ['comments'],
columns: [],
renamedColumns: [],
})
})
it('skips duplicates', () => {
Expand All @@ -97,6 +134,7 @@ describe('getSyncChanges', () => {
from: 1,
tables: ['comments'],
columns: [{ table: 'posts', columns: ['subtitle'] }],
renamedColumns: [],
})
})
const bigMigrations = [
Expand Down Expand Up @@ -198,6 +236,7 @@ describe('getSyncChanges', () => {
{ table: 'comments', columns: ['is_pinned', 'extra'] },
{ table: 'workspaces', columns: ['plan_info', 'limits'] },
],
renamedColumns: [],
})
})
it(`returns only the necessary range of migrations`, () => {
Expand All @@ -208,13 +247,20 @@ describe('getSyncChanges', () => {
{ table: 'workspaces', columns: ['plan_info', 'limits'] },
{ table: 'attachment_versions', columns: ['reactions'] },
],
renamedColumns: [],
})
expect(test(bigMigrations, 8, 10)).toEqual({
from: 8,
tables: [],
columns: [{ table: 'attachment_versions', columns: ['reactions'] }],
renamedColumns: [],
})
expect(test(bigMigrations, 9, 10)).toEqual({
from: 9,
tables: [],
columns: [],
renamedColumns: [],
})
expect(test(bigMigrations, 9, 10)).toEqual({ from: 9, tables: [], columns: [] })
expect(test(bigMigrations, 10, 10)).toEqual(null)
})
it(`fails on incorrect migrations`, () => {
Expand All @@ -225,7 +271,6 @@ describe('getSyncChanges', () => {
const possibleFutureTypes = [
'broken',
'rename_table',
'rename_column',
'add_column_index',
'make_column_optional',
'make_column_required',
Expand Down
24 changes: 23 additions & 1 deletion src/Schema/migrations/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { $RE, $Exact } from '../../types'
import { ColumnName } from '../index'
import type { ColumnSchema, TableName, TableSchema, TableSchemaSpec, SchemaVersion } from '../index'

export type CreateTableMigrationStep = $RE<{
Expand All @@ -18,7 +19,18 @@ export type SqlMigrationStep = $RE<{
sql: string
}>

export type MigrationStep = CreateTableMigrationStep | AddColumnsMigrationStep | SqlMigrationStep
export type RenameColumnMigrationStep = $RE<{
type: 'rename_column'
table: TableName<any>
from: ColumnName
to: ColumnName
}>

export type MigrationStep =
| CreateTableMigrationStep
| AddColumnsMigrationStep
| SqlMigrationStep
| RenameColumnMigrationStep

type Migration = $RE<{
toVersion: SchemaVersion
Expand Down Expand Up @@ -51,3 +63,13 @@ export function addColumns({
}>): AddColumnsMigrationStep

export function unsafeExecuteSql(sql: string): SqlMigrationStep

export function renameColumn({
table,
from,
to,
}: $Exact<{
table: TableName<any>
from: string
to: string
}>): RenameColumnMigrationStep
40 changes: 37 additions & 3 deletions src/Schema/migrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import invariant from '../../utils/common/invariant'
import isObj from '../../utils/fp/isObj'

import type { $RE } from '../../types'
import type { ColumnSchema, TableName, TableSchema, TableSchemaSpec, SchemaVersion } from '../index'
import type {
ColumnSchema,
TableName,
TableSchema,
TableSchemaSpec,
SchemaVersion,
ColumnName,
} from '../index'
import { tableSchema, validateColumnSchema } from '../index'

export type CreateTableMigrationStep = $RE<{
Expand All @@ -26,7 +33,18 @@ export type SqlMigrationStep = $RE<{
sql: string,
}>

export type MigrationStep = CreateTableMigrationStep | AddColumnsMigrationStep | SqlMigrationStep
export type RenameColumnMigrationStep = $RE<{
type: 'rename_column',
table: TableName<any>,
from: ColumnName,
to: ColumnName,
}>

export type MigrationStep =
| CreateTableMigrationStep
| AddColumnsMigrationStep
| SqlMigrationStep
| RenameColumnMigrationStep

type Migration = $RE<{
toVersion: SchemaVersion,
Expand Down Expand Up @@ -166,6 +184,23 @@ export function unsafeExecuteSql(sql: string): SqlMigrationStep {
return { type: 'sql', sql }
}

export function renameColumn({
table,
from,
to,
}: $Exact<{
table: TableName<any>,
from: ColumnName,
to: ColumnName,
}>): RenameColumnMigrationStep {
if (process.env.NODE_ENV !== 'production') {
invariant(table, `Missing table name in renameColumn()`)
invariant(from, `Missing 'from' in renameColumn()`)
invariant(to, `Missing 'to' in renameColumn()`)
}
return { type: 'rename_column', table, from, to }
}

/*

TODO: Those types of migrations are currently not implemented. If you need them, feel free to contribute!
Expand All @@ -175,7 +210,6 @@ destroyTable('table_name')
renameTable({ from: 'old_table_name', to: 'new_table_name' })

// column operations
renameColumn({ table: 'table_name', from: 'old_column_name', to: 'new_column_name' })
destroyColumn({ table: 'table_name', column: 'column_name' })

// indexing
Expand Down
22 changes: 19 additions & 3 deletions src/Schema/migrations/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createTable, addColumns, schemaMigrations } from './index'
import { createTable, addColumns, schemaMigrations, renameColumn } from './index'
import { stepsForMigration } from './stepsForMigration'

describe('schemaMigrations()', () => {
Expand Down Expand Up @@ -30,7 +30,7 @@ describe('schemaMigrations()', () => {
it('returns a complex schema migrations spec', () => {
const migrations = schemaMigrations({
migrations: [
{ toVersion: 4, steps: [] },
{ toVersion: 4, steps: [renameColumn({ table: 'comments', from: 'body', to: 'text' })] },
{
toVersion: 3,
steps: [
Expand Down Expand Up @@ -103,7 +103,17 @@ describe('schemaMigrations()', () => {
},
],
},
{ toVersion: 4, steps: [] },
{
toVersion: 4,
steps: [
{
type: 'rename_column',
table: 'comments',
from: 'body',
to: 'text',
},
],
},
],
})
})
Expand Down Expand Up @@ -181,6 +191,12 @@ describe('migration step functions', () => {
'type',
)
})

it('throws if renameColumns() is malformed', () => {
expect(() => renameColumn({ from: 'text', to: 'body' })).toThrow('table')
expect(() => renameColumn({ table: 'foo', from: 'text' })).toThrow('to')
expect(() => renameColumn({ table: 'foo', to: 'body' })).toThrow('from')
})
})

describe('stepsForMigration', () => {
Expand Down
27 changes: 23 additions & 4 deletions src/adapters/sqlite/encodeSchema/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@

import type { TableSchema, AppSchema, ColumnSchema, TableName } from '../../../Schema'
import { nullValue } from '../../../RawRecord'
import type { MigrationStep, AddColumnsMigrationStep } from '../../../Schema/migrations'
import type {
MigrationStep,
AddColumnsMigrationStep,
RenameColumnMigrationStep,
} from '../../../Schema/migrations'
import type { SQL } from '../index'

import encodeValue from '../encodeValue'

const standardColumns = `"id" primary key, "_changed", "_status"`
const standardColumns = ['id', '_changed', '_status']
const standardColumnsSQL = standardColumns.map((column) => column === 'id' ? `"${column}" primary key` : `"${column}"`).join(', ')
const commonSchema =
'create table "local_storage" ("key" varchar(16) primary key not null, "value" text not null);' +
'create index "local_storage_key_index" on "local_storage" ("key");'

const encodeCreateTable = ({ name, columns }: TableSchema): SQL => {
const columnsSQL = [standardColumns]
const columnsSQL = [standardColumnsSQL]
.concat(Object.keys(columns).map((column) => `"${column}"`))
.join(', ')
return `create table "${name}" (${columnsSQL});`
Expand Down Expand Up @@ -85,7 +90,19 @@ const encodeAddColumnsMigrationStep: (AddColumnsMigrationStep) => SQL = ({
})
.join('')

export const encodeMigrationSteps: (MigrationStep[]) => SQL = (steps) =>
const encodeRenameColumnMigrationsStep: (RenameColumnMigrationStep, TableSchema) => SQL = ({ table, from, to }, tableSchema) => {
const newTempTable = { ...tableSchema, name: `${table}Temp` }
const newColumns = [...standardColumns, ...Object.keys(tableSchema.columns)]
const prevColumns = newColumns.map(c => c === to ? from : c)
return `
${encodeTable(newTempTable)}
INSERT INTO ${table}Temp(${newColumns.join(',')}) SELECT ${prevColumns.join(',')} FROM ${table};
DROP TABLE ${table};
ALTER TABLE ${table}Temp RENAME TO ${table};
`
}

export const encodeMigrationSteps: (MigrationStep[], AppSchema) => SQL = (steps, schema) =>
steps
.map((step) => {
if (step.type === 'create_table') {
Expand All @@ -94,6 +111,8 @@ export const encodeMigrationSteps: (MigrationStep[]) => SQL = (steps) =>
return encodeAddColumnsMigrationStep(step)
} else if (step.type === 'sql') {
return step.sql
} else if (step.type === 'rename_column') {
return encodeRenameColumnMigrationsStep(step, schema.tables[step.table])
}

throw new Error(`Unsupported migration step ${step.type}`)
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/sqlite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export default class SQLiteAdapter implements DatabaseAdapter {
'setUpWithMigrations',
[
this.dbName,
require('./encodeSchema').encodeMigrationSteps(migrationSteps),
require('./encodeSchema').encodeMigrationSteps(migrationSteps, this.schema),
databaseVersion,
this.schema.version,
],
Expand Down
Loading