diff --git a/.github/workflows/add_to_pr_review.yml b/.github/workflows/add_to_pr_review.yml deleted file mode 100644 index 384f2be..0000000 --- a/.github/workflows/add_to_pr_review.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Add Pull Requests to PR review project - -on: - pull_request: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/mindsdb/projects/65 - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/add_to_roadmap_project_v2.yml b/.github/workflows/add_to_roadmap_project_v2.yml deleted file mode 100644 index 240c700..0000000 --- a/.github/workflows/add_to_roadmap_project_v2.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Add issue to roadmap project -on: - issues: - types: - - opened -jobs: - add-to-project: - name: Add issue to roadmap project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.4.0 - with: - project-url: https://github.com/orgs/mindsdb/projects/53 - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} \ No newline at end of file diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..8497757 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,22 @@ +name: "MindsDB CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.CLA_TOKEN }} + PERSONAL_ACCESS_TOKEN : ${{ secrets.CLA_TOKEN }} + with: + path-to-signatures: 'assets/contributions-agreement/cla.json' + # Add path to the CLA here + path-to-document: 'https://github.com/mindsdb/mindsdb/blob/main/assets/contributions-agreement/individual-contributor.md' + branch: 'cla' + allowlist: bot*, ZoranPandovski, torrmal, Stpmax, mindsdbadmin, ea-rus, tmichaeldb, dusvyat, hamishfagg, MinuraPunchihewa, martyna-mindsdb, lucas-koontz diff --git a/assets/contributions-agreement/cla.json b/assets/contributions-agreement/cla.json new file mode 100644 index 0000000..e69de29 diff --git a/src/index.ts b/src/index.ts index ea1d722..fd379f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import DatabasesModule from './databases/databasesModule'; import ProjectsModule from './projects/projectsModule'; import SQLModule from './sql/sqlModule'; import ViewsModule from './views/viewsModule'; +import JobsModule from './jobs/jobsModule'; import Constants from './constants'; import { createDefaultAxiosInstance, @@ -52,6 +53,7 @@ const Projects = new ProjectsModule.ProjectsRestApiClient( ); const Tables = new TablesModule.TablesRestApiClient(SQL); const Views = new ViewsModule.ViewsRestApiClient(SQL); +const Jobs = new JobsModule.JobsRestApiClient(SQL); const MLEngines = new MLEnginesModule.MLEnginesRestApiClient( SQL, defaultAxiosInstance, @@ -117,6 +119,7 @@ export default { Projects, Tables, Views, + Jobs, MLEngines, Callbacks, }; diff --git a/src/jobs/job.ts b/src/jobs/job.ts new file mode 100644 index 0000000..6c54f35 --- /dev/null +++ b/src/jobs/job.ts @@ -0,0 +1,139 @@ +import JobsApiClient from './jobsApiClient'; +import { MindsDbError } from '../errors'; + +/** + * Represents a MindsDB job and provides methods to build and create it. + */ +export default class Job { + /** API client to use for executing job operations. */ + private jobsApiClient: JobsApiClient; + + /** Name of the job. */ + name: string; + + /** Project the job belongs to. */ + project: string; + + /** Accumulated queries to be executed by the job. */ + private queries: string[]; + + /** Optional start date for the job. */ + private start?: string; + + /** Optional end date for the job. */ + private end?: string; + + /** Optional repetition frequency. */ + private every?: string; + + /** Optional condition for job execution. */ + private ifCondition?: string; + + /** + * @param {JobsApiClient} jobsApiClient - API client to use for executing job operations. + * @param {string} name - Name of the job. + * @param {string} project - Project the job belongs to. + */ + constructor(jobsApiClient: JobsApiClient, name: string, project: string) { + this.jobsApiClient = jobsApiClient; + this.name = name; + this.project = project; + this.queries = []; + } + + /** + * Adds a query to the job. + * @param {string} query - The query to add. + * @returns {Job} - The current job instance (for chaining). + * @throws {MindsDbError} - If the query is invalid. + */ + addQuery(query: string): Job { + if (typeof query === 'string') { + this.queries.push(query); + } else { + throw new MindsDbError('Invalid query type. Must be a string.'); + } + return this; + } + + /** + * Sets the start date for the job. + * @param {string} start - The start date as a string. + * @returns {Job} - The current job instance (for chaining). + */ + setStart(start: string): Job { + this.start = start; + return this; + } + + /** + * Sets the end date for the job. + * @param {string} end - The end date as a string. + * @returns {Job} - The current job instance (for chaining). + */ + setEnd(end: string): Job { + this.end = end; + return this; + } + + /** + * Sets the repetition frequency for the job. + * @param {string} every - The repetition frequency. + * @returns {Job} - The current job instance (for chaining). + */ + setEvery(every: string): Job { + this.every = every; + return this; + } + + /** + * Sets the condition for job execution. + * @param {string} ifCondition - The condition as a string. + * @returns {Job} - The current job instance (for chaining). + */ + setIfCondition(ifCondition: string): Job { + this.ifCondition = ifCondition; + return this; + } + + /** + * Creates the job in MindsDB. + * @returns {Promise} - Resolves when the job is created. + * @throws {MindsDbError} - If job creation fails. + */ + async createJob(): Promise { + if (this.queries.length === 0) { + throw new MindsDbError('No queries added to the job.'); + } + const queryStr = this.queries.join(';\n'); + await this.jobsApiClient.createJob( + this.name, + this.project, + queryStr, + this.start, + this.end, + this.every, + this.ifCondition + ); + } + + /** + * Deletes the job from MindsDB. + * @returns {Promise} - Resolves when the job is deleted. + * @throws {MindsDbError} - If job deletion fails. + */ + async deleteJob(): Promise { + await this.jobsApiClient.deleteJob(this.name, this.project); + } + + /** + * Drops (deletes) a job from MindsDB by name. + * @param {string} name - Name of the job to drop. + * @param {string} project - Project the job belongs to. + * @returns {Promise} - Resolves when the job is dropped. + * @throws {MindsDbError} - Something went wrong while dropping the job. + */ + async dropJob(name: string, project: string = "mindsdb"): Promise { + await this.jobsApiClient.dropJob(name, project); + } +} diff --git a/src/jobs/jobsApiClient.ts b/src/jobs/jobsApiClient.ts new file mode 100644 index 0000000..25185fc --- /dev/null +++ b/src/jobs/jobsApiClient.ts @@ -0,0 +1,66 @@ +import Job from './job'; + +/** + * Abstract class outlining Job API operations supported by the SDK. + */ +export default abstract class JobsApiClient { + /** + * Creates a new Job instance for building and creating a job. + * @param {string} name - Name of the job. + * @param {string} project - Project the job belongs to. + * @returns {Job} - A new Job instance. + */ + abstract create(name: string, project: string): Job; + + /** + * Internal method to create the job in MindsDB. + * @param {string} name - Name of the job to create. + * @param {string} project - Project the job will be created in. + * @param {string} query - Queries to be executed by the job. + * @param {string} [start] - Optional start date for the job. + * @param {string} [end] - Optional end date for the job. + * @param {string} [every] - Optional repetition frequency. + * @param {string} [ifCondition] - Optional condition for job execution. + * @returns {Promise} - Resolves when the job is created. + * @throws {MindsDbError} - Something went wrong while creating the job. + */ + abstract createJob( + name: string, + project: string, + query: string, + start?: string, + end?: string, + every?: string, + ifCondition?: string + ): Promise; + + /** + * Internal method to delete the job in MindsDB. + * @param {string} name - Name of the job to delete. + * @param {string} project - Project the job will be deleted. + * @returns {Promise} - Resolves when the job is deleted. + * @throws {MindsDbError} - Something went wrong while deleting the job. + */ + abstract deleteJob( + name: string, + project: string + ): Promise + + /** + * Drops (deletes) a job from MindsDB by name. + * @param {string} name - Name of the job to drop. + * @param {string} project - Project the job belongs to. + * @returns {Promise} - Resolves when the job is dropped. + * @throws {MindsDbError} - Something went wrong while dropping the job. + */ + abstract dropJob(name: string, project: string): Promise; + + /** + * Lists all jobs from MindsDB. + * @param {string} name - Name of the job. + * @param {string} project - Project the job belongs to. + * @returns {Promise} - Resolves after all the jobs are listed. + * @throws {MindsDbError} - Something went wrong while listing the job. + */ + abstract list(name?: string, project?: string): Promise>; +} diff --git a/src/jobs/jobsModule.ts b/src/jobs/jobsModule.ts new file mode 100644 index 0000000..0ea5be8 --- /dev/null +++ b/src/jobs/jobsModule.ts @@ -0,0 +1,3 @@ +import JobsRestApiClient from './jobsRestApiClient'; + +export default { JobsRestApiClient }; diff --git a/src/jobs/jobsRestApiClient.ts b/src/jobs/jobsRestApiClient.ts new file mode 100644 index 0000000..f186a7f --- /dev/null +++ b/src/jobs/jobsRestApiClient.ts @@ -0,0 +1,164 @@ +import mysql from 'mysql'; +import { MindsDbError } from '../errors'; + +import SqlApiClient from '../sql/sqlApiClient'; +import Job from './job'; +import JobsApiClient from './jobsApiClient'; + +export default class JobsRestApiClient extends JobsApiClient { + sqlClient: SqlApiClient; + + /** + * @param {SqlApiClient} sqlClient - SQL API client to send all SQL query requests. + */ + constructor(sqlClient: SqlApiClient) { + super(); + this.sqlClient = sqlClient; + } + + /** + * Creates a new Job instance for building and creating a job. + * @param {string} name - Name of the job. + * @param {string} project - Project the job belongs to. + * @returns {Job} - A new Job instance. + */ + override create(name: string, project: string = "mindsdb"): Job { + return new Job(this, name, project); + } + + /** + * Internal method to create the job in MindsDB. + * @param {string} name - Name of the job to create. + * @param {string} project - Project the job will be created in. + * @param {string} query - Queries to be executed by the job. + * @param {string} [start] - Optional start date for the job. + * @param {string} [end] - Optional end date for the job. + * @param {string} [every] - Optional repetition frequency. + * @param {string} [ifCondition] - Optional condition for job execution. + * @returns {Promise} - Resolves when the job is created. + * @throws {MindsDbError} - Something went wrong while creating the job. + */ + override async createJob( + name: string, + project: string, + query: string, + start?: string, + end?: string, + every?: string, + ifCondition?: string + ): Promise { + let createJobQuery = `CREATE JOB ${mysql.escapeId(project)}.${mysql.escapeId(name)} (\n${query}\n)`; + + if (start) { + createJobQuery += `\nSTART '${start}'`; + } + if (end) { + createJobQuery += `\nEND '${end}'`; + } + if (every) { + createJobQuery += `\nEVERY ${every}`; + } + if (ifCondition) { + createJobQuery += `\nIF (\n${ifCondition}\n)`; + } + + const sqlQueryResult = await this.sqlClient.runQuery(createJobQuery); + if (sqlQueryResult.error_message) { + throw new MindsDbError(sqlQueryResult.error_message); + } + } + + /** + * Internal method to delete the job in MindsDB. + * @param {string} name - Name of the job to delete. + * @param {string} project - Project the job belongs to. + * @returns {Promise} - Resolves when the job is deleted. + * @throws {MindsDbError} - Something went wrong while deleting the job. + */ + override async deleteJob( + name: string, + project: string + ): Promise { + const dropJobQuery = `DROP JOB ${mysql.escapeId(project)}.${mysql.escapeId(name)};`; + + const sqlQueryResult = await this.sqlClient.runQuery(dropJobQuery); + if (sqlQueryResult.error_message) { + throw new MindsDbError(sqlQueryResult.error_message); + } + } + + /** + * Internal method to deleting the job in MindsDB with users providing a name + * @param {string} name - Name of the job to delete. + * @param {string} project - Project the job belongs to. + * @returns {Promise} - Resolves when the job is deleted. + * @throws {MindsDbError} - Something went wrong while deleting the job. + */ + override async dropJob( + name: string, + project: string = "mindsdb" + ): Promise { + const dropJobQuery = `DROP JOB ${mysql.escapeId(project)}.${mysql.escapeId(name)};`; + + const sqlQueryResult = await this.sqlClient.runQuery(dropJobQuery); + if (sqlQueryResult.error_message) { + throw new MindsDbError(sqlQueryResult.error_message); + } + } + + /** + * Retrieves a list of jobs from the information schema. + * + * This asynchronous method queries the database for jobs, optionally filtering + * by project name and/or job name. If both parameters are provided, the method + * combines the filters to return a more specific list of jobs. + * + * @param {string} [name] - The optional name of the job to filter the results. + * If provided, only jobs matching this name will be included. + * @param {string} [project] - The optional project name to filter the results. + * If provided, only jobs associated with this project will be included. + * + * @returns {Promise>} - A promise that resolves to an array of Job objects + * representing the jobs retrieved from the database. + * + * @throws {MindsDbError} - Throws an error if the SQL query execution fails, + * containing the error message returned from the database. + * + * @example + * const jobs = await list('myJob', 'myProject'); + * console.log(jobs); + */ + override async list(name?: string, project?: string): Promise> { + const selectClause = `SELECT * FROM information_schema.jobs`; + let projectClause = ''; + let nameClause = ''; + if(project){ + projectClause = `WHERE project = ${mysql.escape(project)}`; + } + + if(name){ + nameClause = `name = ${mysql.escape(name)}`; + } + + const listJobsQuery = [selectClause, projectClause, nameClause].join( + '\n' + ); + + const sqlQueryResult = await this.sqlClient.runQuery(listJobsQuery); + if (sqlQueryResult.error_message) { + throw new MindsDbError(sqlQueryResult.error_message); + } + return sqlQueryResult.rows.map( + (row) => { + const job = new Job(this, row['NAME'], row['PROJECT']); + job.setEnd(row['END_AT']); + job.setEvery(row['SCHEDULE_STR']); + job.setStart(row['START_AT']); + job.setIfCondition(row['IF_QUERY']); + job.addQuery(row['QUERY']); + return job; + } + ); + } + +} diff --git a/src/ml_engines/ml_engine.ts b/src/ml_engines/ml_engine.ts index 4cdbfaa..c751a52 100644 --- a/src/ml_engines/ml_engine.ts +++ b/src/ml_engines/ml_engine.ts @@ -35,6 +35,24 @@ export default class MLEngine { this.connection_data = connection_data; } + /** + * Lists all mlEngines for the user. + * @returns {Promise>} - List of all mlEngines. + */ + async list(): Promise> { + return this.mlEnginesApiClient.getAllMLEngines(); + } + + /** + * Removes a specified mlEngine by its name. + * @param {string} mlEngineName - The name of the mlEngine to remove. + * @returns {Promise} - Resolves when the mlEngine is successfully removed. + * @throws {MindsDbError} - Something went wrong deleting the mlEngine. + */ + async remove(mlEngineName: string): Promise { + await this.mlEnginesApiClient.deleteMLEngine(mlEngineName); + } + /** Deletes this mlEngine. * @throws {MindsDbError} - Something went wrong deleting the mlEngine. */ diff --git a/src/models/model.ts b/src/models/model.ts index 33ad284..502d4e0 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -149,7 +149,7 @@ class Model { version: number, accuracy?: number, tag?: string, - active?: boolean + active?: boolean, ) { this.modelsApiClient = modelsApiClient; this.name = name; @@ -168,7 +168,19 @@ class Model { * @returns {Array} - All feature descriptions of the model. */ describe(): Promise> { - return this.modelsApiClient.describeModel(this.name, this.project, this.version); + return this.modelsApiClient.describeModel( + this.name, + this.project, + this.version, + ); + } + + /** + * Lists all models in the project. + * @returns {Array} - All models in the project. + */ + listModels(): Promise> { + return this.modelsApiClient.getAllModels(this.project); } /** @@ -177,8 +189,17 @@ class Model { * @param {string} unique_id - Optional unique id to filter the accuracy by. * @returns {Array} - Result. */ - describeAttribute(attribute: string, unique_id?: string): Promise> { - return this.modelsApiClient.describeModelAttribute(this.name, this.project, attribute, this.version, unique_id); + describeAttribute( + attribute: string, + unique_id?: string, + ): Promise> { + return this.modelsApiClient.describeModelAttribute( + this.name, + this.project, + attribute, + this.version, + unique_id, + ); } /** @@ -201,7 +222,7 @@ class Model { this.version, this.targetColumn, this.project, - options + options, ); } @@ -217,7 +238,7 @@ class Model { this.version, this.targetColumn, this.project, - options + options, ); } @@ -232,13 +253,13 @@ class Model { this.name, this.targetColumn, this.project, - options + options, ); } return this.modelsApiClient.retrainModel( this.name, this.targetColumn, - this.project + this.project, ); } @@ -251,6 +272,55 @@ class Model { finetune(integration: string, options: FinetuneOptions): Promise { return this.modelsApiClient.finetuneModel(this.name, this.project, options); } + /** + * List all versions of the model. + * + * @returns {Promise} - A promise that resolves to an array of ModelVersion objects. + */ + listVersions(): Promise { + return this.modelsApiClient.listVersions(this.project); + } + + /** + * Get a specific version of the model by its version number. + * + * @param {number} v - The version number to retrieve. + * @returns {Promise} - A promise that resolves to the requested ModelVersion. + */ + getVersion(v: number): Promise { + return this.modelsApiClient.getVersion( + Math.floor(v), + this.project, + this.name, + ); + } + + /** + * Drop a specific version of the model. + * + * @param {number} v - The version number to drop. + * @param {string} [project=this.project] - The project name. Defaults to the current project. + * @param {string} [model=this.name] - The model name. Defaults to the current model. + * @returns {Promise} - A promise that resolves when the version is dropped. + */ + dropVersion( + v: number, + project: string = this.project, + model: string = this.name, + ): Promise { + return this.modelsApiClient.dropVersion(Math.floor(v), project, model); + } + /** + * Sets the active version of the specified model within a given project. + * @param {number} v - The version number to set as active. + */ + setActiveVersion(v: number): Promise { + return this.modelsApiClient.setActiveVersion( + Math.floor(v), + this.project, + this, + ); + } /** * Creates a Model instance from a row returned from the MindsDB database. @@ -269,9 +339,56 @@ class Model { obj['version'], obj['accuracy'], obj['tag'], - obj['active'] + obj['active'], + ); + } +} + +/** + * Represents a MindsDB model with version and all supported operations. + */ +class ModelVersion extends Model { + /** + * Constructor for ModelVersion. + * + * @param {string} project - Name of the project the model belongs to. + * @param {object} data - Data containing the model details. + */ + constructor( + project: string, + data: { + modelsApiClient: ModelsApiClient; + name: string; + targetColumn: string; + status: string; + updateStatus: UpdateStatus; + version: number; + accuracy?: number; + tag?: string; + active?: boolean; + }, + ) { + super( + data.modelsApiClient, + data.name, + project, + data.targetColumn, + data.status, + data.updateStatus, + data.version, + data.accuracy, + data.tag, + data.active, ); + this.version = data.version; } } -export { Model, ModelFeatureDescription, ModelPrediction, ModelRow, ModelDescribeAttribute }; +export { + Model, + ModelFeatureDescription, + ModelPrediction, + ModelRow, + ModelDescribeAttribute, + ModelVersion, +}; diff --git a/src/models/modelsApiClient.ts b/src/models/modelsApiClient.ts index b49163e..4efb24f 100644 --- a/src/models/modelsApiClient.ts +++ b/src/models/modelsApiClient.ts @@ -1,4 +1,10 @@ -import { Model, ModelDescribeAttribute, ModelFeatureDescription, ModelPrediction } from './model'; +import { + Model, + ModelDescribeAttribute, + ModelFeatureDescription, + ModelPrediction, + ModelVersion, +} from './model'; import { BatchQueryOptions, QueryOptions } from './queryOptions'; import { FinetuneOptions, TrainingOptions } from './trainingOptions'; @@ -31,7 +37,7 @@ export default abstract class ModelsApiClient { abstract describeModel( name: string, project: string, - version?: number + version?: number, ): Promise>; /** @@ -48,7 +54,7 @@ export default abstract class ModelsApiClient { project: string, attribute: string, version?: number, - unique_id?: string + unique_id?: string, ): Promise>; /** @@ -74,7 +80,7 @@ export default abstract class ModelsApiClient { version: number, targetColumn: string, project: string, - options: QueryOptions + options: QueryOptions, ): Promise; /** @@ -91,7 +97,7 @@ export default abstract class ModelsApiClient { version: number, targetColumn: string, project: string, - options: BatchQueryOptions + options: BatchQueryOptions, ): Promise>; /** @@ -106,7 +112,7 @@ export default abstract class ModelsApiClient { name: string, targetColumn: string, project: string, - options: TrainingOptions + options: TrainingOptions, ): Promise; /** @@ -121,7 +127,7 @@ export default abstract class ModelsApiClient { name: string, targetColumn: string, project: string, - options?: TrainingOptions + options?: TrainingOptions, ): Promise; /** @@ -134,6 +140,56 @@ export default abstract class ModelsApiClient { abstract finetuneModel( name: string, project: string, - options?: FinetuneOptions + options?: FinetuneOptions, ): Promise; + /** + * Lists all versions of the model in the specified project. + * + * @param {string} project - The project to list the model versions from. + * @returns {Promise} - A promise that resolves to an array of ModelVersion objects. + */ + abstract listVersions(project: string): Promise; + + /** + * Gets a specific version of the model by its version number and name. + * + * @param {number} v - The version number to retrieve. + * @param {string} project - The project name. + * @param {string} name - The model name. + * @returns {Promise} - A promise that resolves to the requested ModelVersion. + * @throws {Error} - Throws an error if the version is not found. + */ + abstract getVersion( + v: number, + project: string, + name: string, + ): Promise; + + /** + * Drops a specific version of the model in the given project. + * + * @param {number} v - The version number to drop. + * @param {string} project - The project name. + * @param {string} model - The model name. + * @returns {Promise} - A promise that resolves when the version is dropped. + * @throws {MindsDbError} - Throws an error if something goes wrong during the operation. + */ + abstract dropVersion( + v: number, + project: string, + model: string, + ): Promise; + + /** + * Sets the active version of the specified model within a given project. + * @param {number} v - The version number to set as active. + * @param {string} project - The name of the project the model belongs to. + * @param {string} model - The name of the model for which to set the active version. + * @throws {MindsDbError} - If an error occurs while setting the active version. + */ + abstract setActiveVersion( + v: number, + project: string, + model: Model, + ): Promise; } diff --git a/src/models/modelsRestApiClient.ts b/src/models/modelsRestApiClient.ts index f12f4dc..90a69ea 100644 --- a/src/models/modelsRestApiClient.ts +++ b/src/models/modelsRestApiClient.ts @@ -8,10 +8,10 @@ import { ModelFeatureDescription, ModelPrediction, ModelRow, + ModelVersion, } from './model'; import { BatchQueryOptions, QueryOptions } from './queryOptions'; import { MindsDbError } from '../errors'; -import { version } from 'prettier'; /** Implementation of ModelsApiClient that goes through the REST API */ export default class ModelsRestApiClient extends ModelsApiClient { @@ -44,7 +44,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { } private makeTrainingSelectClause( - options: TrainingOptions | FinetuneOptions + options: TrainingOptions | FinetuneOptions, ): string { const select = options['select']; if (select) { @@ -74,7 +74,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { } private makeTrainingWindowHorizonClause( - trainingOptions: TrainingOptions + trainingOptions: TrainingOptions, ): string { const window = trainingOptions['window']; const horizon = trainingOptions['horizon']; @@ -101,7 +101,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { // Escaping WHERE conditions is quite tricky. We should // come up with a better solution to indicate WHERE conditions // when querying so we aren't passing a raw string. - `AND ${o}` + `AND ${o}`, ) .join('\n'); } else { @@ -110,8 +110,26 @@ export default class ModelsRestApiClient extends ModelsApiClient { return whereClause; } + private makeUsingClause (using: string | Array): string { + if (!using) { + return ''; + } + if (!Array.isArray(using)) { + return `USING ${using}\n`; + } + if (using.length === 0) { + return ''; + } + let usingClause = `USING\n`; + for (let i = 0; i < using.length - 1; i++) { + usingClause += using[i] + `,\n`; + } + usingClause += using[using.length -1 ] + `\n`; + return usingClause; + } + private makeTrainingUsingClause( - options: FinetuneOptions | TrainingOptions + options: FinetuneOptions | TrainingOptions, ): string { const using = options['using']; if (!using) { @@ -141,7 +159,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { override async getModel( name: string, project: string, - version?: number + version?: number, ): Promise { const selectQuery = `SELECT * FROM ${mysql.escapeId(project)}.models${ version ? '_versions' : '' @@ -164,7 +182,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { const selectQuery = `SELECT * FROM ${mysql.escapeId(project)}.models`; const sqlQueryResult = await this.sqlClient.runQuery(selectQuery); return sqlQueryResult.rows.map((modelRow) => - Model.fromModelRow(modelRow as ModelRow, this) + Model.fromModelRow(modelRow as ModelRow, this), ); } @@ -178,11 +196,11 @@ export default class ModelsRestApiClient extends ModelsApiClient { override async describeModel( name: string, project: string, - version?: number + version?: number, ): Promise> { const describeQuery = `DESCRIBE ${mysql.escapeId(project)}.${mysql.escapeId( - name - )}.${ version ? `${mysql.escapeId(version.toString())}.` : ''}\`features\``; + name, + )}.${version ? `${mysql.escapeId(version.toString())}.` : ''}\`features\``; const sqlQueryResult = await this.sqlClient.runQuery(describeQuery); if (sqlQueryResult.rows.length === 0) { return []; @@ -204,11 +222,11 @@ export default class ModelsRestApiClient extends ModelsApiClient { project: string, attribute: string, version?: number, - unique_id?: string + unique_id?: string, ): Promise> { const describeQuery = `DESCRIBE ${mysql.escapeId(project)}.${mysql.escapeId( - name - )}.${ version ? `${mysql.escapeId(version.toString())}.` : ''}${mysql.escapeId(attribute)}${unique_id ? `.${mysql.escapeId(unique_id)}` : ''}`; + name, + )}.${version ? `${mysql.escapeId(version.toString())}.` : ''}${mysql.escapeId(attribute)}${unique_id ? `.${mysql.escapeId(unique_id)}` : ''}`; const sqlQueryResult = await this.sqlClient.runQuery(describeQuery); if (sqlQueryResult.rows.length === 0) { return []; @@ -225,7 +243,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { */ override async deleteModel(name: string, project: string): Promise { const deleteQuery = `DROP MODEL ${mysql.escapeId(project)}.${mysql.escapeId( - name + name, )}`; const sqlQueryResult = await this.sqlClient.runQuery(deleteQuery); if (sqlQueryResult.error_message) { @@ -247,13 +265,14 @@ export default class ModelsRestApiClient extends ModelsApiClient { version: number, targetColumn: string, project: string, - options: QueryOptions + options: QueryOptions, ): Promise { const selectClause = `SELECT * FROM ${mysql.escapeId( - project + project, )}.${mysql.escapeId(name)}.${version}`; const whereClause = this.makeWhereClause(options['where'] || []); - const selectQuery = [selectClause, whereClause].join('\n'); + const usingClause = this.makeUsingClause(options['using'] || []); + const selectQuery = [selectClause, whereClause, usingClause].join('\n'); const sqlQueryResult = await this.sqlClient.runQuery(selectQuery); if (sqlQueryResult.error_message) { throw new MindsDbError(sqlQueryResult.error_message); @@ -281,15 +300,15 @@ export default class ModelsRestApiClient extends ModelsApiClient { version: number, targetColumn: string, project: string, - options: BatchQueryOptions + options: BatchQueryOptions, ): Promise> { const selectClause = `SELECT m.${mysql.escapeId( - targetColumn + targetColumn, )} AS predicted, t.*, m.*`; const joinId = options['join']; const fromClause = `FROM ${mysql.escapeId(joinId)} AS t`; const joinClause = `JOIN ${mysql.escapeId(project)}.${mysql.escapeId( - name + name, )}.${version} AS m`; const whereClause = this.makeWhereClause(options['where'] || []); const limitClause = options['limit'] @@ -325,7 +344,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { name: string, targetColumn: string, project: string, - trainingOptions: TrainingOptions + trainingOptions: TrainingOptions, ): Promise { const createClause = this.makeTrainingCreateClause(name, project); const fromClause = this.makeTrainingFromClause(trainingOptions); @@ -360,7 +379,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { targetColumn, 'generating', 'up_to_date', - 1 + 1, ); } @@ -377,7 +396,7 @@ export default class ModelsRestApiClient extends ModelsApiClient { name: string, targetColumn: string, project: string, - trainingOptions?: TrainingOptions + trainingOptions?: TrainingOptions, ): Promise { const retrainClause = this.makeRetrainClause(name, project); let query = retrainClause; @@ -422,10 +441,10 @@ export default class ModelsRestApiClient extends ModelsApiClient { override async finetuneModel( name: string, project: string, - finetuneOptions: FinetuneOptions + finetuneOptions: FinetuneOptions, ): Promise { const finetuneClause = `FINETUNE ${mysql.escapeId(project)}.${mysql.escapeId( - name + name, )} FROM ${mysql.escapeId(finetuneOptions['integration'])}`; const selectClause = this.makeTrainingSelectClause(finetuneOptions); const usingClause = this.makeTrainingUsingClause(finetuneOptions); @@ -437,4 +456,82 @@ export default class ModelsRestApiClient extends ModelsApiClient { return Model.fromModelRow(sqlQueryResult.rows[0] as ModelRow, this); } + + /** + * List all versions of the model in the specified project. + * + * @param {string} project - The project to list the model versions from. + * @returns {Promise} - A promise that resolves to an array of ModelVersion objects. + */ + override async listVersions(project: string): Promise { + const allModels = await this.getAllModels(project); + return allModels.map((model: any) => new ModelVersion(project, model)); + } + + /** + * Get a specific version of the model by its version number and name. + * + * @param {number} v - The version number to retrieve. + * @param {string} project - The project name. + * @param {string} name - The model name. + * @returns {Promise} - A promise that resolves to the requested ModelVersion. + * @throws {Error} - Throws an error if the version is not found. + */ + override async getVersion( + v: number, + project: string, + name: string, + ): Promise { + const allModels = await this.listVersions(project); + for (const model of allModels) { + if (model.version === v && model.name === name) { + return model; + } + } + throw new Error('Version is not found'); + } + + /** + * Drop a specific version of the model in the given project. + * + * @param {number} v - The version number to drop. + * @param {string} project - The project name. + * @param {string} model - The model name. + * @returns {Promise} - A promise that resolves when the version is dropped. + * @throws {MindsDbError} - Throws an error if something goes wrong during the operation. + */ + override async dropVersion( + v: number, + project: string, + model: string, + ): Promise { + const deleteQuery = `DROP MODEL ${mysql.escapeId(project)}.${mysql.escapeId( + model, + )}.${mysql.escapeId(v)}`; + const sqlQueryResult = await this.sqlClient.runQuery(deleteQuery); + if (sqlQueryResult.error_message) { + throw new MindsDbError(sqlQueryResult.error_message); + } + } + + /** + * Sets the active version of the specified model within a given project. + * @param {number} v - The version number to set as active. + * @param {string} project - The name of the project the model belongs to. + * @param {Model} model - The model for which to set the active version. + * @throws {MindsDbError} - If an error occurs while setting the active version. + */ + override async setActiveVersion(v: number, project: string, model: Model) { + const query = `SET model_active = ${mysql.escapeId(project)}.${mysql.escapeId( + model.name, + )}.${mysql.escapeId(v.toString())};`; + await this.sqlClient + .runQuery(query) + .then( + async () => + (model = + (await this.getModel(model.name, project)) ?? + new ModelVersion(project, { ...model, version: v })), + ); + } } diff --git a/src/models/queryOptions.ts b/src/models/queryOptions.ts index 1128850..cbb22ff 100644 --- a/src/models/queryOptions.ts +++ b/src/models/queryOptions.ts @@ -8,6 +8,11 @@ interface QueryOptions { * Example: ['t.field1 = val1', 't.field2 = val2'] joins against source data where field1 = val1 and field2 = val2. */ where?: string | Array; + + /** A single USING condition, or array of USING statements to set the query options.. + * Example: 'max_tokens = 100' or ['max_tokens = 100', 'tempreature = 0.5']. + */ + using?: string | Array; } /** Structure of options to use when making a batch prediction. */ diff --git a/src/tables/table.ts b/src/tables/table.ts index a4e9c8b..7a4f69d 100644 --- a/src/tables/table.ts +++ b/src/tables/table.ts @@ -30,6 +30,16 @@ export default class Table { this.integration = integration; } + /** + * Creates a table in an integration from a given SELECT statement. + * @param {string} select - SELECT statement to use for populating the new table with data. + * @returns {Promise} - Newly created table. + * @throws {MindsDbError} - Something went wrong creating the table. + */ + async create(select: string): Promise
{ + return await this.tablesApiClient.createTable(this.name, this.integration, select); + } + /** * Deletes this table from its integration. * @throws {MindsDbError} - Something went wrong deleting this table. diff --git a/src/tables/tablesApiClient.ts b/src/tables/tablesApiClient.ts index d82c562..8c27ed8 100644 --- a/src/tables/tablesApiClient.ts +++ b/src/tables/tablesApiClient.ts @@ -40,4 +40,11 @@ export default abstract class TablesApiClient { * @throws {MindsDbError} - Something went wrong deleting the table. */ abstract deleteTable(name: string, integration: string): Promise; + + /** + * Deletes a file from the files integration. + * @param {string} name - Name of the file to be deleted. + * @throws {MindsDbError} - Something went wrong deleting the file. + */ + abstract deleteFile(name: string): Promise; } diff --git a/src/tables/tablesRestApiClient.ts b/src/tables/tablesRestApiClient.ts index 2b83531..27f9462 100644 --- a/src/tables/tablesRestApiClient.ts +++ b/src/tables/tablesRestApiClient.ts @@ -87,4 +87,18 @@ export default class TablesRestApiClient extends TablesApiClient { throw new MindsDbError(sqlQueryResult.error_message); } } + + /** + * Deletes a file from the files integration. + * @param {string} name - Name of the file to be deleted. + * @throws {MindsDbError} - Something went wrong deleting the file. + */ + override async deleteFile(name: string): Promise { + const sqlQuery = `DROP TABLE files.${mysql.escapeId(name)}`; + + const sqlQueryResult = await this.sqlClient.runQuery(sqlQuery); + if (sqlQueryResult.error_message) { + throw new MindsDbError(sqlQueryResult.error_message); + } + } } diff --git a/src/views/view.ts b/src/views/view.ts index 2f724b5..07c8a81 100644 --- a/src/views/view.ts +++ b/src/views/view.ts @@ -25,6 +25,16 @@ export default class View { this.project = project; } + /**Creates this view in the project. + * @param {string} select - SELECT statement to use for populating the new view with data. + * @returns {Promise} - Newly created view. + * @throws {MindsDbError} - Something went wrong creating this view. + */ + async create(select: string): Promise { + return await this.viewsApiClient.createView(this.name, this.project, select); + } + + /** Deletes this view from the project it belongs to. * @throws {MindsDbError} - Something went wrong deleting this view. */