Skip to content

Commit

Permalink
Improve columns casting to snake_case in migrations and code gen (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
romeerez committed Jun 26, 2024
1 parent 52f3f04 commit f278b19
Show file tree
Hide file tree
Showing 37 changed files with 1,615 additions and 2,140 deletions.
13 changes: 13 additions & 0 deletions .changeset/nice-eggs-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'rake-db': minor
'pqb': minor
'orchid-core': minor
'orchid-orm': minor
---

Improve column casting to snake case in migrations and code gen:

When the `snakeCase` option is enabled, columns can be written in camelCase in all contexts,
and will be translated to snake_case.

This includes columns in primary keys, indexes, foreign keys options.
6 changes: 6 additions & 0 deletions BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Breaking changes

## orchid-orm 1.32.0

Improved casting to snake_case, so the columns are translated to snake_case in primary key, indexes, foreign key options.

This may break existing migrations if you had `snakeCase: true` and for some reason have some of the columns in `camelCase` in the db.

## orchid-orm 1.31.2

Removing `primaryKey`, `foreignKey`, `associationForeignKey`, and such, as options for `belongsTo`, `hasMany`, etc.
Expand Down
20 changes: 19 additions & 1 deletion packages/core/src/columns/columnType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RawSQLBase } from '../raw';
import { QueryBaseCommon } from '../query';
import { OperatorBase } from './operators';
import { ColumnTypeSchemaArg } from './columnSchema';
import { RecordString } from '../utils';
import { RecordString, toSnakeCase } from '../utils';

// get columns object type where nullable columns or columns with a default are optional
export type ColumnShapeInput<
Expand Down Expand Up @@ -794,3 +794,21 @@ export abstract class ColumnTypeBase<
return setColumnData(this, 'isHidden', true) as never;
}
}

