diff --git a/package-lock.json b/package-lock.json index 65a7eab..8040297 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sqm", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sqm", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "ts-pattern": "^5.8.0" @@ -1027,6 +1027,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.24" @@ -2066,6 +2067,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3032,6 +3034,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3081,6 +3084,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3542,6 +3546,7 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -3790,6 +3795,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3828,6 +3834,7 @@ "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3926,6 +3933,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/package.json b/package.json index a553ee0..609f690 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ ], "repository": { "type": "git", - "url": "https://github.com/NickRMD/queryMaker.git" + "url": "git+https://github.com/NickRMD/queryMaker.git" }, "homepage": "https://github.com/NickRMD/queryMaker#readme", "bugs": { diff --git a/src/cteMaker.ts b/src/cteMaker.ts index c95d072..19efa72 100644 --- a/src/cteMaker.ts +++ b/src/cteMaker.ts @@ -1,5 +1,5 @@ -import QueryDefinition from "./queryKinds/query.js"; -import SelectQuery from "./queryKinds/select.js"; +import QueryDefinition from "./queryKinds/dml/dmlQueryDefinition.js"; +import SelectQuery from "./queryKinds/dml/select.js"; /** * Cte represents a Common Table Expression (CTE) in SQL. diff --git a/src/index.ts b/src/index.ts index 12a1620..9bb1d29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import Query from "./queryMaker.js"; import Statement from "./statementMaker.js"; -import CteMaker from "./cteMaker.js"; +export * from './cteMaker.js' +export { default as CteMaker } from './cteMaker.js' export { Query, - Statement, - CteMaker + Statement } diff --git a/src/queryKinds/ddl/ddlQueryDefinition.ts b/src/queryKinds/ddl/ddlQueryDefinition.ts new file mode 100644 index 0000000..1fe46a4 --- /dev/null +++ b/src/queryKinds/ddl/ddlQueryDefinition.ts @@ -0,0 +1,244 @@ +import SqlEscaper from "../../sqlEscaper.js"; +import QueryKind from "../../types/QueryKind"; +import sqlFlavor from "../../types/sqlFlavor.js"; + +/** + * An array of function names that can be used to execute SQL queries. + * These functions are commonly found in database client libraries. + */ +const functionNames = ['execute', 'query', 'run', 'all', 'get'] as const; + +/** + * FunctionDeclaration type defines the signature for functions that execute SQL queries. + * It takes a query string and an array of parameters, and returns a promise that resolves + * with any result. + */ +type FunctionDeclaration = (query: string, params?: any[]) => Promise; + +/** + * QueryExecutorObject interface defines the structure for an object that can execute SQL queries. + * It includes optional methods for executing queries in different ways, as well as an optional manager property. + */ +interface QueryExecutorObject { + execute?: FunctionDeclaration; + query?: FunctionDeclaration; + run?: FunctionDeclaration; + all?: FunctionDeclaration; + get?: FunctionDeclaration; + manager?: QueryExecutor; +} + +/** + * QueryExecutor type can be either a QueryExecutorObject or a function that executes a query. + */ +type QueryExecutor = + QueryExecutorObject + | FunctionDeclaration; + +/** + * Abstract class DdlQueryDefinition serves as a blueprint for defining DDL (Data Definition Language) query structures. + * It is intended to be extended by specific DDL query classes such as CreateTableQuery, AlterTableQuery, and DropTableQuery. + * This class will provide common properties and methods that are shared among all DDL query types and also some abstract methods + * that must be implemented by the subclasses to ensure they adhere to a consistent interface for building DDL queries. + */ +export default abstract class DdlQueryDefinition { + /** The name of the table involved in the DDL operation. */ + protected tableName: string = ''; + + /** The built DDL query string, initialized to null. */ + protected builtQuery: string | string[] | null = null; + + /** + * Sets the name of the table for the DDL operation. + * @param name - The name of the table. + * @returns The current instance for method chaining. + */ + public table(name: string | null = null): this { + this.tableName = name ? SqlEscaper.escapeTableName(name, this.flavor) : ''; + return this; + } + + /** + * Checks if the DDL query has been built. + * @returns True if the query has been built, false otherwise. + */ + public isDone(): boolean { + return this.builtQuery !== null; + } + + /** + * Abstract method to build the DDL query string. + * This method must be implemented by subclasses to generate the appropriate SQL statement. + * @param deepAnalysis - Optional boolean to indicate if deep analysis is required (default is false). + * @returns The constructed DDL query string. + */ + public abstract build(deepAnalysis?: boolean): string | string[]; + + /** + * Builds an EXPLAIN query for the DDL operation. + * This method prefixes the built DDL query with "EXPLAIN". + * @param deepAnalysis - Optional boolean to indicate if deep analysis is required (default is false). + * @returns The constructed EXPLAIN query string. + */ + public buildExplain(deepAnalysis?: boolean): string { + return `EXPLAIN ${this.build(deepAnalysis)}`; + } + + /** + * Builds an EXPLAIN ANALYZE query for the DDL operation. + * This method prefixes the built DDL query with "EXPLAIN ANALYZE". + * @param deepAnalysis - Optional boolean to indicate if deep analysis is required (default is false). + * @returns The constructed EXPLAIN ANALYZE query string. + */ + public buildExplainAnalyze(deepAnalysis?: boolean): string { + return `EXPLAIN ANALYZE ${this.build(deepAnalysis)}`; + } + + /** + * Utility method to add indentation to each line of a given string. + * This is useful for formatting multi-line SQL queries for better readability. + * @param str - The input string to be indented. + * @param spaces - The number of spaces to indent each line (default is 0). + * @returns The indented string. + */ + protected spaceLines(str: string, spaces: number = 0): string { + const space = ' '.repeat(spaces); + return str.split('\n').map(line => space + line).join('\n'); + } + + /** + * Abstract method to clone the current DDL query definition instance. + * This method must be implemented by subclasses to return a new instance + * that is a copy of the current instance. + * @returns A new instance of the DDL query definition. + */ + public abstract clone(): DdlQueryDefinition; + + /** + * Abstract method to reset the state of the DDL query definition. + * This method must be implemented by subclasses to clear any set properties + * and return the instance to its initial state. + * @returns The current instance for method chaining. + */ + public abstract reset(): this; + + /** + * Abstract method to convert the DDL query definition to its SQL string representation. + * This method must be implemented by subclasses to return the SQL string + * that represents the DDL operation defined by the instance. + * @returns The SQL string representation of the DDL query. + */ + public abstract toSQL(): string | string[]; + + /** + * Abstract getter to retrieve the kind of DDL query. + * This property must be implemented by subclasses to return the specific + * type of DDL operation (e.g., 'CREATE', 'ALTER', 'DROP'). + */ + public abstract get kind(): QueryKind; + + /** + * The SQL flavor to use for escaping identifiers. + * Default is PostgreSQL. + */ + protected flavor: sqlFlavor = sqlFlavor.postgres; + + /** + * Schemas to be used in the query. + * This is useful for databases that support multiple schemas. + * NOTICE: SQL Injection is not checked in schema names. Be sure to use only trusted schema names. + */ + protected schemas: string[] = []; + + /** + * Sets the SQL flavor for escaping identifiers. + * @param flavor The SQL flavor to set. + * @returns The current DmlQueryDefinition instance for chaining. + */ + public sqlFlavor(flavor: sqlFlavor) { + this.flavor = flavor; + return this; + } + + /** + * Set schemas to be used in the query. + * This is useful for databases that support multiple schemas. + * NOTICE: SQL Injection is not checked in schema names. Be sure to use only trusted schema names. + * @param schemas The schemas to set. + * @returns The current SelectQuery instance for chaining. + */ + public schema(...schemas: string[]): this { + this.schemas = schemas; + return this; + } + + /** + * Adds schemas to the existing list of schemas. + * This is useful for databases that support multiple schemas. + * NOTICE: SQL Injection is not checked in schema names. Be sure to use only trusted schema names. + * @param schemas The schemas to add. + * @returns The current SelectQuery instance for chaining. + */ + public addSchema(...schemas: string[]): this { + this.schemas.push(...schemas); + return this; + } + + /** + * Executes the built SQL query using the provided query executor. + * The query executor can be a function or an object with methods to execute the query. + * The optional noManager parameter can be used to bypass the manager property if present. + * @param queryExecutor The executor to run the SQL query. + * @param noManager If true, bypasses the manager property of the executor object. + * @returns A promise that resolves when the query execution is complete. + * @throws An error if the provided query executor is invalid. + */ + public async execute( + queryExecutor: QueryExecutor, + noManager: boolean = false + ): Promise { + if (typeof queryExecutor === 'function') { + const builtQuery = this.build(); + if (Array.isArray(builtQuery)) { + for (const query of builtQuery) { + await queryExecutor(query); + } + } else { + await queryExecutor(builtQuery); + } + return; + } + + if (!noManager && 'manager' in queryExecutor && typeof queryExecutor?.manager === 'object') { + for (const functionName of functionNames) { + if (typeof queryExecutor.manager[functionName] === 'function') { + const builtQuery = this.build(); + if (Array.isArray(builtQuery)) { + for (const query of builtQuery) { + await queryExecutor.manager[functionName]!(query); + } + } else { + await queryExecutor.manager[functionName]!(builtQuery); + } + return; + } + } + } else if(typeof queryExecutor === 'object') { + for (const functionName of functionNames) { + if (typeof queryExecutor[functionName] === 'function') { + const builtQuery = this.build(); + if (Array.isArray(builtQuery)) { + for (const query of builtQuery) { + await queryExecutor[functionName]!(query); + } + } else { + await queryExecutor[functionName]!(builtQuery); + } + return; + } + } + } + + throw new Error('Invalid query executor provided.'); + } +} diff --git a/src/queryKinds/ddl/index.ts b/src/queryKinds/ddl/index.ts new file mode 100644 index 0000000..9e32728 --- /dev/null +++ b/src/queryKinds/ddl/index.ts @@ -0,0 +1,57 @@ +import sqlFlavor from '../../types/sqlFlavor.js'; +import * as tables from './table/index.js'; + +export * as table from './table/index.js'; +export * from './ddlQueryDefinition.js'; +export { default as DdlQueryDefinition } from './ddlQueryDefinition.js'; + +/** + * Class representing a Table for DDL operations. + * This class provides methods to initiate DDL queries such as CREATE TABLE. + */ +export class Table { + + constructor( + private deepAnalysis: boolean = false, + private flavor = sqlFlavor.postgres + ) {} + + /** + * Initiates a new CREATE TABLE query. + * @returns A new CreateTableQuery instance with a build method that respects the deepAnalysis setting. + */ + public get create() { + const query = new tables.CreateTableQuery(); + query.sqlFlavor(this.flavor); + query.build = (deepAnalysis: boolean = this.deepAnalysis) => { + return tables.CreateTableQuery.prototype.build.call(query, deepAnalysis); + } + return query; + } + + /** + * Initiates a new DROP TABLE query. + * @returns A new DropTableQuery instance with a build method that respects the deepAnalysis setting. + */ + public get drop() { + const query = new tables.DropTableQuery(); + query.sqlFlavor(this.flavor); + query.build = (deepAnalysis: boolean = this.deepAnalysis) => { + return tables.DropTableQuery.prototype.build.call(query, deepAnalysis); + } + return query; + } + + /** + * Initiates a new ALTER TABLE query. + * @returns A new AlterTableQuery instance with a build method that respects the deepAnalysis setting. + */ + public get alter() { + const query = new tables.AlterTableQuery(); + query.sqlFlavor(this.flavor); + query.build = (deepAnalysis: boolean = this.deepAnalysis) => { + return tables.AlterTableQuery.prototype.build.call(query, deepAnalysis); + } + return query; + } +} diff --git a/src/queryKinds/ddl/table/Alter.ts b/src/queryKinds/ddl/table/Alter.ts new file mode 100644 index 0000000..3d06c57 --- /dev/null +++ b/src/queryKinds/ddl/table/Alter.ts @@ -0,0 +1,223 @@ +import SqlEscaper from "../../../sqlEscaper.js"; +import { ColumnDefinition } from "../../../types/Column.js"; +import QueryKind from "../../../types/QueryKind.js"; +import TableQueryDefinition from "./tableColumnDefinition.js"; + +/** + * Class representing an ALTER TABLE SQL query. + * This class allows you to define and build an ALTER TABLE SQL query + * with specified table name, columns to add, alter, or drop. + */ +export default class AlterTableQuery extends TableQueryDefinition { + + /** The columns to be added to the table. */ + columnsToAdd: ColumnDefinition[] = []; + /** The columns to be altered in the table, mapped by column name. */ + columnsToAlter: Map = new Map(); + /** The names of the columns to be dropped from the table. */ + columnsToDrop: string[] = []; + + constructor(tableName: string | null = null) { + super(); + this.tableName = tableName ? SqlEscaper.escapeTableName(tableName, this.flavor) : ''; + } + + /** + * Adds columns to be added to the table. + * @param columns - A single Column instance or an array of Column instances. + * @returns The current instance for method chaining. + */ + public addColumnsToAdd(columns: ColumnDefinition | ColumnDefinition[]): this { + if (Array.isArray(columns)) { + this.columnsToAdd.push(...columns); + } else { + this.columnsToAdd.push(columns); + } + return this; + } + + /** + * Sets the columns to be added to the table, replacing any existing ones. + * @param columns - A single Column instance or an array of Column instances. + * @returns The current instance for method chaining. + */ + public setColumnsToAdd(columns: ColumnDefinition | ColumnDefinition[]): this { + this.columnsToAdd = []; + return this.addColumnsToAdd(columns); + } + + /** + * Adds columns to be altered in the table. + * @param alteration - An object or array of objects containing the column name and the Column instance with alterations. + * @returns The current instance for method chaining. + */ + public addColumnsToAlter( + alteration: { name: string, columns: ColumnDefinition } + | { name: string, columns: ColumnDefinition }[] + ): this { + if (Array.isArray(alteration)) { + alteration.forEach(({ name, columns }) => { + if (!this.columnsToAlter.has(name)) { + this.columnsToAlter.set(name, []); + } + this.columnsToAlter.get(name)?.push(columns); + }); + } else { + const { name, columns } = alteration; + if (!this.columnsToAlter.has(name)) { + this.columnsToAlter.set(name, []); + } + this.columnsToAlter.get(name)?.push(columns); + } + return this; + } + + /** + * Sets the columns to be altered in the table, replacing any existing ones. + * @param alteration - An object or array of objects containing the column name and the Column instance with alterations. + * @returns The current instance for method chaining. + */ + public setColumnsToAlter( + alteration: { name: string, columns: ColumnDefinition } + | { name: string, columns: ColumnDefinition }[] + ): this { + this.columnsToAlter.clear(); + return this.addColumnsToAlter(alteration); + } + + /** + * Adds column names to be dropped from the table. + * @param columnNames - A single column name or an array of column names. + * @returns The current instance for method chaining. + */ + public dropColumnsByName(columnNames: string | string[]): this { + if (Array.isArray(columnNames)) { + this.columnsToDrop.push(...columnNames); + } else { + this.columnsToDrop.push(columnNames); + } + return this; + } + + /** + * Sets the column names to be dropped from the table, replacing any existing ones. + * @param columnNames - A single column name or an array of column names. + * @returns The current instance for method chaining. + */ + public setColumnsToDrop(columnNames: string | string[]): this { + this.columnsToDrop = []; + return this.dropColumnsByName(columnNames); + } + + /** + * Generates the SQL fragment for dropping a column. + * @param columnName - The name of the column to drop. + * @returns The SQL fragment for dropping the specified column. + */ + public static dropColumn(columnName: string): string { + return `DROP COLUMN ${columnName}`; + } + + /** + * Generates the SQL fragments for adding and altering columns. + * @returns An array of SQL fragments for adding and altering columns. + */ + private alterColumns(): string[][] { + const toAdd = this.columnsToAdd.map(col => col.buildToAdd(this.tableName)); + const toAlter = Array.from(this.columnsToAlter.entries()).map(([colName, alterations]) => { + return alterations.map(alteration => alteration.buildToAlter(this.tableName, colName)); + }).flat(); + return [...toAdd, ...toAlter]; + } + + /** + * Generates the SQL fragments for dropping columns. + * @returns An array of SQL fragments for dropping columns. + */ + private dropColumns(): string[] { + return this.columnsToDrop.map(colName => AlterTableQuery.dropColumn(colName)); + } + + /** + * Gets the kind of query. + * @returns The kind of query, which is 'ALTER_TABLE' for this class. + */ + public get kind() { + return QueryKind.ALTER_TABLE + } + + /** + * Builds the ALTER TABLE SQL query strings. + * @param _deepAnalysis - Optional boolean to indicate if deep analysis is required (default is false). + * @returns An array of constructed ALTER TABLE SQL query strings. + * @throws Error if the table name is not provided or if no alterations are specified. + */ + public build(_deepAnalysis?: boolean): string[] { + + if (!this.tableName) { + throw new Error('Table name is required to build ALTER TABLE query.'); + } + + const queries: string[] = []; + const alterCols = this.alterColumns(); + const dropCols = this.dropColumns(); + + alterCols.forEach(colParts => { + queries.push(...colParts); + }); + + dropCols.forEach(dropPart => { + let query = `ALTER TABLE ${this.tableName} ${dropPart}`; + queries.push(query); + }); + + if (queries.length === 0) { + throw new Error('No alterations specified for ALTER TABLE query.'); + } + + this.builtQuery = []; + queries.forEach(query => { + (this.builtQuery as any as string[])?.push(SqlEscaper.appendSchemas(`${query};`, this.schemas)); + }); + return this.builtQuery; + } + + /** + * Returns the SQL string representation of the ALTER TABLE query. + * @returns The SQL string representation of the ALTER TABLE query. + * @throws Error if the query has not been built yet. + */ + public toSQL(): string | string[] { + if(this.builtQuery) this.build(); + if(!this.builtQuery) throw new Error('No built query available. Please build the query first.'); + return this.builtQuery; + } + + /** + * Creates a clone of the current AlterTableQuery instance. + * @returns A new AlterTableQuery instance with the same properties as the current instance. + */ + public clone(): AlterTableQuery { + const cloned = new AlterTableQuery(this.tableName); + cloned.flavor = this.flavor; + cloned.columnsToAdd = [...this.columnsToAdd]; + cloned.columnsToAlter = new Map(this.columnsToAlter); + cloned.columnsToDrop = [...this.columnsToDrop]; + return cloned; + } + + /** + * Resets the AlterTableQuery instance to its initial state. + * This method clears the table name, columns to add, alter, drop, and the built query. + * @returns The current instance for method chaining. + */ + public reset(): this { + this.tableName = ''; + this.builtQuery = null; + this.columnsToAdd = []; + this.columnsToAlter.clear(); + this.columnsToDrop = []; + return this; + } + +} diff --git a/src/queryKinds/ddl/table/Create.ts b/src/queryKinds/ddl/table/Create.ts new file mode 100644 index 0000000..4f79315 --- /dev/null +++ b/src/queryKinds/ddl/table/Create.ts @@ -0,0 +1,120 @@ +import SqlEscaper from "../../../sqlEscaper.js"; +import { ColumnDefinition } from "../../../types/Column.js"; +import QueryKind from "../../../types/QueryKind.js"; +import TableQueryDefinition from "./tableColumnDefinition.js"; + + +/** + * Class representing a CREATE TABLE SQL query. + * This class allows you to define and build a CREATE TABLE SQL query + * with specified table name and columns. + */ +export default class CreateTableQuery extends TableQueryDefinition { + + /** The name of the table to be created. */ + private tableColumns: ColumnDefinition[] = []; + + constructor( + tableName?: string, + ...columns: ColumnDefinition[] + ) { + super(); + this.tableName = tableName ? SqlEscaper.escapeTableName(tableName, this.flavor) : ''; + this.tableColumns = columns ?? []; + } + + /** + * Gets the columns defined for the table. + * @returns An array of Column instances representing the table's columns. + */ + public get columns(): ColumnDefinition[] { + return this.tableColumns; + } + + /** + * Sets the columns for the table. + * @param columns - A single Column instance or an array of Column instances. + * @returns The current instance for method chaining. + */ + public setColumns(columns: ColumnDefinition | ColumnDefinition[]): this { + this.tableColumns = []; + return this.addColumns(columns); + } + + /** + * Adds columns to the table. + * @param columns - A single Column instance or an array of Column instances. + * @returns The current instance for method chaining. + */ + public addColumns(columns: ColumnDefinition | ColumnDefinition[]): this { + if (Array.isArray(columns)) { + this.tableColumns.push(...columns); + } else { + this.tableColumns.push(columns); + } + return this; + } + + /** + * Builds the CREATE TABLE SQL query string. + * @param _deepAnalysis - Optional boolean to indicate if deep analysis is required (default is false). + * @returns The constructed CREATE TABLE SQL query string. + * @throws Error if the table name is not set or if no columns are defined. + */ + public build(_deepAnalysis?: boolean): string { + if (!this.tableName) { + throw new Error('Table name is not set.'); + } + + if (this.tableColumns.length === 0) { + throw new Error('No columns defined for the table.'); + } + + const columnsDef = this.tableColumns.map(col => ` ${col.build()}`).join(',\n'); + const ifNotExists = this.ifNotExistsFlag ? 'IF NOT EXISTS ' : ''; + this.builtQuery = `CREATE TABLE ${ifNotExists}${this.tableName} (\n${columnsDef}\n);`; + this.builtQuery = SqlEscaper.appendSchemas(this.builtQuery, this.schemas); + return this.builtQuery; + } + + /** + * Creates a clone of the current CreateTableQuery instance. + * @returns A new instance of CreateTableQuery with the same properties as the current instance. + */ + public clone(): CreateTableQuery { + const cloned = new CreateTableQuery(this.tableName ?? undefined, ...this.tableColumns); + cloned.flavor = this.flavor; + cloned.ifNotExistsFlag = this.ifNotExistsFlag; + return cloned; + } + + /** + * Resets the state of the CreateTableQuery instance. + * This method clears the table name, columns, and built query. + * @returns The current instance for method chaining. + */ + public reset(): this { + this.tableName = ''; + this.tableColumns = []; + this.builtQuery = null; + this.ifNotExistsFlag = false; + return this; + } + + /** + * Returns the SQL string representation of the CREATE TABLE query. + * @returns The SQL string representation of the CREATE TABLE query. + */ + public toSQL(): string { + return this.build(); + } + + /** + * Gets the kind of the query. + * @returns The kind of the query, which is QueryKind.CREATE_TABLE. + */ + public get kind() { + return QueryKind.CREATE_TABLE; + } + +} diff --git a/src/queryKinds/ddl/table/Drop.ts b/src/queryKinds/ddl/table/Drop.ts new file mode 100644 index 0000000..cb57638 --- /dev/null +++ b/src/queryKinds/ddl/table/Drop.ts @@ -0,0 +1,72 @@ +import SqlEscaper from "../../../sqlEscaper.js"; +import QueryKind from "../../../types/QueryKind.js"; +import TableQueryDefinition from "./tableColumnDefinition.js"; + + +/** + * Class representing a DROP TABLE SQL query. + * This class allows you to define and build a DROP TABLE SQL query + * with specified table name and options like IF EXISTS. + */ +export default class DropTableQuery extends TableQueryDefinition { + + constructor(tableName: string | null = null) { + super(); + this.tableName = tableName ? SqlEscaper.escapeTableName(tableName, this.flavor) : ''; + } + + /** + * Builds the DROP TABLE SQL query string. + * @param _deepAnalysis - Optional boolean to indicate if deep analysis is required (default is false). + * @returns The constructed DROP TABLE SQL query string. + * @throws Error if the table name is not provided. + */ + public build(_deepAnalysis: boolean = false): string { + if (!this.tableName) { + throw new Error('Table name is required to build DROP TABLE query.'); + } + let query = 'DROP TABLE '; + if (this.ifNotExistsFlag) { + query += 'IF EXISTS '; + } + query += `${this.tableName};`; + this.builtQuery = SqlEscaper.appendSchemas(query, this.schemas); + return this.builtQuery; + } + + /** + * Creates a clone of the current DropTableQuery instance. + * @returns A new DropTableQuery instance with the same properties as the current instance. + */ + public clone(): DropTableQuery { + const cloned = new DropTableQuery(this.tableName); + cloned.flavor = this.flavor; + cloned.ifNotExistsFlag = this.ifNotExistsFlag; + return cloned; + } + + /** + * Resets the DropTableQuery instance to its initial state. + * This method clears the table name, built query, and IF NOT EXISTS flag. + * @returns The current instance for method chaining. + */ + public reset(): this { + this.tableName = ''; + this.builtQuery = null; + this.ifNotExistsFlag = false; + return this; + } + + /** + * Returns the SQL string representation of the DROP TABLE query. + * @returns The SQL string representation of the DROP TABLE query. + */ + public toSQL(): string { + return this.build(); + } + + /** Getter for the kind of query. */ + public get kind() { + return QueryKind.DROP_TABLE; + } +} diff --git a/src/queryKinds/ddl/table/index.ts b/src/queryKinds/ddl/table/index.ts new file mode 100644 index 0000000..a683680 --- /dev/null +++ b/src/queryKinds/ddl/table/index.ts @@ -0,0 +1,8 @@ +export * from './tableColumnDefinition.js'; +export { default as TableColumnDefinition } from './tableColumnDefinition.js'; +export * from './Create.js'; +export { default as CreateTableQuery } from './Create.js'; +export * from './Drop.js'; +export { default as DropTableQuery } from './Drop.js'; +export * from './Alter.js'; +export { default as AlterTableQuery } from './Alter.js'; diff --git a/src/queryKinds/ddl/table/tableColumnDefinition.ts b/src/queryKinds/ddl/table/tableColumnDefinition.ts new file mode 100644 index 0000000..e9b6580 --- /dev/null +++ b/src/queryKinds/ddl/table/tableColumnDefinition.ts @@ -0,0 +1,24 @@ +import DdlQueryDefinition from "../ddlQueryDefinition.js"; + +export default abstract class TableQueryDefinition extends DdlQueryDefinition { + /** Flag indicating whether to include IF NOT EXISTS clause. */ + protected ifNotExistsFlag: boolean = false; + + /** + * Marks the table creation to include IF NOT EXISTS clause. + * @returns The current instance for method chaining. + */ + public ifNotExists(): this { + this.ifNotExistsFlag = true; + return this; + } + + /** + * Resets the IF NOT EXISTS clause for the table creation. + * @returns The current instance for method chaining. + */ + public resetIfNotExists(): this { + this.ifNotExistsFlag = false; + return this; + } +} diff --git a/src/queryKinds/delete.test.ts b/src/queryKinds/dml/delete.test.ts similarity index 99% rename from src/queryKinds/delete.test.ts rename to src/queryKinds/dml/delete.test.ts index 8dbd5f5..a7e7dd0 100644 --- a/src/queryKinds/delete.test.ts +++ b/src/queryKinds/dml/delete.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import DeleteQuery from "./delete.js"; -import Statement from "../statementMaker.js"; -import { Cte } from "../cteMaker.js"; +import Statement from "../../statementMaker.js"; +import { Cte } from "../../cteMaker.js"; import SelectQuery from "./select.js"; describe('Delete Query', () => { diff --git a/src/queryKinds/delete.ts b/src/queryKinds/dml/delete.ts similarity index 96% rename from src/queryKinds/delete.ts rename to src/queryKinds/dml/delete.ts index a2fd035..a36d60c 100644 --- a/src/queryKinds/delete.ts +++ b/src/queryKinds/dml/delete.ts @@ -1,9 +1,9 @@ -import CteMaker, { Cte } from "../cteMaker.js"; -import SqlEscaper from "../sqlEscaper.js"; -import Statement from "../statementMaker.js"; -import QueryKind from "../types/QueryKind.js"; -import UsingTable from "../types/UsingTable.js"; -import QueryDefinition from "./query.js"; +import CteMaker, { Cte } from "../../cteMaker.js"; +import SqlEscaper from "../../sqlEscaper.js"; +import Statement from "../../statementMaker.js"; +import QueryKind from "../../types/QueryKind.js"; +import UsingTable from "../../types/UsingTable.js"; +import DmlQueryDefinition from "./dmlQueryDefinition.js"; /** * DeleteQuery class represents a SQL DELETE query. @@ -11,7 +11,7 @@ import QueryDefinition from "./query.js"; * adding USING clauses, WHERE conditions, RETURNING fields, and Common Table Expressions (CTEs). * The class supports cloning, resetting, and building the final SQL query string with parameters. */ -export default class DeleteQuery extends QueryDefinition { +export default class DeleteQuery extends DmlQueryDefinition { /** The table from which records will be deleted. */ private deletingFrom: string; /** An optional alias for the table being deleted from. */ diff --git a/src/queryKinds/query.test.ts b/src/queryKinds/dml/dmlQueryDefinition.test.ts similarity index 99% rename from src/queryKinds/query.test.ts rename to src/queryKinds/dml/dmlQueryDefinition.test.ts index 7bad285..5e38926 100644 --- a/src/queryKinds/query.test.ts +++ b/src/queryKinds/dml/dmlQueryDefinition.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import SelectQuery from "./select.js"; -import sqlFlavor from "../types/sqlFlavor.js"; +import sqlFlavor from "../../types/sqlFlavor.js"; import UpdateQuery from "./update.js"; import InsertQuery from "./insert.js"; import DeleteQuery from "./delete.js"; diff --git a/src/queryKinds/query.ts b/src/queryKinds/dml/dmlQueryDefinition.ts similarity index 90% rename from src/queryKinds/query.ts rename to src/queryKinds/dml/dmlQueryDefinition.ts index 2f2177f..55f6fa1 100644 --- a/src/queryKinds/query.ts +++ b/src/queryKinds/dml/dmlQueryDefinition.ts @@ -1,16 +1,16 @@ import { ValidatorOptions } from "class-validator"; -import deepEqual from "../deepEqual.js"; -import { getClassValidator, getZod } from "../getOptionalPackages.js"; +import deepEqual from "../../deepEqual.js"; +import { getClassValidator, getZod } from "../../getOptionalPackages.js"; // Import types only since they are used for type checking only // and zod is optional peer dependency import type z from "zod"; import type { ZodObject } from "zod"; -import sqlFlavor from "../types/sqlFlavor.js"; -import CteMaker from "../cteMaker.js"; -import Statement from "../statementMaker.js"; -import QueryKind from "../types/QueryKind.js"; -import Join, { isJoinTable } from "../types/Join.js"; -import SqlEscaper from "../sqlEscaper.js"; +import sqlFlavor from "../../types/sqlFlavor.js"; +import CteMaker from "../../cteMaker.js"; +import Statement from "../../statementMaker.js"; +import QueryKind from "../../types/QueryKind.js"; +import Join, { isJoinTable } from "../../types/Join.js"; +import SqlEscaper from "../../sqlEscaper.js"; /** * An array of function names that can be used to execute SQL queries. @@ -55,13 +55,13 @@ type SchemaType = never; /** - * Abstract class QueryDefinition serves as a blueprint for different types of SQL query definitions. + * Abstract class DmlQueryDefinition serves as a blueprint for different types of SQL query definitions. * It defines the essential methods and properties that any concrete query class must implement. * This includes methods for building the SQL query, executing it, cloning the query definition, * resetting its state, and checking if the query is complete. * The class also provides a method to re-analyze the query for duplicate parameters to optimize parameter usage. */ -export default abstract class QueryDefinition { +export default abstract class DmlQueryDefinition { /** * Converts the query definition to its SQL string representation. @@ -87,7 +87,7 @@ export default abstract class QueryDefinition { /** * Creates a deep copy of the current query definition. */ - public abstract clone(): QueryDefinition; + public abstract clone(): DmlQueryDefinition; /** * Resets the query definition to its initial state. @@ -129,12 +129,19 @@ export default abstract class QueryDefinition { /** * Provides access to the current query definition instance. - * @returns The current QueryDefinition instance. + * @returns The current DmlQueryDefinition instance. */ - public get query(): QueryDefinition { + public get query(): DmlQueryDefinition { return this; } + /** + * Utility method to add spaces to each line of a given string. + * This is useful for formatting SQL queries for better readability. + * @param str The string to format. + * @param spaces The number of spaces to add to the beginning of each line (default is 0). + * @returns The formatted string with added spaces. + */ protected spaceLines(str: string, spaces: number = 0): string { const space = ' '.repeat(spaces); return str.split('\n').map(line => space + line).join('\n'); @@ -189,7 +196,7 @@ export default abstract class QueryDefinition { /** * Sets the SQL flavor for escaping identifiers. * @param flavor The SQL flavor to set. - * @returns The current QueryDefinition instance for chaining. + * @returns The current DmlQueryDefinition instance for chaining. */ public sqlFlavor(flavor: sqlFlavor) { this.flavor = flavor; @@ -255,7 +262,7 @@ export default abstract class QueryDefinition { */ public validate any)>( schema: T - ): QueryDefinition> { + ): DmlQueryDefinition> { if ((schema as any).safeParse) { this.isZodSchema = true; } else { @@ -269,7 +276,7 @@ export default abstract class QueryDefinition { /** * Configures options for class-validator validation. * @param config The configuration options for class-validator. - * @returns The current QueryDefinition instance for method chaining. + * @returns The current DmlQueryDefinition instance for method chaining. */ public classValidatorConfig( config: ValidatorOptions @@ -403,12 +410,12 @@ export default abstract class QueryDefinition { values: any[], useDeepEqual: boolean = false ): { text: string; values: any[] } { - return QueryDefinition.reAnalyzeParsedQueryForDuplicateParams(query, values, useDeepEqual); + return DmlQueryDefinition.reAnalyzeParsedQueryForDuplicateParams(query, values, useDeepEqual); } /** * Static method to re-analyze a parsed SQL query for duplicate parameters. - * This method can be used independently of any instance of QueryDefinition. + * This method can be used independently of any instance of DmlQueryDefinition. */ public static reAnalyzeParsedQueryForDuplicateParams( query: string, diff --git a/src/queryKinds/dml/index.ts b/src/queryKinds/dml/index.ts new file mode 100644 index 0000000..a1b331b --- /dev/null +++ b/src/queryKinds/dml/index.ts @@ -0,0 +1,12 @@ +export * from './delete.js'; +export { default as DeleteQuery } from './delete.js'; +export * from './insert.js'; +export { default as InsertQuery } from './insert.js'; +export * from './select.js'; +export { default as SelectQuery } from './select.js'; +export * from './update.js'; +export { default as UpdateQuery } from './update.js'; +export * from './union.js'; +export { default as Union } from './union.js'; +export * from './dmlQueryDefinition.js'; +export { default as DmlQueryDefinition } from './dmlQueryDefinition.js'; diff --git a/src/queryKinds/insert.test.ts b/src/queryKinds/dml/insert.test.ts similarity index 99% rename from src/queryKinds/insert.test.ts rename to src/queryKinds/dml/insert.test.ts index 886730f..bf20c24 100644 --- a/src/queryKinds/insert.test.ts +++ b/src/queryKinds/dml/insert.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import InsertQuery from "./insert.js"; import SelectQuery from "./select.js"; -import { Cte } from "../cteMaker.js"; +import { Cte } from "../../cteMaker.js"; describe('Insert Query', () => { diff --git a/src/queryKinds/insert.ts b/src/queryKinds/dml/insert.ts similarity index 96% rename from src/queryKinds/insert.ts rename to src/queryKinds/dml/insert.ts index 90fc76f..fdcab75 100644 --- a/src/queryKinds/insert.ts +++ b/src/queryKinds/dml/insert.ts @@ -1,8 +1,8 @@ -import CteMaker, { Cte } from "../cteMaker.js"; -import SqlEscaper from "../sqlEscaper.js"; -import ColumnValue from "../types/ColumnValue.js"; -import QueryKind from "../types/QueryKind.js"; -import QueryDefinition from "./query.js"; +import CteMaker, { Cte } from "../../cteMaker.js"; +import SqlEscaper from "../../sqlEscaper.js"; +import ColumnValue from "../../types/ColumnValue.js"; +import QueryKind from "../../types/QueryKind.js"; +import DmlQueryDefinition from "./dmlQueryDefinition.js"; import SelectQuery from "./select.js"; /** @@ -10,7 +10,7 @@ import SelectQuery from "./select.js"; * It supports inserting values directly or from a SELECT query. * It also supports Common Table Expressions (CTEs) and RETURNING clauses. */ -export default class InsertQuery extends QueryDefinition { +export default class InsertQuery extends DmlQueryDefinition { /** The table into which records will be inserted. */ private table: string; /** The column-value pairs to be inserted. */ diff --git a/src/queryKinds/select.test.ts b/src/queryKinds/dml/select.test.ts similarity index 99% rename from src/queryKinds/select.test.ts rename to src/queryKinds/dml/select.test.ts index 4064a62..26453a8 100644 --- a/src/queryKinds/select.test.ts +++ b/src/queryKinds/dml/select.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import SelectQuery from "./select.js"; -import Statement from "../statementMaker.js"; -import CteMaker, { Cte } from "../cteMaker.js"; +import Statement from "../../statementMaker.js"; +import CteMaker, { Cte } from "../../cteMaker.js"; describe('Select Query', () => { it('should generate correct SELECT SQL', () => { diff --git a/src/queryKinds/select.ts b/src/queryKinds/dml/select.ts similarity index 97% rename from src/queryKinds/select.ts rename to src/queryKinds/dml/select.ts index c6ff83f..7a468cd 100644 --- a/src/queryKinds/select.ts +++ b/src/queryKinds/dml/select.ts @@ -1,10 +1,10 @@ -import CteMaker, { Cte } from "../cteMaker.js"; -import SqlEscaper from "../sqlEscaper.js"; -import Statement from "../statementMaker.js"; -import Join, { isJoinTable } from "../types/Join.js"; -import QueryKind from "../types/QueryKind.js"; -import OrderBy, { isOrderByField } from "../types/OrderBy.js"; -import QueryDefinition from "./query.js"; +import CteMaker, { Cte } from "../../cteMaker.js"; +import SqlEscaper from "../../sqlEscaper.js"; +import Statement from "../../statementMaker.js"; +import Join, { isJoinTable } from "../../types/Join.js"; +import QueryKind from "../../types/QueryKind.js"; +import OrderBy, { isOrderByField } from "../../types/OrderBy.js"; +import DmlQueryDefinition from "./dmlQueryDefinition.js"; import Union from "./union.js"; /** @@ -12,7 +12,7 @@ import Union from "./union.js"; * It includes methods to build various parts of the query such as SELECT fields, WHERE conditions, JOINs, ORDER BY, LIMIT, OFFSET, GROUP BY, and CTEs. * The class provides functionality to build the final SQL query string and manage query parameters. */ -export default class SelectQuery extends QueryDefinition { +export default class SelectQuery extends DmlQueryDefinition { /** * The table to select from. */ diff --git a/src/queryKinds/union.test.ts b/src/queryKinds/dml/union.test.ts similarity index 94% rename from src/queryKinds/union.test.ts rename to src/queryKinds/dml/union.test.ts index 34f6db4..2eae0d3 100644 --- a/src/queryKinds/union.test.ts +++ b/src/queryKinds/dml/union.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import Union from "./union.js"; -import Query from "../queryMaker.js"; -import Statement from "../statementMaker.js"; +import Query from "../../queryMaker.js"; +import Statement from "../../statementMaker.js"; describe("Union Query", () => { it('should create a UNION query with two SELECT statements', () => { @@ -462,4 +462,29 @@ describe("Union Query", () => { expect(unionQuery.values).toEqual(['value1', 'value2']); }); + it('should throw if selects have different selected lengths', () => { + const select1 = Query.select + .from("table1") + .select(["column1", "column2"]) + .where("column1 = ?", "value1"); + + const select2 = Query.select + .from("table2") + .select(["column1", "column2", "column3"]) + .where("column2 = ?", "value2"); + + const union = new Union() + .addManyOfType([select1, select2], 'union all') + .as('union_table'); + + expect(() => union.build()).toThrowError('All SELECT queries must have the same number of fields. Query at index 1 differs.'); + expect(() => union.rawUnion()).toThrowError('All SELECT queries must have the same number of fields. Query at index 1 differs.'); + + try { + union.build(); + } catch (e: any) { + expect(e.cause?.selectQuery).toStrictEqual(select2); + } + }); + }); diff --git a/src/queryKinds/union.ts b/src/queryKinds/dml/union.ts similarity index 90% rename from src/queryKinds/union.ts rename to src/queryKinds/dml/union.ts index a10cb3f..8c224c4 100644 --- a/src/queryKinds/union.ts +++ b/src/queryKinds/dml/union.ts @@ -1,9 +1,9 @@ -import Statement from "../statementMaker.js"; -import QueryKind from "../types/QueryKind.js"; -import OrderBy from "../types/OrderBy.js"; -import QueryDefinition from "./query.js"; +import Statement from "../../statementMaker.js"; +import QueryKind from "../../types/QueryKind.js"; +import OrderBy from "../../types/OrderBy.js"; +import DmlQueryDefinition from "./dmlQueryDefinition.js"; import SelectQuery from "./select.js"; -import SqlEscaper from "../sqlEscaper.js"; +import SqlEscaper from "../../sqlEscaper.js"; /** Allowed types for UnionType */ export const UnionTypes = { @@ -42,8 +42,8 @@ export type SelectQueryWithUnionType = { * It supports adding queries with different union types (UNION or UNION ALL) * and can optionally assign an alias to the resulting union query. */ -export default class Union extends QueryDefinition { - +export default class Union extends DmlQueryDefinition { + /** The selected fields for the union query */ private selectFields: string[] = []; /** Needed alias for the union query */ @@ -67,15 +67,50 @@ export default class Union extends QueryDefinition { /** Having statement for the union query */ private havingStatement: Statement | null = null; + /** + * Checks if all added SELECT queries have the same number of fields. + * This is important for ensuring that the UNION operation is valid. + * @returns True if all SELECT queries have the same number of fields, + * or the index of the first query that differs in field count. + */ + private allSelectsHaveSameNumberOfFields(): number | null { + if (this.selectQueries.length === 0) return null; + + const selects = this.selectQueries.map(sq => sq.query); + + const firstSelectFieldCount = selects[0]?.columns.length || 0; + + let indexThatDiffers: number | null = null; + const result = selects.every((select, i) => { + if (select.columns.length !== firstSelectFieldCount) { + indexThatDiffers = i; + return false; + } else return true; + }); + + return result ? null : indexThatDiffers; + } + /** * Make the union without selecting from it * Useful when the raw union is needed as a subquery + * @throws Error if no SELECT queries have been added to the union + * @throws Error if the SELECT queries do not have the same number of fields * @returns An object containing the raw SQL text of the union and its parameter values. */ public rawUnion(): { text: string; values: any[] } { if (this.selectQueries.length === 0) { throw new Error('No SELECT queries added to the UNION.'); } + + const differingIndex = this.allSelectsHaveSameNumberOfFields(); + if (differingIndex !== null) { + console.log('This is erroring out') + throw new Error( + `All SELECT queries must have the same number of fields. Query at index ${differingIndex} differs.`, + { cause: { selectQuery: this.selectQueries[differingIndex]?.query } } + ); + } let unionItself: string = ''; const values: any[] = []; @@ -379,6 +414,15 @@ export default class Union extends QueryDefinition { throw new Error('No SELECT queries added to the UNION.'); } + const differingIndex = this.allSelectsHaveSameNumberOfFields(); + if (differingIndex !== null) { + console.log('This is erroring out') + throw new Error( + `All SELECT queries must have the same number of fields. Query at index ${differingIndex} differs.`, + { cause: { selectQuery: this.selectQueries[differingIndex]?.query } } + ); + } + let unionItself: string = ''; const values: any[] = []; diff --git a/src/queryKinds/update.test.ts b/src/queryKinds/dml/update.test.ts similarity index 99% rename from src/queryKinds/update.test.ts rename to src/queryKinds/dml/update.test.ts index b78cb7d..acd3c2b 100644 --- a/src/queryKinds/update.test.ts +++ b/src/queryKinds/dml/update.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import UpdateQuery from "./update.js"; -import Statement from "../statementMaker.js"; -import CteMaker, { Cte } from "../cteMaker.js"; +import Statement from "../../statementMaker.js"; +import CteMaker, { Cte } from "../../cteMaker.js"; import SelectQuery from "./select.js"; describe('Update Query', () => { diff --git a/src/queryKinds/update.ts b/src/queryKinds/dml/update.ts similarity index 96% rename from src/queryKinds/update.ts rename to src/queryKinds/dml/update.ts index 81e72e3..b140fc3 100644 --- a/src/queryKinds/update.ts +++ b/src/queryKinds/dml/update.ts @@ -1,18 +1,18 @@ -import CteMaker, { Cte } from "../cteMaker.js"; -import SqlEscaper from "../sqlEscaper.js"; -import Statement from "../statementMaker.js"; -import Join, { isJoinTable } from "../types/Join.js"; -import QueryKind from "../types/QueryKind.js"; -import SetValue from "../types/SetValue.js"; -import QueryDefinition from "./query.js"; +import CteMaker, { Cte } from "../../cteMaker.js"; +import SqlEscaper from "../../sqlEscaper.js"; +import Statement from "../../statementMaker.js"; +import Join, { isJoinTable } from "../../types/Join.js"; +import QueryKind from "../../types/QueryKind.js"; +import SetValue from "../../types/SetValue.js"; +import DmlQueryDefinition from "./dmlQueryDefinition.js"; /** * UpdateQuery class is used to build SQL UPDATE queries. * It provides methods to specify the table to update, set values, add joins, and define conditions. * The class supports Common Table Expressions (CTEs) and returning clauses. - * It extends the QueryDefinition class to inherit common query functionalities. + * It extends the DmlQueryDefinition class to inherit common query functionalities. */ -export default class UpdateQuery extends QueryDefinition { +export default class UpdateQuery extends DmlQueryDefinition { /** The table to update. */ private table: string; /** Optional alias for the table. */ diff --git a/src/queryMaker.ts b/src/queryMaker.ts index fc9a363..49f120e 100644 --- a/src/queryMaker.ts +++ b/src/queryMaker.ts @@ -1,9 +1,10 @@ import { Cte } from "./cteMaker.js"; -import DeleteQuery from "./queryKinds/delete.js"; -import InsertQuery from "./queryKinds/insert.js"; -import SelectQuery from "./queryKinds/select.js"; -import Union from "./queryKinds/union.js"; -import UpdateQuery from "./queryKinds/update.js"; +import { Table } from "./queryKinds/ddl/index.js"; +import DeleteQuery from "./queryKinds/dml/delete.js"; +import InsertQuery from "./queryKinds/dml/insert.js"; +import SelectQuery from "./queryKinds/dml/select.js"; +import Union from "./queryKinds/dml/union.js"; +import UpdateQuery from "./queryKinds/dml/update.js"; import Statement from "./statementMaker.js"; import sqlFlavor from "./types/sqlFlavor.js"; @@ -86,6 +87,10 @@ class Query { return new Cte(); } + /** + * Initiates a new UNION query. + * @returns A new Union instance with a build method that respects the deepAnalysisDefault setting. + */ public get union(): Union { const unionQuery = new Union(); (unionQuery as any).flavor = this.flavor; @@ -95,6 +100,16 @@ class Query { return unionQuery; } + /** + * Initiates a new Statement instance for building complex SQL statements. + * This can be used to create WHERE clauses, JOIN conditions, etc. + * @returns A new Statement instance. + */ + public get table() { + const table = new Table(this.deepAnalysisDefault, this.flavor); + return table; + } + /** * Initiates a new SELECT query. * @returns A new SelectQuery instance. @@ -145,10 +160,23 @@ class Query { return new Cte(); } + /** + * Initiates a new UNION query. + * @returns A new Union instance. + */ public static get union(): Union { return new Union(); } + /** + * Initiates a new Table instance for DDL operations. + * This can be used to create tables and other DDL statements. + * @returns A new Table instance. + */ + public static get table() { + return new Table(); + } + } export default Query; diff --git a/src/types/Column.ts b/src/types/Column.ts new file mode 100644 index 0000000..069a5bf --- /dev/null +++ b/src/types/Column.ts @@ -0,0 +1,311 @@ +import { ColumnTypes } from "./ColumnTypes.js"; + +/** + * Class representing a column type. + * It allows setting the type and adding properties, and can build a string representation of the column type. + */ +export class ColumnType { + /** The name of the column type. */ + private typeName: ColumnTypes | null; + /** The properties associated with the column type. */ + private properties: string[] = []; + + constructor( + /** The name of the column type. */ + typeName: ColumnTypes | null = null, + /** The properties associated with the column type. */ + properties: (string | { toString(): string })[] = [] + ) { + this.typeName = typeName; + this.properties = properties.map(prop => prop.toString()); + } + + /** + * Sets the type of the column. + * @param typeName - The name of the column type. + * @returns The current instance for method chaining. + */ + public setType(typeName: ColumnTypes): this { + this.typeName = typeName; + return this; + } + + /** + * Adds a property to the column type. + * @param property - The property to add. + * @returns The current instance for method chaining. + */ + public addProperty(property: string): this { + this.properties.push(property); + return this; + } + + /** + * Builds the string representation of the column type. + * @returns The string representation of the column type. + * @throws Error if the type name is not set. + */ + public build(): string { + if (!this.typeName) { + throw new Error("Type name is not set."); + } + + if (this.properties.length > 0) { + return `${this.typeName}(${this.properties.join(', ')})`; + } + return this.typeName; + } + + /** + * Returns the string representation of the column type. + * @returns The string representation of the column type. + * @throws Error if the type name is not set. + */ + public toString(): string { + return this.build(); + } + +} + +/** + * Class representing a database column with various attributes and constraints. + * It allows setting the column name, type, nullability, primary key status, uniqueness, + * default value, and check conditions. The class can build a string representation of the column definition. + */ +export class ColumnDefinition { + /** The name of the column. */ + private name: string | null = null; + /** The type of the column. */ + private type: ColumnType | null = null; + /** Indicates if the column is nullable. */ + private isNullable: boolean = true; + /** Indicates if the column is a primary key. */ + private isPrimaryKey: boolean = false; + /** Indicates if the column has a unique constraint. */ + private isUnique: boolean = false; + /** The default value for the column, if any. */ + private defaultValue?: string; + /** The check condition for the column, if any. */ + private checkCondition?: string; + + constructor( + name: string | null = null, + type: ColumnType | string | null = null + ) { + this.name = name ?? null; + if (type) { + if (typeof type === 'string') { + this.type = new ColumnType(type as ColumnTypes); + } else { + this.type = type; + } + } else { + this.type = null; + } + } + + /** + * Sets the name of the column. + * @param name - The name of the column. + * @returns The current instance for method chaining. + */ + public setName(name: string): this { + this.name = name; + return this; + } + + /** + * Sets the type of the column. + * @param type - The type of the column, either as a ColumnType instance or a string. + * @returns The current instance for method chaining. + */ + public setType(type: ColumnType | string): this { + if (typeof type === 'string') { + this.type = new ColumnType(type as ColumnTypes); + } else { + this.type = type; + } + return this; + } + + /** + * Marks the column as nullable. + * @returns The current instance for method chaining. + */ + public null(): this { + this.isNullable = true; + return this; + } + + /** + * Marks the column as not nullable. + * @returns The current instance for method chaining. + */ + public notNull(): this { + this.isNullable = false; + return this; + } + + /** + * Marks the column as a primary key. + * This also sets the column as not nullable. + * @returns The current instance for method chaining. + */ + public primaryKey(): this { + this.isPrimaryKey = true; + this.isNullable = false; // Primary key columns cannot be null + return this; + } + + /** + * Marks the column as unique. + * @returns The current instance for method chaining. + */ + public unique(): this { + this.isUnique = true; + return this; + } + + /** + * Sets the default value for the column. + * @param value - The default value for the column. + * @returns The current instance for method chaining. + */ + public default(value: string | { toString(): string }): this { + this.defaultValue = value.toString(); + return this; + } + + /** + * Sets a check condition for the column. + * @param condition - The check condition for the column. + * @returns The current instance for method chaining. + */ + public check(condition: string): this { + this.checkCondition = condition; + return this; + } + + /** + * Builds the string representation of the column definition. + * @returns The string representation of the column definition. + * @throws Error if the column name or type is not set. + */ + public build(forAdding: boolean = false): string { + if (!this.name || !this.type) { + let errorMsg = !this.name && !this.type + ? "Column name and type" + : !this.name + ? "Column name" + : "Column type"; + throw new Error(`${errorMsg} ${!this.name && !this.type ? "are" : "is"} not set.`); + } + + let parts: string[] = []; + parts.push(this.name); + parts.push(this.type.toString()); + + if (this.isPrimaryKey && !forAdding) { + parts.push("PRIMARY KEY"); + } else { + if (this.isUnique && !forAdding) { + parts.push("UNIQUE"); + } + if (!this.isNullable && !forAdding) { + parts.push("NOT NULL"); + } + } + + if (this.defaultValue !== undefined) { + parts.push(`DEFAULT ${this.defaultValue}`); + } + + if (this.checkCondition !== undefined && !forAdding) { + parts.push(`CHECK (${this.checkCondition})`); + } + + return parts.join(' '); + } + + public buildToAdd(tableName: string): string[] { + let addColumn = `ALTER TABLE ${tableName} ADD COLUMN ${this.build(true)}`; + let addColumnNullability = this.isNullable + ? `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} DROP NOT NULL` + : `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} SET NOT NULL`; + let addColumnDefault = this.defaultValue !== undefined + ? `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} SET DEFAULT ${this.defaultValue}` + : ''; + let addColumnCheck = this.checkCondition !== undefined + ? `ALTER TABLE ${tableName} ADD CONSTRAINT ${this.name}_check CHECK (${this.checkCondition})` + : ''; + let addColumnPrimaryKey = this.isPrimaryKey + ? `ALTER TABLE ${tableName} ADD CONSTRAINT ${this.name}_pkey PRIMARY KEY (${this.name})` + : ''; + let addColumnUnique = this.isUnique + ? `ALTER TABLE ${tableName} ADD CONSTRAINT ${this.name}_unique UNIQUE (${this.name})` + : ''; + + let additions = [ + addColumn, + addColumnNullability, + addColumnDefault, + addColumnCheck, + addColumnPrimaryKey, + addColumnUnique + ].filter(part => part !== ''); + + return additions; + } + + public buildToAlter( + tableName: string, + previousName?: string + ): string[] { + let alterColumnName = previousName !== this.name ? `ALTER TABLE ${tableName} RENAME COLUMN ${previousName} TO ${this.name}` : ''; + let alterColumnType = `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} TYPE ${this.type?.toString()}`; + let alterColumnNullability = this.isNullable + ? `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} DROP NOT NULL` + : `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} SET NOT NULL`; + let alterColumnDefault = this.defaultValue !== undefined + ? `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} SET DEFAULT ${this.defaultValue}` + : `ALTER TABLE ${tableName} ALTER COLUMN ${this.name} DROP DEFAULT`; + let alterColumnCheck = this.checkCondition !== undefined + ? `ALTER TABLE ${tableName} ADD CONSTRAINT ${this.name}_check CHECK (${this.checkCondition})` + : ''; + let alterColumnPrimaryKey = this.isPrimaryKey + ? `ALTER TABLE ${tableName} ADD CONSTRAINT ${this.name}_pkey PRIMARY KEY (${this.name})` + : ''; + let alterColumnUnique = this.isUnique + ? `ALTER TABLE ${tableName} ADD CONSTRAINT ${this.name}_unique UNIQUE (${this.name})` + : ''; + + let alterations = [ + alterColumnName, + alterColumnType, + alterColumnNullability, + alterColumnDefault, + alterColumnCheck, + alterColumnPrimaryKey, + alterColumnUnique + ].filter(part => part !== ''); + + return alterations; + } + + /** + * Returns the string representation of the column definition. + * @returns The string representation of the column definition. + * @throws Error if the column name or type is not set. + */ + public toString(): string { + return this.build(); + } + +} + +export default function Column( + name: string | null = null, + type: ColumnType | string | null = null +): ColumnDefinition { + return new ColumnDefinition(name, type); +} diff --git a/src/types/ColumnTypes.ts b/src/types/ColumnTypes.ts new file mode 100644 index 0000000..56c6188 --- /dev/null +++ b/src/types/ColumnTypes.ts @@ -0,0 +1,181 @@ +import { ColumnType } from "./Column.js"; + +/** + * Contain all the possible column types in PostgreSQL + * Reference: https://www.postgresql.org/docs/current/datatype.html + */ +export enum ColumnTypesEnum { + // Numeric Types + SMALLINT = "smallint", + INTEGER = "integer", + BIGINT = "bigint", + DECIMAL = "decimal", + NUMERIC = "numeric", + REAL = "real", + DOUBLE_PRECISION = "double precision", + SERIAL = "serial", + BIGSERIAL = "bigserial", + SMALLSERIAL = "smallserial", + MONEY = "money", + + // Character Types + VARCHAR = "character varying", + CHAR = "character", + TEXT = "text", + + // Binary + BYTEA = "bytea", + + // Date/Time + TIMESTAMP = "timestamp", + TIMESTAMPTZ = "timestamptz", + DATE = "date", + TIME = "time", + TIMETZ = "timetz", + INTERVAL = "interval", + + // Boolean + BOOLEAN = "boolean", + + // Enumerated + ENUM = "enum", + + // Geometric + POINT = "point", + LINE = "line", + LSEG = "lseg", + BOX = "box", + PATH = "path", + POLYGON = "polygon", + CIRCLE = "circle", + + // Network + CIDR = "cidr", + INET = "inet", + MACADDR = "macaddr", + MACADDR8 = "macaddr8", + + // Bit Strings + BIT = "bit", + VARBIT = "bit varying", + + // Text Search + TSVECTOR = "tsvector", + TSQUERY = "tsquery", + + // UUID + UUID = "uuid", + + // JSON + JSON = "json", + JSONB = "jsonb", + + // XML + XML = "xml", + + // Arrays + ARRAY = "array", + + // Composite + COMPOSITE = "composite", + + // Range Types + INT4RANGE = "int4range", + INT8RANGE = "int8range", + NUMRANGE = "numrange", + TSRANGE = "tsrange", + TSTZRANGE = "tstzrange", + DATERANGE = "daterange", + + // Special + OID = "oid", + PG_LSN = "pg_lsn", + TXID_SNAPSHOT = "txid_snapshot", + REGPROC = "regproc", + REGPROCEDURE = "regprocedure", + REGOPER = "regoper", + REGOPERATOR = "regoperator", + REGCLASS = "regclass", + REGTYPE = "regtype" +} + +/** + * Union type for ColumnTypesEnum keys + */ +export type ColumnTypesUnion = keyof typeof ColumnTypesEnum; + +/** + * Union type for ColumnTypesEnum values and keys + */ +export type ColumnTypes = ColumnTypesEnum | ColumnTypesUnion; + +/** + * Create a ColumnType of type VARCHAR with specified length + * @param length - The maximum length of the VARCHAR column + * @returns A ColumnType instance representing a VARCHAR column with the specified length + */ +function Varchar(length: number) { + return new ColumnType('VARCHAR', [length]); +} + +/** + * Create a ColumnType of type NUMERIC with specified precision and optional scale + * @param precision - The total number of digits + * @param scale - The number of digits to the right of the decimal point (optional) + * @returns A ColumnType instance representing a NUMERIC column with the specified precision and scale + */ +function Numeric(precision: number, scale?: number) { + if (scale !== undefined) { + return new ColumnType('NUMERIC', [precision, scale]); + } + return new ColumnType('NUMERIC', [precision]); +} + +/** + * Create a ColumnType of type DECIMAL with specified precision and optional scale + * @param precision - The total number of digits + * @param scale - The number of digits to the right of the decimal point (optional) + * @returns A ColumnType instance representing a DECIMAL column with the specified precision and scale + */ +function Decimal(precision: number, scale?: number) { + if (scale !== undefined) { + return new ColumnType('DECIMAL', [precision, scale]); + } + return new ColumnType('DECIMAL', [precision]); +} + +/** + * Create a ColumnType of type CHAR with specified length + * @param length - The fixed length of the CHAR column + * @returns A ColumnType instance representing a CHAR column with the specified length + */ +function Char(length: number) { + return new ColumnType('CHAR', [length]); +} + +/** + * Create a ColumnType of type VARBIT with specified length + * @param length - The maximum length of the VARBIT column + * @returns A ColumnType instance representing a VARBIT column with the specified length + */ +function VarBit(length: number) { + return new ColumnType('VARBIT', [length]); +} + +/** + * Create a ColumnType of type BIT with specified length + * @param length - The fixed length of the BIT column + * @returns A ColumnType instance representing a BIT column with the specified length + */ +function Bit(length: number) { + return new ColumnType('BIT', [length]); +} + +export { + Varchar, + Numeric, + Decimal, + Char, + VarBit, + Bit +} diff --git a/src/types/Join.ts b/src/types/Join.ts index d9f4f4e..4bec8a5 100644 --- a/src/types/Join.ts +++ b/src/types/Join.ts @@ -1,4 +1,4 @@ -import SelectQuery from "../queryKinds/select.js"; +import SelectQuery from "../queryKinds/dml/select.js"; import Statement from "../statementMaker.js"; type joinTypeBase = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'; diff --git a/src/types/QueryKind.ts b/src/types/QueryKind.ts index 8e9b584..d890bee 100644 --- a/src/types/QueryKind.ts +++ b/src/types/QueryKind.ts @@ -5,7 +5,10 @@ enum QueryKind { INSERT = "INSERT", UPDATE = "UPDATE", DELETE = "DELETE", - UNION = "UNION" + UNION = "UNION", + CREATE_TABLE = "CREATE_TABLE", + DROP_TABLE = "DROP_TABLE", + ALTER_TABLE = "ALTER_TABLE", } export default QueryKind; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..e6c007e --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,16 @@ +export type { default as ColumnValue } from './ColumnValue.js'; +export * from './Join.js'; +export type { default as Join } from './Join.js'; +export * from './OrderBy.js'; +export type { default as OrderBy } from './OrderBy.js'; +export * from './QueryKind.js'; +export type { default as QueryKind } from './QueryKind.js'; +export * from './SetValue.js'; +export type { default as SetValue } from './SetValue.js'; +export * from './sqlFlavor.js'; +export { default as sqlFlavor } from './sqlFlavor.js'; +export * from './UsingTable.js'; +export type { default as UsingTable } from './UsingTable.js'; +export * from './Column.js'; +export { default as Column } from './Column.js'; +export * from './ColumnTypes.js'; diff --git a/tsup.config.ts b/tsup.config.ts index 2f4cc67..7882013 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,12 @@ import { defineConfig } from 'tsup' export default defineConfig({ - entry: ['src/index.ts'], + entry: [ + 'src/index.ts', + 'src/types/index.ts', + 'src/queryKinds/dml/index.ts', + 'src/queryKinds/ddl/index.ts' + ], target: ['esnext', 'node21'], format: ['cjs', 'esm'], ignoreWatch: ['**/*.test.ts', '**/*.spec.ts'], diff --git a/vitest.config.ts b/vitest.config.ts index 1fd82d2..3dd1f83 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ '**/*.spec.ts', 'vitest.config.ts', '**/getOptionalPackages.ts', - 'src/index.ts', + '**/index.ts', ], include: ['src/**/*.ts'] },