diff --git a/src/client.ts b/src/client.ts index 92715df..a116914 100644 --- a/src/client.ts +++ b/src/client.ts @@ -75,8 +75,7 @@ export interface QueryBuilderInternal { selectRawValue?: string; // General operation values, such as when renaming tables referencing the old and new name - originalValue?: string; - newValue?: string; + newTableName?: string; } function buildWhereClause(args: unknown[]): WhereCondition | WhereClaues { @@ -329,9 +328,21 @@ class QueryBuilderAlterTable extends IQueryBuilder { this.state.table = tableName; } + alterColumn(columnName: string, definition: TableColumnDefinition) { + this.state.action = QueryBuilderAction.UPDATE_COLUMNS; + this.state.columns.push({ name: columnName, definition }); + return this; + } + + addColumn(name: string, definition: TableColumnDefinition) { + this.state.action = QueryBuilderAction.ADD_COLUMNS; + this.state.columns.push({ name, definition }); + return this; + } + renameTable(newTableName: string) { - this.state.originalValue = this.state.table; - this.state.newValue = newTableName; + this.state.action = QueryBuilderAction.RENAME_TABLE; + this.state.newTableName = newTableName; return this; } @@ -439,37 +450,14 @@ function buildQueryString( return dialect.createTable(queryBuilder); case QueryBuilderAction.DELETE_TABLE: return dialect.dropTable(queryBuilder); - // case QueryBuilderAction.RENAME_TABLE: - // query.query = dialect.renameTable( - // queryBuilder, - // queryType, - // query - // ).query; - // break; - - // case QueryBuilderAction.ADD_COLUMNS: - // query.query = dialect.addColumn( - // queryBuilder, - // queryType, - // query - // ).query; - // break; - // case QueryBuilderAction.DROP_COLUMNS: - // query.query = dialect.dropColumn( - // queryBuilder, - // queryType, - // query - // ).query; - // break; + case QueryBuilderAction.RENAME_TABLE: + return dialect.renameTable(queryBuilder); + case QueryBuilderAction.ADD_COLUMNS: + return dialect.addColumn(queryBuilder); + case QueryBuilderAction.UPDATE_COLUMNS: + return dialect.alterColumn(queryBuilder); case QueryBuilderAction.RENAME_COLUMNS: return dialect.renameColumn(queryBuilder); - // case QueryBuilderAction.UPDATE_COLUMNS: - // query.query = dialect.updateColumn( - // queryBuilder, - // queryType, - // query - // ).query; - // break; default: throw new Error('Invalid action'); } diff --git a/src/connections/index.ts b/src/connections/index.ts index 9b695d7..f0c3300 100644 --- a/src/connections/index.ts +++ b/src/connections/index.ts @@ -1,5 +1,9 @@ import { Query } from '../query'; -import { Database, TableColumn } from '../models/database'; +import { + Database, + TableColumn, + TableColumnDefinition, +} from '../models/database'; import { AbstractDialect } from 'src/query-builder'; import { Outerbase } from 'src/client'; @@ -88,6 +92,26 @@ export abstract class Connection { columnName: string, newColumnName: string ): Promise; + + abstract renameTable( + schemaName: string | undefined, + tableName: string, + newTableName: string + ): Promise; + + abstract alterColumn( + schemaName: string | undefined, + tableName: string, + columnName: string, + defintion: TableColumnDefinition + ): Promise; + + abstract addColumn( + schemaName: string | undefined, + tableName: string, + columnName: string, + defintion: TableColumnDefinition + ): Promise; } export abstract class SqlConnection extends Connection { @@ -246,4 +270,57 @@ export abstract class SqlConnection extends Connection { .toQuery() ); } + + async renameTable( + schemaName: string | undefined, + tableName: string, + newTableName: string + ): Promise { + const qb = Outerbase(this); + + return await this.query( + qb + .alterTable( + schemaName ? `${schemaName}.${tableName}` : tableName + ) + .renameTable(newTableName) + .toQuery() + ); + } + + async alterColumn( + schemaName: string | undefined, + tableName: string, + columnName: string, + defintion: TableColumnDefinition + ): Promise { + const qb = Outerbase(this); + + return await this.query( + qb + .alterTable( + schemaName ? `${schemaName}.${tableName}` : tableName + ) + .alterColumn(columnName, defintion) + .toQuery() + ); + } + + async addColumn( + schemaName: string | undefined, + tableName: string, + columnName: string, + defintion: TableColumnDefinition + ): Promise { + const qb = Outerbase(this); + + return await this.query( + qb + .alterTable( + schemaName ? `${schemaName}.${tableName}` : tableName + ) + .addColumn(columnName, defintion) + .toQuery() + ); + } } diff --git a/src/connections/mongodb.ts b/src/connections/mongodb.ts index 9b7606b..ce27298 100644 --- a/src/connections/mongodb.ts +++ b/src/connections/mongodb.ts @@ -3,6 +3,7 @@ import { Database, Table, TableColumn, + TableColumnDefinition, TableIndex, TableIndexType, } from '../models/database'; @@ -296,4 +297,27 @@ export class MongoDBConnection implements Connection { [this.defaultDatabase]: tableList, }; } + + async addColumn(): Promise { + // Do nothing, MongoDB does not have a schema + return createOkResult(); + } + + async alterColumn(): Promise { + // Do nothing, MongoDB does not have a schema + return createOkResult(); + } + + async renameTable( + schemaName: string | undefined, + tableName: string, + newTableName: string + ): Promise { + await this.client + .db(schemaName ?? this.defaultDatabase) + .collection(tableName) + .rename(newTableName); + + return createOkResult(); + } } diff --git a/src/connections/neon-http.bk.txt b/src/connections/neon-http.bk.txt deleted file mode 100644 index 039e2f6..0000000 --- a/src/connections/neon-http.bk.txt +++ /dev/null @@ -1,90 +0,0 @@ -import { Client } from '@neondatabase/serverless'; -import ws from 'ws'; -import { Connection } from './index'; -import { Query, constructRawQuery } from '../query'; -import { QueryParamsPositional, QueryType } from '../query-params'; -import { PostgresDialect } from '../query-builder/dialects/postgres'; - -export type NeonConnectionDetails = { - databaseUrl: string -}; - -export class NeonHttpConnection implements Connection { - databaseUrl: string; - client: Client; - - // Default query type to named for Outerbase - queryType = QueryType.positional - - // Default dialect for Cloudflare - dialect = new PostgresDialect() - - /** - * Creates a new NeonHttpConnection object with the provided API key, - * account ID, and database ID. - * - * @param databaseUrl - The URL to the database to be used for the connection. - */ - constructor(private _: NeonConnectionDetails) { - this.databaseUrl = _.databaseUrl; - - this.client = new Client(this.databaseUrl); - this.client.neonConfig.webSocketConstructor = ws; - } - - /** - * Performs a connect action on the current Connection object. - * - * @param details - Unused in the Neon scenario. - * @returns Promise - */ - async connect(): Promise { - return this.client.connect(); - } - - /** - * Performs a disconnect action on the current Connection object. - * - * @returns Promise - */ - async disconnect(): Promise { - return this.client.end(); - } - - /** - * Triggers a query action on the current Connection object. The query - * is a SQL query that will be executed on a Neon database. Neon's driver - * requires positional parameters to be used in the specific format of `$1`, - * `$2`, etc. The query is sent to the Neon database and the response is returned. - * - * @param query - The SQL query to be executed. - * @param parameters - An object containing the parameters to be used in the query. - * @returns Promise<{ data: any, error: Error | null }> - */ - async query(query: Query): Promise<{ data: any; error: Error | null; query: string }> { - let items = null - let error = null - - // Replace all `?` with `$1`, `$2`, etc. - let index = 0; - const formattedQuery = query.query.replace(/\?/g, () => `$${++index}`); - - try { - await this.client.query('BEGIN'); - const { rows } = await this.client.query(formattedQuery, query.parameters as QueryParamsPositional); - items = rows; - await this.client.query('COMMIT'); - } catch (error) { - await this.client.query('ROLLBACK'); - throw error; - } - - const rawSQL = constructRawQuery(query) - - return { - data: items, - error: error, - query: rawSQL - }; - } -}; \ No newline at end of file diff --git a/src/playground.ts b/src/playground.ts index d522c07..6719e08 100644 --- a/src/playground.ts +++ b/src/playground.ts @@ -1,40 +1,55 @@ -import { createConnection } from 'mysql2'; -import { Connection, MySQLConnection, QueryResult } from '.'; - -function log(result: QueryResult) { - console.log('Result'); - console.table(result.data); - - console.log('Headers'); - console.table(result.headers); -} - -async function run(db: Connection, sql: string) { - console.log('------------------------------'); - console.log(`\x1b[32m${sql}\x1b[0m`); - console.log('------------------------------'); - - log(await db.raw(sql)); -} - -async function main() { - const db = new MySQLConnection( - createConnection({ - host: 'localhost', - user: 'root', - password: '123456', - database: 'testing', - }) - ); - - await run(db, 'SELECT 1 AS `a`, 2 AS `a`;'); - - await run( - db, - 'SELECT * FROM students INNER JOIN teachers ON (students.teacher_id = teachers.id)' - ); -} - -main() - .then() - .finally(() => process.exit()); +// import { createConnection } from 'mysql2'; +// import { Connection, MySQLConnection, QueryResult } from '.'; + +// function log(result: QueryResult) { +// console.log('Result'); +// console.table(result.data); + +// console.log('Headers'); +// console.table(result.headers); +// } + +// async function run(db: Connection, sql: string) { +// console.log('------------------------------'); +// console.log(`\x1b[32m${sql}\x1b[0m`); +// console.log('------------------------------'); + +// log(await db.raw(sql)); +// } + +// async function main() { +// const db = new MySQLConnection( +// createConnection({ +// host: 'localhost', +// user: 'root', +// password: '123456', +// database: 'testing', +// }) +// ); + +// await run(db, 'SELECT 1 AS `a`, 2 AS `a`;'); + +// await run( +// db, +// 'SELECT * FROM students INNER JOIN teachers ON (students.teacher_id = teachers.id)' +// ); +// } + +// main() +// .then() +// .finally(() => process.exit()); + +import duckDB from 'duckdb'; + +const client = new duckDB.Database('md:my_db', { + motherduck_token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImludmlzYWxAZ21haWwuY29tIiwic2Vzc2lvbiI6ImludmlzYWwuZ21haWwuY29tIiwicGF0IjoiVkdfZ1BmRXdaWjN5M29zY0VFemRLWElMVVJ4ZmxFdUpxbktZM3RkVjEtUSIsInVzZXJJZCI6ImVkZjQ4NjAyLTJlZmMtNGU0Ny04Y2VmLWNhNGU5NzQ3OTQ0MSIsImlzcyI6Im1kX3BhdCIsImlhdCI6MTcyOTEzMDcxMX0.ysqXODqC9BpMeOBeedjQW0y6GfiMdpOgHBy1OihUtKI', +}); + +client + .connect() + .prepare('SELECT 1;') + .all((err, res) => { + console.log(res); + process.exit(); + }); diff --git a/src/query-builder/index.ts b/src/query-builder/index.ts index abed014..e7c865c 100644 --- a/src/query-builder/index.ts +++ b/src/query-builder/index.ts @@ -16,7 +16,10 @@ interface Dialect { delete(builder: QueryBuilderInternal): Query; createTable(builder: QueryBuilderInternal): Query; dropTable(builder: QueryBuilderInternal): Query; + renameTable(builder: QueryBuilderInternal): Query; renameColumn(builder: QueryBuilderInternal): Query; + alterColumn(builder: QueryBuilderInternal): Query; + addColumn(builder: QueryBuilderInternal): Query; } export enum ColumnDataType { @@ -396,6 +399,84 @@ export abstract class AbstractDialect implements Dialect { }; } + renameTable(builder: QueryBuilderInternal): Query { + const tableName = builder.table; + const newTableName = builder.newTableName; + + if (!tableName) { + throw new Error( + 'Table name is required to build a RENAME TABLE query.' + ); + } + + if (!newTableName) { + throw new Error('New table name is required to rename.'); + } + + return { + query: `ALTER TABLE ${this.escapeId(tableName)} RENAME TO ${this.escapeId(newTableName)}`, + parameters: [], + }; + } + + alterColumn(builder: QueryBuilderInternal): Query { + const tableName = builder.table; + + if (!tableName) { + throw new Error( + 'Table name is required to build a ALTER COLUMN query.' + ); + } + + if (builder.columns.length !== 1) { + throw new Error('Exactly one column is required to alter.'); + } + + const column = builder.columns[0]; + + if (!column.name) { + throw new Error('Column name is required to alter.'); + } + + if (!column.definition) { + throw new Error('Column definition is required to alter.'); + } + + return { + query: `ALTER TABLE ${this.escapeId(tableName)} ALTER COLUMN ${this.escapeId(column.name)} ${this.buildColumnDefinition(column.definition)}`, + parameters: [], + }; + } + + addColumn(builder: QueryBuilderInternal): Query { + const tableName = builder.table; + + if (!tableName) { + throw new Error( + 'Table name is required to build a ADD COLUMN query.' + ); + } + + if (builder.columns.length !== 1) { + throw new Error('Exactly one column is required to add.'); + } + + const column = builder.columns[0]; + + if (!column.name) { + throw new Error('Column name is required to add.'); + } + + if (!column.definition) { + throw new Error('Column definition is required to add.'); + } + + return { + query: `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${this.escapeId(column.name)} ${this.buildColumnDefinition(column.definition)}`, + parameters: [], + }; + } + delete(builder: QueryBuilderInternal): Query { const tableName = builder.table; diff --git a/tests/connections/connection.test.ts b/tests/connections/connection.test.ts index 0ff4641..e62a043 100644 --- a/tests/connections/connection.test.ts +++ b/tests/connections/connection.test.ts @@ -5,7 +5,7 @@ beforeAll(async () => { await db.connect(); // It is better to cleanup here in case any previous test failed - await db.dropTable(DEFAULT_SCHEMA, 'persons'); + await db.dropTable(DEFAULT_SCHEMA, 'people'); }); afterAll(async () => { @@ -169,8 +169,30 @@ describe('Database Connection', () => { ]); }); - test('Delete a row', async () => { - await db.delete(DEFAULT_SCHEMA, 'persons', { id: 1 }); + test('Add table column', async () => { + const { error } = await db.addColumn( + DEFAULT_SCHEMA, + 'persons', + 'email', + { + type: + process.env.CONNECTION_TYPE === 'bigquery' + ? 'STRING' + : 'VARCHAR(255)', + } + ); + + expect(error).not.toBeTruthy(); + + // Need to update email because MongoDB does not have schema + await db.update( + DEFAULT_SCHEMA, + 'persons', + { + email: 'test@outerbase.com', + }, + {} + ); const { data } = await db.select(DEFAULT_SCHEMA, 'persons', { orderBy: ['id'], @@ -178,6 +200,49 @@ describe('Database Connection', () => { offset: 0, }); + expect(cleanup(data)).toEqual([ + { + id: 1, + full_name: 'Visal In', + age: 25, + email: 'test@outerbase.com', + }, + { + id: 2, + full_name: 'Outerbase', + age: 30, + email: 'test@outerbase.com', + }, + ]); + }); + + test('Rename table name', async () => { + const { error } = await db.renameTable( + DEFAULT_SCHEMA, + 'persons', + 'people' + ); + + expect(error).not.toBeTruthy(); + + const { data } = await db.select(DEFAULT_SCHEMA, 'people', { + orderBy: ['id'], + limit: 1000, + offset: 0, + }); + + expect(cleanup(data).length).toEqual(2); + }); + + test('Delete a row', async () => { + await db.delete(DEFAULT_SCHEMA, 'people', { id: 1 }); + + const { data } = await db.select(DEFAULT_SCHEMA, 'people', { + orderBy: ['id'], + limit: 1000, + offset: 0, + }); + expect(cleanup(data)).toEqual([ { id: 2, full_name: 'Outerbase', age: 30 }, ]);