Skip to content

Commit

Permalink
feat(casts): support create and drop cast (#942)
Browse files Browse the repository at this point in the history
Co-authored-by: Shinigami92 <[email protected]>
  • Loading branch information
aschrab and Shinigami92 authored Apr 25, 2024
1 parent 569d173 commit ee76beb
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ Want to know more? Read docs:
- [Policies](policies.md)
- [Extensions](extensions.md)
- [Grants](grants.md)
- [Casts](casts.md)
- [Miscellaneous](misc.md)
- [Transpiling migrations](transpiling.md)
- [Troubleshooting](troubleshooting.md)
Expand Down
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Policies](policies.md)
- [Extensions](extensions.md)
- [Grants](grants.md)
- [Casts](casts.md)
- [Miscellaneous](misc.md)
- [Transpiling migrations](transpiling.md)
- [Troubleshooting](troubleshooting.md)
45 changes: 45 additions & 0 deletions docs/casts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Cast Operations

### `pgm.createCast( source_type, target_type, opttions )`

> Create a new cast - [postgres docs](https://www.postgresql.org/docs/current/sql-createcast.html)
**Arguments**

- `source_type` _[string]_ - The name of the source data type of the cast
- `target_type` _[string]_ - The name of the target data type of the cast
- `options` _[object]_ - options:

- `functionName` _[string]_ - Name of function to use to do the cast
- `argumentTypes` _[array]_ - Array of types of arguments for the function

Array of strings listing the types of arguments for the conversion
function. If this is not present, it defaults to just the `source_type`.

- `inout` _[boolean]_ - Use standard I/O routines for conversion

Setting this to `true` indicates that conversion should be used by using
the standard text output conversion for `source_type` and passing the
result to the input conversion process for `target_type`.

- `as` _[string]_ - Indicate when this may cast may be done implicitly.

This may be either `assignment` or `implicit`. If `implicit` is used, the
cast may be used implicitly in any context; this is not recommended. If
`assignment` is used the cast will only be done implicitly in
assignments.

**Reverse Operation:** `dropCast`

---

### `pgm.dropCast( source_type, target_type )`

> Drop a cast - [postgres docs](https://www.postgresql.org/docs/current/sql-dropcast.html)
**Arguments**

- `source_type` _[string]_ - The name of the source data type of the cast
- `target_type` _[string]_ - The name of the target data type of the cast
- `options` _[object]_ - options:
- `ifExists` _[boolean]_ - Do not throw an error if the cast does not exist
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { Migration } from './migration';
export type {
CreateCast,
CreateCastFn,
CreateCastOptions,
DropCast,
} from './operations/casts';
export type {
AlterDomain,
CreateDomain,
Expand Down
8 changes: 8 additions & 0 deletions src/migrationBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
and it makes inference of down migrations possible.
*/

import * as casts from './operations/casts';
import * as domains from './operations/domains';
import * as extensions from './operations/extensions';
import * as functions from './operations/functions';
Expand Down Expand Up @@ -315,6 +316,10 @@ export default class MigrationBuilderImpl implements MigrationBuilder {
...args: Parameters<grants.RevokeOnTables>
) => void;

public readonly createCast: (...args: Parameters<casts.CreateCast>) => void;

public readonly dropCast: (...args: Parameters<casts.DropCast>) => void;

public readonly sql: (...args: Parameters<sql.Sql>) => void;

public readonly func: (sql: string) => PgLiteral;
Expand Down Expand Up @@ -472,6 +477,9 @@ export default class MigrationBuilderImpl implements MigrationBuilder {
this.grantOnTables = wrap(grants.grantOnTables(options));
this.revokeOnTables = wrap(grants.revokeOnTables(options));

this.createCast = wrap(casts.createCast(options));
this.dropCast = wrap(casts.dropCast(options));

this.sql = wrap(sql.sql(options));

// Other utilities which may be useful
Expand Down
62 changes: 62 additions & 0 deletions src/operations/casts/createCast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { MigrationOptions } from '../../types';
import type { Name, Reversible } from '../generalTypes';
import type { DropCastOptions } from './dropCast';
import { dropCast } from './dropCast';

export interface CreateCastWithFunctionOptions {
functionName: Name;
argumentTypes?: string[];
inout?: undefined;
}

export interface CreateCastWithoutFunctionOptions {
functionName?: undefined;
argumentTypes?: undefined;
inout?: undefined;
}

export interface CreateCastWithInoutOptions {
functionName?: undefined;
argumentTypes?: undefined;
inout: boolean;
}

export type CreateCastOptions = (
| CreateCastWithFunctionOptions
| CreateCastWithoutFunctionOptions
| CreateCastWithInoutOptions
) & {
as?: 'ASSIGNMENT' | 'IMPLICIT';
};

export type CreateCastFn = (
fromType: string,
toType: string,
options: CreateCastOptions & DropCastOptions
) => string;

export type CreateCast = Reversible<CreateCastFn>;

export function createCast(mOptions: MigrationOptions): CreateCast {
const _create: CreateCast = (sourceType, targetType, options = {}) => {
const { functionName, argumentTypes, inout, as } = options;

let conversion = '';
if (functionName) {
const args = argumentTypes || [sourceType];
conversion = ` WITH FUNCTION ${mOptions.literal(functionName)}(${args.join(', ')})`;
} else if (inout) {
conversion = ' WITH INOUT';
} else {
conversion = ' WITHOUT FUNCTION';
}

const implicit = as ? ` AS ${as}` : '';

return `CREATE CAST (${sourceType} AS ${targetType})${conversion}${implicit};`;
};

_create.reverse = dropCast(mOptions);

return _create;
}
22 changes: 22 additions & 0 deletions src/operations/casts/dropCast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { MigrationOptions } from '../../types';
import type { DropOptions } from '../generalTypes';

export type DropCastOptions = Omit<DropOptions, 'cascade'>;

export type DropCast = (
fromType: string,
toType: string,
dropOptions: DropCastOptions
) => string;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function dropCast(mOptions: MigrationOptions): DropCast {
const _drop: DropCast = (sourceType, targetType, options = {}) => {
const { ifExists } = options;
const ifExistsStr = ifExists ? ' IF EXISTS' : '';

return `DROP CAST${ifExistsStr} (${sourceType} AS ${targetType});`;
};

return _drop;
}
2 changes: 2 additions & 0 deletions src/operations/casts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './createCast';
export * from './dropCast';
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
QueryConfig,
QueryResult,
} from 'pg';
import type * as casts from './operations/casts';
import type * as domains from './operations/domains';
import type * as extensions from './operations/extensions';
import type * as functions from './operations/functions';
Expand Down Expand Up @@ -609,6 +610,19 @@ export interface MigrationBuilder {
*/
revokeOnTables: (...args: Parameters<grants.RevokeOnTables>) => void;

/**
* Define a new cast.
*
* @see https://www.postgresql.org/docs/current/sql-createcast.html
*/
createCast: (...args: Parameters<casts.CreateCast>) => void;
/**
* Remove a cast.
*
* @see https://www.postgresql.org/docs/current/sql-dropcast.html
*/
dropCast: (...args: Parameters<casts.DropCast>) => void;

/**
* Run raw SQL, with some optional _[very basic](http://mir.aculo.us/2011/03/09/little-helpers-a-tweet-sized-javascript-templating-engine/)_ mustache templating.
*
Expand Down
25 changes: 25 additions & 0 deletions test/migrations/090_create_cast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
exports.up = (pgm) => {
pgm.createCast('varchar', 'integer', {
inout: true,
as: 'IMPLICIT',
});

pgm.createFunction(
'text_to_integer',
['text'],
{
returns: 'integer',
language: 'plpgsql',
},
`
BEGIN
RETURN CAST($1 AS integer);
END;
`
);
pgm.createCast('text', 'integer', {
functionName: 'text_to_integer',
argumentTypes: ['text'],
as: 'ASSIGNMENT',
});
};
94 changes: 94 additions & 0 deletions test/operations/casts/createCast.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest';
import { PgType } from '../../../src';
import { createCast } from '../../../src/operations/casts';
import { options1 } from '../../presetMigrationOptions';

describe('operations', () => {
describe('casts', () => {
describe('createCast', () => {
const createCastFn = createCast(options1);

it('should return a function', () => {
expect(createCastFn).toBeTypeOf('function');
});

it('should return sql statement with string', () => {
const statement = createCastFn('bigint', 'int4', {});

expect(statement).toBeTypeOf('string');
expect(statement).toBe(
'CREATE CAST (bigint AS int4) WITHOUT FUNCTION;'
);
});

it('should return sql statement with PgType', () => {
const statement = createCastFn(PgType.BIGINT, PgType.INT4, {});

expect(statement).toBeTypeOf('string');
expect(statement).toBe(
'CREATE CAST (bigint AS int4) WITHOUT FUNCTION;'
);
});

it('should return sql statement with schema', () => {
const statement = createCastFn('bigint', 'int4', {
functionName: { name: 'add', schema: 'myschema' },
});

expect(statement).toBeTypeOf('string');
expect(statement).toBe(
'CREATE CAST (bigint AS int4) WITH FUNCTION "myschema"."add"(bigint);'
);
});

it('should return sql statement with castOptions with function', () => {
const statement = createCastFn('bigint', 'int4', {
functionName: 'add',
argumentTypes: ['integer', 'integer'],
as: 'ASSIGNMENT',
});

expect(statement).toBeTypeOf('string');
expect(statement).toBe(
'CREATE CAST (bigint AS int4) WITH FUNCTION "add"(integer, integer) AS ASSIGNMENT;'
);
});

it('should return sql statement with castOptions without function', () => {
const statement = createCastFn('bigint', 'int4', {
as: 'IMPLICIT',
});

expect(statement).toBeTypeOf('string');
expect(statement).toBe(
'CREATE CAST (bigint AS int4) WITHOUT FUNCTION AS IMPLICIT;'
);
});

it('should return sql statement with castOptions with inout', () => {
const statement = createCastFn('bigint', 'int4', {
inout: true,
as: 'ASSIGNMENT',
});

expect(statement).toBeTypeOf('string');
expect(statement).toBe(
'CREATE CAST (bigint AS int4) WITH INOUT AS ASSIGNMENT;'
);
});

describe('reverse', () => {
it('should contain a reverse function', () => {
expect(createCastFn.reverse).toBeTypeOf('function');
});

it('should return sql statement', () => {
const statement = createCastFn.reverse('bigint', 'int4', {});

expect(statement).toBeTypeOf('string');
expect(statement).toBe('DROP CAST (bigint AS int4);');
});
});
});
});
});
31 changes: 31 additions & 0 deletions test/operations/casts/dropCast.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest';
import { dropCast } from '../../../src/operations/casts';
import { options1 } from '../../presetMigrationOptions';

describe('operations', () => {
describe('casts', () => {
describe('dropCast', () => {
const dropCastFn = dropCast(options1);

it('should return a function', () => {
expect(dropCastFn).toBeTypeOf('function');
});

it('should return sql statement', () => {
const statement = dropCastFn('bigint', 'int4', {});

expect(statement).toBeTypeOf('string');
expect(statement).toBe('DROP CAST (bigint AS int4);');
});

it('should return sql statement with dropOptions', () => {
const statement = dropCastFn('bigint', 'int4', {
ifExists: true,
});

expect(statement).toBeTypeOf('string');
expect(statement).toBe('DROP CAST IF EXISTS (bigint AS int4);');
});
});
});
});

0 comments on commit ee76beb

Please sign in to comment.