2024-01-30 14:25:04 -05:00
import { expect } from 'chai' ;
import { describe , it } from 'mocha' ;
2024-01-30 17:50:03 -05:00
import {
getCollectionChangeQueries ,
getMigrationQueries ,
} from '../../dist/core/cli/migration-queries.js' ;
import { getCreateTableQuery } from '../../dist/core/queries.js' ;
2024-02-06 13:55:20 -05:00
import { field , defineCollection , collectionSchema } from '../../dist/core/types.js' ;
2024-02-08 13:32:56 -05:00
import { NOW , sql } from '../../dist/runtime/index.js' ;
2024-01-30 14:25:04 -05:00
const COLLECTION _NAME = 'Users' ;
2024-02-06 13:55:20 -05:00
// `parse` to resolve schema transformations
// ex. convert field.date() to ISO strings
const userInitial = collectionSchema . parse (
defineCollection ( {
fields : {
name : field . text ( ) ,
age : field . number ( ) ,
email : field . text ( { unique : true } ) ,
mi : field . text ( { optional : true } ) ,
} ,
} )
) ;
2024-01-30 14:25:04 -05:00
2024-02-06 13:48:41 -05:00
const defaultAmbiguityResponses = {
collectionRenames : { } ,
fieldRenames : { } ,
2024-01-30 14:25:04 -05:00
} ;
2024-02-06 13:48:41 -05:00
function userChangeQueries (
oldCollection ,
newCollection ,
ambiguityResponses = defaultAmbiguityResponses
) {
2024-01-30 14:25:04 -05:00
return getCollectionChangeQueries ( {
collectionName : COLLECTION _NAME ,
oldCollection ,
newCollection ,
2024-02-06 13:48:41 -05:00
ambiguityResponses ,
2024-01-30 14:25:04 -05:00
} ) ;
}
function configChangeQueries (
oldCollections ,
newCollections ,
2024-02-06 13:48:41 -05:00
ambiguityResponses = defaultAmbiguityResponses
2024-01-30 14:25:04 -05:00
) {
return getMigrationQueries ( {
oldSnapshot : { schema : oldCollections , experimentalVersion : 1 } ,
newSnapshot : { schema : newCollections , experimentalVersion : 1 } ,
2024-02-06 13:48:41 -05:00
ambiguityResponses ,
2024-01-30 14:25:04 -05:00
} ) ;
}
describe ( 'field queries' , ( ) => {
describe ( 'getMigrationQueries' , ( ) => {
it ( 'should be empty when collections are the same' , async ( ) => {
const oldCollections = { [ COLLECTION _NAME ] : userInitial } ;
const newCollections = { [ COLLECTION _NAME ] : userInitial } ;
2024-02-06 13:48:41 -05:00
const { queries } = await configChangeQueries ( oldCollections , newCollections ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [ ] ) ;
} ) ;
it ( 'should create table for new collections' , async ( ) => {
const oldCollections = { } ;
const newCollections = { [ COLLECTION _NAME ] : userInitial } ;
2024-02-06 13:48:41 -05:00
const { queries } = await configChangeQueries ( oldCollections , newCollections ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [ getCreateTableQuery ( COLLECTION _NAME , userInitial ) ] ) ;
} ) ;
it ( 'should drop table for removed collections' , async ( ) => {
const oldCollections = { [ COLLECTION _NAME ] : userInitial } ;
const newCollections = { } ;
2024-02-06 13:48:41 -05:00
const { queries } = await configChangeQueries ( oldCollections , newCollections ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [ ` DROP TABLE " ${ COLLECTION _NAME } " ` ] ) ;
} ) ;
it ( 'should rename table for renamed collections' , async ( ) => {
const rename = 'Peeps' ;
const oldCollections = { [ COLLECTION _NAME ] : userInitial } ;
const newCollections = { [ rename ] : userInitial } ;
2024-02-06 13:48:41 -05:00
const { queries } = await configChangeQueries ( oldCollections , newCollections , {
... defaultAmbiguityResponses ,
2024-01-30 14:25:04 -05:00
collectionRenames : { [ rename ] : COLLECTION _NAME } ,
} ) ;
expect ( queries ) . to . deep . equal ( [ ` ALTER TABLE " ${ COLLECTION _NAME } " RENAME TO " ${ rename } " ` ] ) ;
} ) ;
} ) ;
describe ( 'getCollectionChangeQueries' , ( ) => {
it ( 'should be empty when collections are the same' , async ( ) => {
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userInitial ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [ ] ) ;
} ) ;
it ( 'should be empty when type updated to same underlying SQL type' , async ( ) => {
2024-02-06 13:55:20 -05:00
const blogInitial = collectionSchema . parse ( {
2024-01-30 14:25:04 -05:00
... userInitial ,
fields : {
title : field . text ( ) ,
draft : field . boolean ( ) ,
} ,
} ) ;
2024-02-06 13:55:20 -05:00
const blogFinal = collectionSchema . parse ( {
2024-01-30 14:25:04 -05:00
... userInitial ,
fields : {
... blogInitial . fields ,
draft : field . number ( ) ,
} ,
} ) ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( blogInitial , blogFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [ ] ) ;
} ) ;
2024-02-07 18:15:21 -05:00
it ( 'should respect user primary key without adding a hidden id' , async ( ) => {
const user = collectionSchema . parse ( {
... userInitial ,
fields : {
... userInitial . fields ,
id : field . number ( { primaryKey : true } ) ,
} ,
} ) ;
const userFinal = collectionSchema . parse ( {
... user ,
fields : {
... user . fields ,
name : field . text ( { unique : true , optional : true } ) ,
} ,
} ) ;
const { queries } = await userChangeQueries ( user , userFinal ) ;
expect ( queries [ 0 ] ) . to . not . be . undefined ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE \" ${ tempTableName } \" ( \" name \" text UNIQUE, \" age \" integer NOT NULL, \" email \" text NOT NULL UNIQUE, \" mi \" text, \" id \" integer PRIMARY KEY) ` ,
` INSERT INTO \" ${ tempTableName } \" ( \" name \" , \" age \" , \" email \" , \" mi \" , \" id \" ) SELECT \" name \" , \" age \" , \" email \" , \" mi \" , \" id \" FROM \" Users \" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
2024-01-30 14:25:04 -05:00
describe ( 'ALTER RENAME COLUMN' , ( ) => {
it ( 'when renaming a field' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
} ,
} ;
userFinal . fields . middleInitial = userFinal . fields . mi ;
delete userFinal . fields . mi ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal , {
collectionRenames : { } ,
fieldRenames : { [ COLLECTION _NAME ] : { middleInitial : 'mi' } } ,
2024-01-30 14:25:04 -05:00
} ) ;
expect ( queries ) . to . deep . equal ( [
` ALTER TABLE " ${ COLLECTION _NAME } " RENAME COLUMN "mi" TO "middleInitial" ` ,
] ) ;
} ) ;
} ) ;
describe ( 'Lossy table recreate' , ( ) => {
it ( 'when changing a field type' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
age : field . text ( ) ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [
'DROP TABLE "Users"' ,
` CREATE TABLE "Users" (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" text NOT NULL, "email" text NOT NULL UNIQUE, "mi" text) ` ,
] ) ;
} ) ;
it ( 'when adding a required field without a default' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
phoneNumber : field . text ( ) ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [
'DROP TABLE "Users"' ,
` CREATE TABLE "Users" (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL, "email" text NOT NULL UNIQUE, "mi" text, "phoneNumber" text NOT NULL) ` ,
] ) ;
} ) ;
} ) ;
describe ( 'Lossless table recreate' , ( ) => {
2024-02-07 18:15:21 -05:00
it ( 'when adding a primary key' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
id : field . number ( { primaryKey : true } ) ,
} ,
} ;
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
expect ( queries [ 0 ] ) . to . not . be . undefined ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE \" ${ tempTableName } \" ( \" name \" text NOT NULL, \" age \" integer NOT NULL, \" email \" text NOT NULL UNIQUE, \" mi \" text, \" id \" integer PRIMARY KEY) ` ,
` INSERT INTO \" ${ tempTableName } \" ( \" name \" , \" age \" , \" email \" , \" mi \" ) SELECT \" name \" , \" age \" , \" email \" , \" mi \" FROM \" Users \" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
it ( 'when dropping a primary key' , async ( ) => {
const user = {
... userInitial ,
fields : {
... userInitial . fields ,
id : field . number ( { primaryKey : true } ) ,
} ,
} ;
const { queries } = await userChangeQueries ( user , userInitial ) ;
expect ( queries [ 0 ] ) . to . not . be . undefined ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE \" ${ tempTableName } \" (_id INTEGER PRIMARY KEY, \" name \" text NOT NULL, \" age \" integer NOT NULL, \" email \" text NOT NULL UNIQUE, \" mi \" text) ` ,
` INSERT INTO \" ${ tempTableName } \" ( \" name \" , \" age \" , \" email \" , \" mi \" ) SELECT \" name \" , \" age \" , \" email \" , \" mi \" FROM \" Users \" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
2024-01-30 14:25:04 -05:00
it ( 'when adding an optional unique field' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
phoneNumber : field . text ( { unique : true , optional : true } ) ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . have . lengthOf ( 4 ) ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( tempTableName ) . to . be . a ( 'string' ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE " ${ tempTableName } " (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL, "email" text NOT NULL UNIQUE, "mi" text, "phoneNumber" text UNIQUE) ` ,
` INSERT INTO " ${ tempTableName } " ("_id", "name", "age", "email", "mi") SELECT "_id", "name", "age", "email", "mi" FROM "Users" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
it ( 'when dropping unique column' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
} ,
} ;
delete userFinal . fields . email ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . have . lengthOf ( 4 ) ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( tempTableName ) . to . be . a ( 'string' ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE " ${ tempTableName } " (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL, "mi" text) ` ,
` INSERT INTO " ${ tempTableName } " ("_id", "name", "age", "mi") SELECT "_id", "name", "age", "mi" FROM "Users" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
it ( 'when updating to a runtime default' , async ( ) => {
2024-02-06 13:55:20 -05:00
const initial = collectionSchema . parse ( {
2024-01-30 14:25:04 -05:00
... userInitial ,
fields : {
... userInitial . fields ,
age : field . date ( ) ,
} ,
} ) ;
2024-02-06 13:55:20 -05:00
const userFinal = collectionSchema . parse ( {
2024-02-06 13:48:41 -05:00
... initial ,
2024-01-30 14:25:04 -05:00
fields : {
... initial . fields ,
2024-02-08 13:32:56 -05:00
age : field . date ( { default : NOW } ) ,
2024-01-30 14:25:04 -05:00
} ,
2024-02-06 13:48:41 -05:00
} ) ;
2024-01-30 14:25:04 -05:00
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( initial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . have . lengthOf ( 4 ) ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( tempTableName ) . to . be . a ( 'string' ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE " ${ tempTableName } " (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" text NOT NULL DEFAULT CURRENT_TIMESTAMP, "email" text NOT NULL UNIQUE, "mi" text) ` ,
` INSERT INTO " ${ tempTableName } " ("_id", "name", "age", "email", "mi") SELECT "_id", "name", "age", "email", "mi" FROM "Users" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
it ( 'when adding a field with a runtime default' , async ( ) => {
2024-02-06 13:55:20 -05:00
const userFinal = collectionSchema . parse ( {
... userInitial ,
2024-01-30 14:25:04 -05:00
fields : {
... userInitial . fields ,
2024-02-08 13:32:56 -05:00
birthday : field . date ( { default : NOW } ) ,
2024-01-30 14:25:04 -05:00
} ,
2024-02-06 13:55:20 -05:00
} ) ;
2024-01-30 14:25:04 -05:00
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . have . lengthOf ( 4 ) ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( tempTableName ) . to . be . a ( 'string' ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE " ${ tempTableName } " (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL, "email" text NOT NULL UNIQUE, "mi" text, "birthday" text NOT NULL DEFAULT CURRENT_TIMESTAMP) ` ,
` INSERT INTO " ${ tempTableName } " ("_id", "name", "age", "email", "mi") SELECT "_id", "name", "age", "email", "mi" FROM "Users" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
/ * *
* REASON : to follow the "expand" and "contract" migration model ,
* you ' ll need to update the schema from NOT NULL to NULL .
* It ' s up to the user to ensure all data follows the new schema !
*
* @ see https : //planetscale.com/blog/safely-making-database-schema-changes#backwards-compatible-changes
* /
it ( 'when changing a field to required' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
mi : field . text ( ) ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . have . lengthOf ( 4 ) ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( tempTableName ) . to . be . a ( 'string' ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE " ${ tempTableName } " (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL, "email" text NOT NULL UNIQUE, "mi" text NOT NULL) ` ,
` INSERT INTO " ${ tempTableName } " ("_id", "name", "age", "email", "mi") SELECT "_id", "name", "age", "email", "mi" FROM "Users" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
it ( 'when changing a field to unique' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
age : field . number ( { unique : true } ) ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . have . lengthOf ( 4 ) ;
const tempTableName = getTempTableName ( queries [ 0 ] ) ;
expect ( tempTableName ) . to . be . a ( 'string' ) ;
expect ( queries ) . to . deep . equal ( [
` CREATE TABLE " ${ tempTableName } " (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL UNIQUE, "email" text NOT NULL UNIQUE, "mi" text) ` ,
` INSERT INTO " ${ tempTableName } " ("_id", "name", "age", "email", "mi") SELECT "_id", "name", "age", "email", "mi" FROM "Users" ` ,
'DROP TABLE "Users"' ,
` ALTER TABLE " ${ tempTableName } " RENAME TO "Users" ` ,
] ) ;
} ) ;
} ) ;
describe ( 'ALTER ADD COLUMN' , ( ) => {
it ( 'when adding an optional field' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
... userInitial . fields ,
birthday : field . date ( { optional : true } ) ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [ 'ALTER TABLE "Users" ADD COLUMN "birthday" text' ] ) ;
} ) ;
it ( 'when adding a required field with default' , async ( ) => {
const defaultDate = new Date ( '2023-01-01' ) ;
2024-02-06 13:55:20 -05:00
const userFinal = collectionSchema . parse ( {
2024-01-30 14:25:04 -05:00
... userInitial ,
fields : {
... userInitial . fields ,
birthday : field . date ( { default : new Date ( '2023-01-01' ) } ) ,
} ,
} ) ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [
` ALTER TABLE "Users" ADD COLUMN "birthday" text NOT NULL DEFAULT ' ${ defaultDate . toISOString ( ) } ' ` ,
] ) ;
} ) ;
} ) ;
describe ( 'ALTER DROP COLUMN' , ( ) => {
it ( 'when removing optional or required fields' , async ( ) => {
const userFinal = {
... userInitial ,
fields : {
name : userInitial . fields . name ,
email : userInitial . fields . email ,
} ,
} ;
2024-02-06 13:48:41 -05:00
const { queries } = await userChangeQueries ( userInitial , userFinal ) ;
2024-01-30 14:25:04 -05:00
expect ( queries ) . to . deep . equal ( [
'ALTER TABLE "Users" DROP COLUMN "age"' ,
'ALTER TABLE "Users" DROP COLUMN "mi"' ,
] ) ;
} ) ;
} ) ;
} ) ;
} ) ;
/** @param {string} query */
function getTempTableName ( query ) {
return query . match ( /Users_([a-z0-9]+)/ ) ? . [ 0 ] ;
}