export const columnToCode = (
key: string,
column: ColumnTypeBase,
snakeCase: boolean | undefined,
) => {
if (snakeCase) key = toSnakeCase(key);

if (column.data.name === key) {
const name = column.data.name;
column.data.name = undefined;
const code = column.toCode('t', true);
column.data.name = name;
return code;
} else {
return column.toCode('t', true);
}
};
7 changes: 4 additions & 3 deletions packages/orm/src/migrations/generate/generate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QueryColumn } from 'orchid-core';
import { QueryColumn, toCamelCase } from 'orchid-core';
import {
Adapter,
AdapterOptions,
Expand Down Expand Up @@ -407,7 +407,7 @@ const processHasAndBelongsToManyColumn = (
const prev = habtmTables.get(q.table);
if (prev) {
for (const key in q.shape) {
if (q.shape[key] !== prev.shape[key]) {
if (q.shape[key].dataType !== prev.shape[key]?.dataType) {
throw new Error(
`Column ${key} in ${q.table} in hasAndBelongsToMany relation does not match with the relation on the other side`,
);
Expand All @@ -424,11 +424,12 @@ const processHasAndBelongsToManyColumn = (
const column = Object.create(joinTable.shape[key]);
column.data = {
...column.data,
name: column.data.name ?? key,
identity: undefined,
primaryKey: undefined,
default: undefined,
};
shape[key] = column;
shape[toCamelCase(key)] = column;
}
joinTable.shape = shape;
joinTable.internal = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ describe('checks', () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
column: t.integer(),
colUmn: t.integer(),
}));
},
tables: [
table((t) => ({
column: t.integer().check(t.sql`"column" = 42`),
colUmn: t.integer().check(t.sql`"col_umn" = 42`),
})),
],
});
Expand All @@ -38,26 +38,26 @@ describe('checks', () => {
change(async (db) => {
await db.changeTable('table', (t) => ({
...t.add(
t.check(t.sql\`"column" = 42\`)
t.check(t.sql\`"col_umn" = 42\`)
),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${green('+ add check')} "column" = 42`);
${green('+ add check')} "col_umn" = 42`);
});

it('should drop a column check', async () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
column: t.integer().check(t.sql`"column" = 42`),
colUmn: t.integer().check(t.sql`"col_umn" = 42`),
}));
},
tables: [
table((t) => ({
column: t.integer(),
colUmn: t.integer(),
})),
],
});
Expand All @@ -69,26 +69,26 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
...t.drop(
t.check(t.sql\`("column" = 42)\`, 'table_column_check')
t.check(t.sql\`(col_umn = 42)\`, 'table_col_umn_check')
),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${red('- drop check')} ("column" = 42)`);
${red('- drop check')} (col_umn = 42)`);
});

it('should not recreate a column check when it is identical', async () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
id: t.integer().check(t.sql`id != 123`),
iD: t.integer().check(t.sql`i_d != 123`),
}));
},
tables: [
table((t) => ({
id: t.integer().check(t.sql`id != 123`),
iD: t.integer().check(t.sql`i_d != 123`),
})),
],
});
Expand All @@ -102,12 +102,12 @@ change(async (db) => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
id: t.integer().check(t.sql`id = 123`),
iD: t.integer().check(t.sql`i_d = 123`),
}));
},
tables: [
table((t) => ({
id: t.integer().check(t.sql`id != 123`),
iD: t.integer().check(t.sql`i_d != 123`),
})),
],
});
Expand All @@ -119,33 +119,33 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
...t.drop(
t.check(t.sql\`(id = 123)\`, 'table_id_check')
t.check(t.sql\`(i_d = 123)\`, 'table_i_d_check')
),
...t.add(
t.check(t.sql\`id != 123\`)
t.check(t.sql\`i_d != 123\`)
),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${red('- drop check')} (id = 123)
${green('+ add check')} id != 123`);
${red('- drop check')} (i_d = 123)
${green('+ add check')} i_d != 123`);
});

it('should create a table check', async () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
id: t.integer(),
iD: t.integer(),
}));
},
tables: [
table(
(t) => ({
id: t.integer(),
iD: t.integer(),
}),
(t) => t.check(t.sql`"id" = 42`),
(t) => t.check(t.sql`"i_d" = 42`),
),
],
});
Expand All @@ -157,14 +157,14 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
...t.add(
t.check(t.sql\`"id" = 42\`)
t.check(t.sql\`"i_d" = 42\`)
),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${green('+ add check')} "id" = 42`);
${green('+ add check')} "i_d" = 42`);
});

it('should be added together with a column', async () => {
Expand All @@ -174,7 +174,7 @@ change(async (db) => {
},
tables: [
table((t) => ({
id: t.integer().check(t.sql`"id" = 5`),
iD: t.integer().check(t.sql`"i_d" = 5`),
})),
],
});
Expand All @@ -185,20 +185,20 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
id: t.add(t.integer().check(t.sql\`"id" = 5\`)),
iD: t.add(t.integer().check(t.sql\`"i_d" = 5\`)),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${green('+ add column')} id integer, checks "id" = 5`);
${green('+ add column')} iD integer, checks "i_d" = 5`);
});

it('should be dropped together with a column', async () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
id: t.integer().check(t.sql`id = 123`),
iD: t.integer().check(t.sql`i_d = 123`),
}));
},
tables: [table()],
Expand All @@ -210,25 +210,25 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
id: t.drop(t.integer().check(t.sql\`(id = 123)\`)),
iD: t.drop(t.integer().check(t.sql\`(i_d = 123)\`)),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${red('- drop column')} id integer, checks (id = 123)`);
${red('- drop column')} iD integer, checks (i_d = 123)`);
});

it('should be added in a column change', async () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
id: t.integer().nullable(),
iD: t.integer().nullable(),
}));
},
tables: [
table((t) => ({
id: t.integer().check(t.sql`"id" = 5`),
iD: t.integer().check(t.sql`"i_d" = 5`),
})),
],
});
Expand All @@ -239,27 +239,27 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
id: t.change(t.integer().nullable(), t.integer().check(t.sql\`"id" = 5\`)),
iD: t.change(t.integer().nullable(), t.integer().check(t.sql\`"i_d" = 5\`)),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${yellow('~ change column')} id:
${yellow('~ change column')} iD:
${yellow('from')}: t.integer().nullable()
${yellow('to')}: t.integer().check(t.sql\`"id" = 5\`)`);
${yellow('to')}: t.integer().check(t.sql\`"i_d" = 5\`)`);
});

it('should not be recreated when a column is renamed', async () => {
await arrange({
async prepareDb(db) {
await db.createTable('table', { noPrimaryKey: true }, (t) => ({
from: t.integer().check(t.sql`2 = 2`),
frOm: t.integer().check(t.sql`2 = 2`),
}));
},
tables: [
table((t) => ({
to: t.integer().check(t.sql`2 = 2`),
tO: t.integer().check(t.sql`2 = 2`),
})),
],
selects: [1],
Expand All @@ -271,12 +271,12 @@ change(async (db) => {
change(async (db) => {
await db.changeTable('table', (t) => ({
from: t.rename('to'),
fr_om: t.rename('tO'),
}));
});
`);

assert.report(`${yellow('~ change table')} table:
${yellow('~ rename column')} from ${yellow('=>')} to`);
${yellow('~ rename column')} fr_om ${yellow('=>')} tO`);
});
});
Loading

0 comments on commit f278b19

Please sign in to comment.