0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-23 21:53:55 -05:00
astro/packages/db/test/unit/column-queries.test.js
Ben Holmes 3488be9b59
finalize WIP API (#10280)
* feat: no more readable / writable

* fix: table typegen

* wip: move data seeding

* chore: add scripts to basics

* feat: data() -> seed file

* refactor: ensure precedence of file name

* feat: db execute command

* fix: test imports

* chore: remove old readable error tests

* feat: support local db with `db execute`

* refactor: remove integrations from test for now

* chore: stray comment

* chore: remove `table` config object

* feat: `db.batch`!

* refactor: move migrations/ inside db/

* fix: move ticketing-example to seed file

* fix: disable foreign keys when recreating tables

* refactor: standardize migrations dir

* feat: move to db/config.ts

* feat: file watching for db/config.ts dependencies

* feat: remove unsafeDisableStudio

* chroe: remove bad import

* feat: parse config.ts from cli

* chore: remove async from localDatabaseClient

* fix: update recipes config and seed

* chore: update unit tests

* chore: update tests to dev server

* refactor: collectionToTable -> asDrizzleTable

* chore: tidy up collection -> table error states

* refactor: regexp -> endsWith

* feat: pretty error inserting into table

* refactor: try/catch -> catch()

* feat: expose utils for integration seed files

* fix: add config import to db client modules

* fix: just use generic "seeding database" error

* chore: remove unused link args

* fix: migration queries im,port

* chore: remove irrelevant glob/ example

* feat: format migration file path

* feat: support all config file names

* chore: remove db.batch() for now

* chore: remove `db` object

* core: remove unused integration file

* chore: changeset

* fix: foreign key empty error message

* chore: remove old TODO

* fix: bad context reference

* refactor: seedDev -> seedLocal

* wip: throw some console logs at github

* wip: avoid seeding astro:db imported by seed file

* wip: use anything in db/

* refactor: only seed when loaded within srcDir

* refactor: avoid resolution when not seeding

* chore: remove logs

* refactor: seed within create local db client

* refactor: use normalizePath

* wip: logs

* wip: logs

* refactor: early return

* chore: more logs

* refactor: no batch

* fix: use beforeAll

* refactor: move all tests to base block

* wip: log dev server starting

* chore: remove logs

* wip: demo ready

* chore: remove duplicate recreateTables() call

* Revert "wip: demo ready"

This reverts commit 37585ce5cb.

* refactor: beforeEach to isolate dev servers

* chore: remove useBundledDbUrl

* refactor: naming and seed scope

* chore: remove stray console logs

* wip: fix windows file import

* wip: try fileURLToPath

* Revert "wip: try fileURLToPath"

This reverts commit 46fd65d61a.

* Revert "wip: fix windows file import"

This reverts commit 1a669ea646.

* refactor: dir -> directory

* refactor: move execute file to cli

* refactor: remove seed.dev convention

* wip: attempt fileURLToPath

* wip: debug the file exists

* fix: use mjs??

* chore: remove duplicate seedLocal

* chore: remove log check

* refactor: use in memory db for tests

* chore: clean up test comment

* fix: avoid file writes for db setup on in memory db

* chore: bump db changeset to minor

---------

Co-authored-by: Nate Moore <nate@astro.build>
2024-03-01 19:29:55 -05:00

446 lines
15 KiB
JavaScript

import { expect } from 'chai';
import { describe, it } from 'mocha';
import {
getCollectionChangeQueries,
getMigrationQueries,
} from '../../dist/core/cli/migration-queries.js';
import { getCreateTableQuery } from '../../dist/runtime/queries.js';
import { column, defineTable } from '../../dist/runtime/config.js';
import { tableSchema } from '../../dist/core/types.js';
import { NOW } from '../../dist/runtime/index.js';
const TABLE_NAME = 'Users';
// `parse` to resolve schema transformations
// ex. convert column.date() to ISO strings
const userInitial = tableSchema.parse(
defineTable({
columns: {
name: column.text(),
age: column.number(),
email: column.text({ unique: true }),
mi: column.text({ optional: true }),
},
})
);
const defaultAmbiguityResponses = {
collectionRenames: {},
columnRenames: {},
};
function userChangeQueries(oldTable, newTable, ambiguityResponses = defaultAmbiguityResponses) {
return getCollectionChangeQueries({
collectionName: TABLE_NAME,
oldCollection: oldTable,
newCollection: newTable,
ambiguityResponses,
});
}
function configChangeQueries(
oldCollections,
newCollections,
ambiguityResponses = defaultAmbiguityResponses
) {
return getMigrationQueries({
oldSnapshot: { schema: oldCollections, experimentalVersion: 1 },
newSnapshot: { schema: newCollections, experimentalVersion: 1 },
ambiguityResponses,
});
}
describe('column queries', () => {
describe('getMigrationQueries', () => {
it('should be empty when tables are the same', async () => {
const oldCollections = { [TABLE_NAME]: userInitial };
const newCollections = { [TABLE_NAME]: userInitial };
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([]);
});
it('should create table for new tables', async () => {
const oldCollections = {};
const newCollections = { [TABLE_NAME]: userInitial };
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([getCreateTableQuery(TABLE_NAME, userInitial)]);
});
it('should drop table for removed tables', async () => {
const oldCollections = { [TABLE_NAME]: userInitial };
const newCollections = {};
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([`DROP TABLE "${TABLE_NAME}"`]);
});
it('should rename table for renamed tables', async () => {
const rename = 'Peeps';
const oldCollections = { [TABLE_NAME]: userInitial };
const newCollections = { [rename]: userInitial };
const { queries } = await configChangeQueries(oldCollections, newCollections, {
...defaultAmbiguityResponses,
collectionRenames: { [rename]: TABLE_NAME },
});
expect(queries).to.deep.equal([`ALTER TABLE "${TABLE_NAME}" RENAME TO "${rename}"`]);
});
});
describe('getCollectionChangeQueries', () => {
it('should be empty when tables are the same', async () => {
const { queries } = await userChangeQueries(userInitial, userInitial);
expect(queries).to.deep.equal([]);
});
it('should be empty when type updated to same underlying SQL type', async () => {
const blogInitial = tableSchema.parse({
...userInitial,
columns: {
title: column.text(),
draft: column.boolean(),
},
});
const blogFinal = tableSchema.parse({
...userInitial,
columns: {
...blogInitial.columns,
draft: column.number(),
},
});
const { queries } = await userChangeQueries(blogInitial, blogFinal);
expect(queries).to.deep.equal([]);
});
it('should respect user primary key without adding a hidden id', async () => {
const user = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
id: column.number({ primaryKey: true }),
},
});
const userFinal = tableSchema.parse({
...user,
columns: {
...user.columns,
name: column.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"`,
]);
});
describe('ALTER RENAME COLUMN', () => {
it('when renaming a column', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
},
};
userFinal.columns.middleInitial = userFinal.columns.mi;
delete userFinal.columns.mi;
const { queries } = await userChangeQueries(userInitial, userFinal, {
collectionRenames: {},
columnRenames: { [TABLE_NAME]: { middleInitial: 'mi' } },
});
expect(queries).to.deep.equal([
`ALTER TABLE "${TABLE_NAME}" RENAME COLUMN "mi" TO "middleInitial"`,
]);
});
});
describe('Lossy table recreate', () => {
it('when changing a column type', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
age: column.text(),
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
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 column without a default', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
phoneNumber: column.text(),
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
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', () => {
it('when adding a primary key', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
id: column.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,
columns: {
...userInitial.columns,
id: column.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"`,
]);
});
it('when adding an optional unique column', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
phoneNumber: column.text({ unique: true, optional: true }),
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
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,
columns: {
...userInitial.columns,
},
};
delete userFinal.columns.email;
const { queries } = await userChangeQueries(userInitial, userFinal);
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 () => {
const initial = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
age: column.date(),
},
});
const userFinal = tableSchema.parse({
...initial,
columns: {
...initial.columns,
age: column.date({ default: NOW }),
},
});
const { queries } = await userChangeQueries(initial, userFinal);
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 column with a runtime default', async () => {
const userFinal = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
birthday: column.date({ default: NOW }),
},
});
const { queries } = await userChangeQueries(userInitial, userFinal);
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 column to required', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
mi: column.text(),
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
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 column to unique', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
age: column.number({ unique: true }),
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
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 column', async () => {
const userFinal = {
...userInitial,
columns: {
...userInitial.columns,
birthday: column.date({ optional: true }),
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal(['ALTER TABLE "Users" ADD COLUMN "birthday" text']);
});
it('when adding a required column with default', async () => {
const defaultDate = new Date('2023-01-01');
const userFinal = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
birthday: column.date({ default: new Date('2023-01-01') }),
},
});
const { queries } = await userChangeQueries(userInitial, userFinal);
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 columns', async () => {
const userFinal = {
...userInitial,
columns: {
name: userInitial.columns.name,
email: userInitial.columns.email,
},
};
const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.deep.equal([
'ALTER TABLE "Users" DROP COLUMN "age"',
'ALTER TABLE "Users" DROP COLUMN "mi"',
]);
});
});
});
});
/** @param {string} query */
function getTempTableName(query) {
// eslint-disable-next-line regexp/no-unused-capturing-group
return query.match(/Users_([a-z\d]+)/)?.[0];
}