Skip to content

Commit

Permalink
feat: add renameColumn migration step
Browse files Browse the repository at this point in the history
  • Loading branch information
zhenkaGo committed Sep 19, 2023
1 parent 0974c47 commit ea8771a
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 19 deletions.
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

0 comments on commit ea8771a

Please sign in to comment.