diff --git a/lib/client.js b/lib/client.js index a4fe8eaf..33147389 100644 --- a/lib/client.js +++ b/lib/client.js @@ -28,6 +28,8 @@ const Ref = require('./ref'); const Formatter = require('./formatter'); const Logger = require('./logger'); const { POOL_CONFIG_OPTIONS } = require('./constants'); +const ViewBuilder = require('./schema/viewbuilder.js'); +const ViewCompiler = require('./schema/viewcompiler.js'); const debug = require('debug')('knex:client'); @@ -102,10 +104,18 @@ class Client extends EventEmitter { return new TableBuilder(this, type, tableName, tableNameLike, fn); } + viewBuilder(type, viewBuilder, fn) { + return new ViewBuilder(this, type, viewBuilder, fn); + } + tableCompiler(tableBuilder) { return new TableCompiler(this, tableBuilder); } + viewCompiler(viewCompiler) { + return new ViewCompiler(this, viewCompiler); + } + columnBuilder(tableBuilder, type, args) { return new ColumnBuilder(this, tableBuilder, type, args); } diff --git a/lib/dialects/cockroachdb/crdb-viewcompiler.js b/lib/dialects/cockroachdb/crdb-viewcompiler.js new file mode 100644 index 00000000..4faed557 --- /dev/null +++ b/lib/dialects/cockroachdb/crdb-viewcompiler.js @@ -0,0 +1,15 @@ +const ViewCompiler_PG = require('../postgres/schema/pg-viewcompiler.js'); + +class ViewCompiler_CRDB extends ViewCompiler_PG { + renameColumn(from, to) { + throw new Error('rename column of views is not supported by this dialect.'); + } + + defaultTo(column, defaultValue) { + throw new Error( + 'change default values of views is not supported by this dialect.' + ); + } +} + +module.exports = ViewCompiler_CRDB; diff --git a/lib/dialects/cockroachdb/index.js b/lib/dialects/cockroachdb/index.js index 55fa0602..31159b34 100644 --- a/lib/dialects/cockroachdb/index.js +++ b/lib/dialects/cockroachdb/index.js @@ -4,6 +4,7 @@ const Client_PostgreSQL = require('../postgres'); const Transaction = require('../postgres/execution/pg-transaction'); const QueryCompiler = require('./crdb-querycompiler'); const TableCompiler = require('./crdb-tablecompiler'); +const ViewCompiler = require('./crdb-viewcompiler'); // Always initialize with the "QueryBuilder" and "QueryCompiler" // objects, which extend the base 'lib/query/builder' and @@ -21,6 +22,10 @@ class Client_CockroachDB extends Client_PostgreSQL { return new TableCompiler(this, ...arguments); } + viewCompiler() { + return new ViewCompiler(this, ...arguments); + } + _parseVersion(versionString) { return versionString.split(' ')[2]; } diff --git a/lib/dialects/mssql/index.js b/lib/dialects/mssql/index.js index 56b16e9a..a918182e 100644 --- a/lib/dialects/mssql/index.js +++ b/lib/dialects/mssql/index.js @@ -11,6 +11,7 @@ const Transaction = require('./transaction'); const QueryCompiler = require('./query/mssql-querycompiler'); const SchemaCompiler = require('./schema/mssql-compiler'); const TableCompiler = require('./schema/mssql-tablecompiler'); +const ViewCompiler = require('./schema/mssql-viewcompiler'); const ColumnCompiler = require('./schema/mssql-columncompiler'); const QueryBuilder = require('../../query/querybuilder'); @@ -112,6 +113,9 @@ class Client_MSSQL extends Client { return new TableCompiler(this, ...arguments); } + viewCompiler() { + return new ViewCompiler(this, ...arguments); + } queryBuilder() { const b = new QueryBuilder(this); return b; diff --git a/lib/dialects/mssql/schema/mssql-compiler.js b/lib/dialects/mssql/schema/mssql-compiler.js index 22cfbea4..0bfb5e92 100644 --- a/lib/dialects/mssql/schema/mssql-compiler.js +++ b/lib/dialects/mssql/schema/mssql-compiler.js @@ -14,6 +14,13 @@ class SchemaCompiler_MSSQL extends SchemaCompiler { ); } + dropViewIfExists(viewName) { + const name = this.formatter.wrap(prefixedTableName(this.schema, viewName)); + this.pushQuery( + `if object_id('${name}', 'V') is not null DROP VIEW ${name}` + ); + } + // Rename a table on the schema. renameTable(tableName, to) { this.pushQuery( @@ -25,6 +32,16 @@ class SchemaCompiler_MSSQL extends SchemaCompiler { ); } + renameView(viewTable, to) { + this.pushQuery( + `exec sp_rename ${this.client.parameter( + prefixedTableName(this.schema, viewTable), + this.builder, + this.bindingsHolder + )}, ${this.client.parameter(to, this.builder, this.bindingsHolder)}` + ); + } + // Check whether a table exists on the query. hasTable(tableName) { const formattedTable = this.client.parameter( diff --git a/lib/dialects/mssql/schema/mssql-viewcompiler.js b/lib/dialects/mssql/schema/mssql-viewcompiler.js new file mode 100644 index 00000000..3dedb76c --- /dev/null +++ b/lib/dialects/mssql/schema/mssql-viewcompiler.js @@ -0,0 +1,41 @@ +/* eslint max-len: 0 */ + +const ViewCompiler = require('../../../schema/viewcompiler.js'); + +class ViewCompiler_MSSQL extends ViewCompiler { + constructor(client, viewCompiler) { + super(client, viewCompiler); + } + + createQuery(columns, selectQuery, materialized, replace) { + const createStatement = 'CREATE ' + (replace ? 'OR ALTER ' : '') + 'VIEW '; + let sql = createStatement + this.viewName(); + + sql += ' (' + columns.join(', ') + ')'; + sql += ' AS '; + sql += selectQuery.toString(); + this.pushQuery({ + sql, + }); + } + + renameColumn(from, to) { + this.pushQuery( + `exec sp_rename ${this.client.parameter( + this.viewName() + '.' + from, + this.viewBuilder, + this.bindingsHolder + )}, ${this.client.parameter( + to, + this.viewBuilder, + this.bindingsHolder + )}, 'COLUMN'` + ); + } + + createOrReplace() { + this.createQuery(this.columns, this.selectQuery, false, true); + } +} + +module.exports = ViewCompiler_MSSQL; diff --git a/lib/dialects/mysql/index.js b/lib/dialects/mysql/index.js index bb233d9f..d4eb4efb 100644 --- a/lib/dialects/mysql/index.js +++ b/lib/dialects/mysql/index.js @@ -12,6 +12,8 @@ const TableCompiler = require('./schema/mysql-tablecompiler'); const ColumnCompiler = require('./schema/mysql-columncompiler'); const { makeEscape } = require('../../util/string'); +const ViewCompiler = require('./schema/mysql-viewcompiler'); +const ViewBuilder = require('./schema/mysql-viewbuilder'); // Always initialize with the "QueryBuilder" and "QueryCompiler" // objects, which extend the base 'lib/query/builder' and @@ -33,6 +35,14 @@ class Client_MySQL extends Client { return new TableCompiler(this, ...arguments); } + viewCompiler() { + return new ViewCompiler(this, ...arguments); + } + + viewBuilder() { + return new ViewBuilder(this, ...arguments); + } + columnCompiler() { return new ColumnCompiler(this, ...arguments); } diff --git a/lib/dialects/mysql/schema/mysql-compiler.js b/lib/dialects/mysql/schema/mysql-compiler.js index 71100954..9330e479 100644 --- a/lib/dialects/mysql/schema/mysql-compiler.js +++ b/lib/dialects/mysql/schema/mysql-compiler.js @@ -16,6 +16,10 @@ class SchemaCompiler_MySQL extends SchemaCompiler { ); } + renameView(from, to) { + this.renameTable(from, to); + } + // Check whether a table exists on the query. hasTable(tableName) { let sql = 'select * from information_schema.tables where table_name = ?'; diff --git a/lib/dialects/mysql/schema/mysql-viewbuilder.js b/lib/dialects/mysql/schema/mysql-viewbuilder.js new file mode 100644 index 00000000..bfbec27b --- /dev/null +++ b/lib/dialects/mysql/schema/mysql-viewbuilder.js @@ -0,0 +1,21 @@ +const ViewBuilder = require('../../../schema/viewbuilder.js'); + +class ViewBuilder_MySQL extends ViewBuilder { + constructor() { + super(...arguments); + } + + checkOption() { + this._single.checkOption = 'default_option'; + } + + localCheckOption() { + this._single.checkOption = 'local'; + } + + cascadedCheckOption() { + this._single.checkOption = 'cascaded'; + } +} + +module.exports = ViewBuilder_MySQL; diff --git a/lib/dialects/mysql/schema/mysql-viewcompiler.js b/lib/dialects/mysql/schema/mysql-viewcompiler.js new file mode 100644 index 00000000..60e2ad65 --- /dev/null +++ b/lib/dialects/mysql/schema/mysql-viewcompiler.js @@ -0,0 +1,15 @@ +/* eslint max-len: 0 */ + +const ViewCompiler = require('../../../schema/viewcompiler.js'); + +class ViewCompiler_MySQL extends ViewCompiler { + constructor(client, viewCompiler) { + super(client, viewCompiler); + } + + createOrReplace() { + this.createQuery(this.columns, this.selectQuery, false, true); + } +} + +module.exports = ViewCompiler_MySQL; diff --git a/lib/dialects/oracle/schema/oracle-compiler.js b/lib/dialects/oracle/schema/oracle-compiler.js index 8bf0cda7..2307911a 100644 --- a/lib/dialects/oracle/schema/oracle-compiler.js +++ b/lib/dialects/oracle/schema/oracle-compiler.js @@ -77,16 +77,45 @@ class SchemaCompiler_Oracle extends SchemaCompiler { } dropTableIfExists(tableName) { + this.dropObject(tableName, 'table'); + } + + dropViewIfExists(viewName) { + this.dropObject(viewName, 'view'); + } + + dropObject(objectName, type) { const prefix = this.schema ? `"${this.schema}".` : ''; + let errorCode = -942; + if (type === 'materialized view') { + // https://stackoverflow.com/a/1801453 + errorCode = -12003; + } this.pushQuery( utils.wrapSqlWithCatch( - `drop table ${prefix}${this.formatter.wrap(tableName)}`, - -942 + `drop ${type} ${prefix}${this.formatter.wrap(objectName)}`, + errorCode ) ); // removing the sequence that was possibly generated by increments() column - this._dropRelatedSequenceIfExists(tableName); + this._dropRelatedSequenceIfExists(objectName); + } + + refreshMaterializedView(viewName) { + return this.pushQuery({ + sql: `BEGIN DBMS_MVIEW.REFRESH('${ + this.schemaNameRaw ? this.schemaNameRaw + '.' : '' + }${viewName}'); END;`, + }); + } + + dropMaterializedView(viewName) { + this._dropView(viewName, false, true); + } + + dropMaterializedViewIfExists(viewName) { + this.dropObject(viewName, 'materialized view'); } } diff --git a/lib/dialects/oracledb/index.js b/lib/dialects/oracledb/index.js index 0e18ef38..1a38c29b 100644 --- a/lib/dialects/oracledb/index.js +++ b/lib/dialects/oracledb/index.js @@ -12,6 +12,8 @@ const Formatter = require('../../formatter'); const QueryCompiler = require('./query/oracledb-querycompiler'); const TableCompiler = require('./schema/oracledb-tablecompiler'); const ColumnCompiler = require('./schema/oracledb-columncompiler'); +const ViewCompiler = require('./schema/oracledb-viewcompiler'); +const ViewBuilder = require('./schema/oracledb-viewbuilder'); const { BlobHelper, ReturningHelper, isConnectionError } = require('./utils'); const Transaction = require('./transaction'); const Client_Oracle = require('../oracle'); @@ -67,6 +69,14 @@ class Client_Oracledb extends Client_Oracle { return new ColumnCompiler(this, ...arguments); } + viewBuilder() { + return new ViewBuilder(this, ...arguments); + } + + viewCompiler() { + return new ViewCompiler(this, ...arguments); + } + formatter(builder) { return new Formatter(this, builder); } diff --git a/lib/dialects/oracledb/schema/oracledb-viewbuilder.js b/lib/dialects/oracledb/schema/oracledb-viewbuilder.js new file mode 100644 index 00000000..6bd08ec0 --- /dev/null +++ b/lib/dialects/oracledb/schema/oracledb-viewbuilder.js @@ -0,0 +1,13 @@ +const ViewBuilder = require('../../../schema/viewbuilder.js'); + +class ViewBuilder_Oracledb extends ViewBuilder { + constructor() { + super(...arguments); + } + + checkOption() { + this._single.checkOption = 'default_option'; + } +} + +module.exports = ViewBuilder_Oracledb; diff --git a/lib/dialects/oracledb/schema/oracledb-viewcompiler.js b/lib/dialects/oracledb/schema/oracledb-viewcompiler.js new file mode 100644 index 00000000..fd815d24 --- /dev/null +++ b/lib/dialects/oracledb/schema/oracledb-viewcompiler.js @@ -0,0 +1,19 @@ +/* eslint max-len: 0 */ + +const ViewCompiler = require('../../../schema/viewcompiler.js'); + +class ViewCompiler_Oracledb extends ViewCompiler { + constructor(client, viewCompiler) { + super(client, viewCompiler); + } + + createOrReplace() { + this.createQuery(this.columns, this.selectQuery, false, true); + } + + createMaterializedView() { + this.createQuery(this.columns, this.selectQuery, true); + } +} + +module.exports = ViewCompiler_Oracledb; diff --git a/lib/dialects/postgres/index.js b/lib/dialects/postgres/index.js index 0ae78041..ce44a0c7 100644 --- a/lib/dialects/postgres/index.js +++ b/lib/dialects/postgres/index.js @@ -9,6 +9,8 @@ const Transaction = require('./execution/pg-transaction'); const QueryCompiler = require('./query/pg-querycompiler'); const ColumnCompiler = require('./schema/pg-columncompiler'); const TableCompiler = require('./schema/pg-tablecompiler'); +const ViewCompiler = require('./schema/pg-viewcompiler'); +const ViewBuilder = require('./schema/pg-viewbuilder'); const SchemaCompiler = require('./schema/pg-compiler'); const { makeEscape } = require('../../util/string'); const { isString } = require('../../util/is'); @@ -44,6 +46,14 @@ class Client_PG extends Client { return new TableCompiler(this, ...arguments); } + viewCompiler() { + return new ViewCompiler(this, ...arguments); + } + + viewBuilder() { + return new ViewBuilder(this, ...arguments); + } + _driver() { return require('pg'); } diff --git a/lib/dialects/postgres/schema/pg-compiler.js b/lib/dialects/postgres/schema/pg-compiler.js index 63214b47..e34cfa46 100644 --- a/lib/dialects/postgres/schema/pg-compiler.js +++ b/lib/dialects/postgres/schema/pg-compiler.js @@ -110,6 +110,27 @@ class SchemaCompiler_PG extends SchemaCompiler { `create extension if not exists ${this.formatter.wrap(extensionName)}` ); } + + renameView(from, to) { + this.pushQuery( + this.alterViewPrefix + + `${this.formatter.wrap(from)} rename to ${this.formatter.wrap(to)}` + ); + } + + refreshMaterializedView(viewName) { + this.pushQuery({ + sql: `refresh materialized view ${this.formatter.wrap(viewName)}`, + }); + } + + dropMaterializedView(viewName) { + this._dropView(viewName, false, true); + } + + dropMaterializedViewIfExists(viewName) { + this._dropView(viewName, true, true); + } } module.exports = SchemaCompiler_PG; diff --git a/lib/dialects/postgres/schema/pg-viewbuilder.js b/lib/dialects/postgres/schema/pg-viewbuilder.js new file mode 100644 index 00000000..bb02be00 --- /dev/null +++ b/lib/dialects/postgres/schema/pg-viewbuilder.js @@ -0,0 +1,21 @@ +const ViewBuilder = require('../../../schema/viewbuilder.js'); + +class ViewBuilder_PG extends ViewBuilder { + constructor() { + super(...arguments); + } + + checkOption() { + this._single.checkOption = 'default_option'; + } + + localCheckOption() { + this._single.checkOption = 'local'; + } + + cascadedCheckOption() { + this._single.checkOption = 'cascaded'; + } +} + +module.exports = ViewBuilder_PG; diff --git a/lib/dialects/postgres/schema/pg-viewcompiler.js b/lib/dialects/postgres/schema/pg-viewcompiler.js new file mode 100644 index 00000000..c9a18da2 --- /dev/null +++ b/lib/dialects/postgres/schema/pg-viewcompiler.js @@ -0,0 +1,35 @@ +/* eslint max-len: 0 */ + +const ViewCompiler = require('../../../schema/viewcompiler.js'); + +class ViewCompiler_PG extends ViewCompiler { + constructor(client, viewCompiler) { + super(client, viewCompiler); + } + + renameColumn(from, to) { + return this.pushQuery({ + sql: `alter view ${this.viewName()} rename ${this.formatter.wrap( + from + )} to ${this.formatter.wrap(to)}`, + }); + } + + defaultTo(column, defaultValue) { + return this.pushQuery({ + sql: `alter view ${this.viewName()} alter ${this.formatter.wrap( + column + )} set default ${defaultValue}`, + }); + } + + createOrReplace() { + this.createQuery(this.columns, this.selectQuery, false, true); + } + + createMaterializedView() { + this.createQuery(this.columns, this.selectQuery, true); + } +} + +module.exports = ViewCompiler_PG; diff --git a/lib/dialects/redshift/index.js b/lib/dialects/redshift/index.js index ab6700be..ba98d285 100644 --- a/lib/dialects/redshift/index.js +++ b/lib/dialects/redshift/index.js @@ -9,6 +9,7 @@ const ColumnBuilder = require('./schema/redshift-columnbuilder'); const ColumnCompiler = require('./schema/redshift-columncompiler'); const TableCompiler = require('./schema/redshift-tablecompiler'); const SchemaCompiler = require('./schema/redshift-compiler'); +const ViewCompiler = require('./schema/redshift-viewcompiler'); class Client_Redshift extends Client_PG { transaction() { @@ -35,6 +36,10 @@ class Client_Redshift extends Client_PG { return new SchemaCompiler(this, ...arguments); } + viewCompiler() { + return new ViewCompiler(this, ...arguments); + } + _driver() { return require('pg'); } diff --git a/lib/dialects/redshift/schema/redshift-viewcompiler.js b/lib/dialects/redshift/schema/redshift-viewcompiler.js new file mode 100644 index 00000000..53c5f427 --- /dev/null +++ b/lib/dialects/redshift/schema/redshift-viewcompiler.js @@ -0,0 +1,11 @@ +/* eslint max-len: 0 */ + +const ViewCompiler_PG = require('../../postgres/schema/pg-viewcompiler.js'); + +class ViewCompiler_Redshift extends ViewCompiler_PG { + constructor(client, viewCompiler) { + super(client, viewCompiler); + } +} + +module.exports = ViewCompiler_Redshift; diff --git a/lib/dialects/sqlite3/index.js b/lib/dialects/sqlite3/index.js index 7bdb2a43..2be5d231 100644 --- a/lib/dialects/sqlite3/index.js +++ b/lib/dialects/sqlite3/index.js @@ -12,6 +12,7 @@ const SqliteQueryCompiler = require('./query/sqlite-querycompiler'); const SchemaCompiler = require('./schema/sqlite-compiler'); const ColumnCompiler = require('./schema/sqlite-columncompiler'); const TableCompiler = require('./schema/sqlite-tablecompiler'); +const ViewCompiler = require('./schema/sqlite-viewcompiler'); const SQLite3_DDL = require('./schema/ddl'); const Formatter = require('../../formatter'); @@ -43,6 +44,10 @@ class Client_SQLite3 extends Client { return new SqliteQueryCompiler(this, builder, formatter); } + viewCompiler(builder, formatter) { + return new ViewCompiler(this, builder, formatter); + } + columnCompiler() { return new ColumnCompiler(this, ...arguments); } diff --git a/lib/dialects/sqlite3/schema/sqlite-viewcompiler.js b/lib/dialects/sqlite3/schema/sqlite-viewcompiler.js new file mode 100644 index 00000000..1359d77e --- /dev/null +++ b/lib/dialects/sqlite3/schema/sqlite-viewcompiler.js @@ -0,0 +1,11 @@ +/* eslint max-len: 0 */ + +const ViewCompiler = require('../../../schema/viewcompiler.js'); + +class ViewCompiler_PG extends ViewCompiler { + constructor(client, viewCompiler) { + super(client, viewCompiler); + } +} + +module.exports = ViewCompiler_PG; diff --git a/lib/schema/builder.js b/lib/schema/builder.js index b424205a..f9a38e97 100644 --- a/lib/schema/builder.js +++ b/lib/schema/builder.js @@ -46,6 +46,14 @@ class SchemaBuilder extends EventEmitter { 'createTable', 'createTableIfNotExists', 'createTableLike', + 'createView', + 'createViewOrReplace', + 'createMaterializedView', + 'refreshMaterializedView', + 'dropView', + 'dropViewIfExists', + 'dropMaterializedView', + 'dropMaterializedViewIfExists', 'createSchema', 'createSchemaIfNotExists', 'dropSchema', @@ -56,10 +64,13 @@ class SchemaBuilder extends EventEmitter { 'dropExtensionIfExists', 'table', 'alterTable', + 'view', + 'alterView', 'hasTable', 'hasColumn', 'dropTable', 'renameTable', + 'renameView', 'dropTableIfExists', 'raw', ].forEach(function (method) { @@ -76,6 +87,7 @@ class SchemaBuilder extends EventEmitter { ); } if (method === 'table') method = 'alterTable'; + if (method === 'view') method = 'alterView'; this._sequence.push({ method, args: toArray(arguments), diff --git a/lib/schema/compiler.js b/lib/schema/compiler.js index 5a06001c..4a891686 100644 --- a/lib/schema/compiler.js +++ b/lib/schema/compiler.js @@ -52,6 +52,40 @@ class SchemaCompiler { ); } + dropView(viewName) { + this._dropView(viewName, false, false); + } + + dropViewIfExists(viewName) { + this._dropView(viewName, true, false); + } + + dropMaterializedView(viewName) { + throw new Error('materialized views are not supported by this dialect.'); + } + + dropMaterializedViewIfExists(viewName) { + throw new Error('materialized views are not supported by this dialect.'); + } + + renameView(from, to) { + throw new Error( + 'rename view is not supported by this dialect (instead drop then create another view).' + ); + } + + refreshMaterializedView() { + throw new Error('materialized views are not supported by this dialect.'); + } + + _dropView(viewName, ifExists, materialized) { + this.pushQuery( + (materialized ? this.dropMaterializedViewPrefix : this.dropViewPrefix) + + (ifExists ? 'if exists ' : '') + + this.formatter.wrap(prefixedTableName(this.schema, viewName)) + ); + } + raw(sql, bindings) { this.sequence.push(this.client.raw(sql, bindings).toSQL()); } @@ -78,32 +112,42 @@ class SchemaCompiler { } SchemaCompiler.prototype.dropTablePrefix = 'drop table '; +SchemaCompiler.prototype.dropViewPrefix = 'drop view '; +SchemaCompiler.prototype.dropMaterializedViewPrefix = 'drop materialized view '; +SchemaCompiler.prototype.alterViewPrefix = 'alter view '; SchemaCompiler.prototype.alterTable = buildTable('alter'); SchemaCompiler.prototype.createTable = buildTable('create'); SchemaCompiler.prototype.createTableIfNotExists = buildTable('createIfNot'); SchemaCompiler.prototype.createTableLike = buildTable('createLike'); +SchemaCompiler.prototype.createView = buildView('create'); +SchemaCompiler.prototype.createViewOrReplace = buildView('createOrReplace'); +SchemaCompiler.prototype.createMaterializedView = buildView( + 'createMaterializedView' +); +SchemaCompiler.prototype.alterView = buildView('alter'); + SchemaCompiler.prototype.pushQuery = pushQuery; SchemaCompiler.prototype.pushAdditional = pushAdditional; SchemaCompiler.prototype.unshiftQuery = unshiftQuery; -function buildTable(type) { - function build(builder) { - // pass queryContext down to tableBuilder but do not overwrite it if already set - const queryContext = this.builder.queryContext(); - if (queryContext !== undefined && builder.queryContext() === undefined) { - builder.queryContext(queryContext); - } - - builder.setSchema(this.schema); - const sql = builder.toSQL(); - - for (let i = 0, l = sql.length; i < l; i++) { - this.sequence.push(sql[i]); - } +function build(builder) { + // pass queryContext down to tableBuilder but do not overwrite it if already set + const queryContext = this.builder.queryContext(); + if (queryContext !== undefined && builder.queryContext() === undefined) { + builder.queryContext(queryContext); } + builder.setSchema(this.schema); + const sql = builder.toSQL(); + + for (let i = 0, l = sql.length; i < l; i++) { + this.sequence.push(sql[i]); + } +} + +function buildTable(type) { if (type === 'createLike') { return function (tableName, tableNameLike, fn) { const builder = this.client.tableBuilder( @@ -122,6 +166,13 @@ function buildTable(type) { } } +function buildView(type) { + return function (viewName, fn) { + const builder = this.client.viewBuilder(type, viewName, fn); + build.call(this, builder); + }; +} + function prefixedTableName(prefix, table) { return prefix ? `${prefix}.${table}` : table; } diff --git a/lib/schema/viewbuilder.js b/lib/schema/viewbuilder.js new file mode 100644 index 00000000..f33e7067 --- /dev/null +++ b/lib/schema/viewbuilder.js @@ -0,0 +1,81 @@ +const helpers = require('../util/helpers'); +const extend = require('lodash/extend'); + +class ViewBuilder { + constructor(client, method, viewName, fn) { + this.client = client; + this._method = method; + this._schemaName = undefined; + this._columns = undefined; + this._fn = fn; + this._viewName = viewName; + this._statements = []; + this._single = {}; + } + + setSchema(schemaName) { + this._schemaName = schemaName; + } + + columns(columns) { + this._columns = columns; + } + + as(selectQuery) { + this._selectQuery = selectQuery; + } + + checkOption() { + throw new Error( + 'check option definition is not supported by this dialect.' + ); + } + + localCheckOption() { + throw new Error( + 'check option definition is not supported by this dialect.' + ); + } + + cascadedCheckOption() { + throw new Error( + 'check option definition is not supported by this dialect.' + ); + } + + toSQL() { + if (this._method === 'alter') { + extend(this, AlterMethods); + } + this._fn.call(this, this); + return this.client.viewCompiler(this).toSQL(); + } +} + +const AlterMethods = { + column(column) { + const self = this; + return { + rename: function (newName) { + self._statements.push({ + grouping: 'alterView', + method: 'renameColumn', + args: [column, newName], + }); + return this; + }, + defaultTo: function (defaultValue) { + self._statements.push({ + grouping: 'alterView', + method: 'defaultTo', + args: [column, defaultValue], + }); + return this; + }, + }; + }, +}; + +helpers.addQueryContext(ViewBuilder); + +module.exports = ViewBuilder; diff --git a/lib/schema/viewcompiler.js b/lib/schema/viewcompiler.js new file mode 100644 index 00000000..25d68734 --- /dev/null +++ b/lib/schema/viewcompiler.js @@ -0,0 +1,135 @@ +/* eslint max-len:0 */ + +// View Compiler +// ------- +const { pushQuery } = require('./internal/helpers'); +const groupBy = require('lodash/groupBy'); +const { columnize: columnize_ } = require('../formatter/wrappingFormatter'); + +class ViewCompiler { + constructor(client, viewBuilder) { + this.client = client; + this.viewBuilder = viewBuilder; + this._commonBuilder = this.viewBuilder; + this.method = viewBuilder._method; + this.schemaNameRaw = viewBuilder._schemaName; + this.viewNameRaw = viewBuilder._viewName; + this.single = viewBuilder._single; + this.selectQuery = viewBuilder._selectQuery; + this.columns = viewBuilder._columns; + this.grouped = groupBy(viewBuilder._statements, 'grouping'); + + this.formatter = client.formatter(viewBuilder); + this.bindings = []; + this.formatter.bindings = this.bindings; + this.bindingsHolder = this; + + this.sequence = []; + } + + // Convert the tableCompiler toSQL + toSQL() { + this[this.method](); + return this.sequence; + } + + // Column Compilation + // ------- + + create() { + this.createQuery(this.columns, this.selectQuery); + } + + createOrReplace() { + throw new Error('replace views is not supported by this dialect.'); + } + + createMaterializedView() { + throw new Error('materialized views are not supported by this dialect.'); + } + + createQuery(columns, selectQuery, materialized, replace) { + const createStatement = + 'create ' + + (materialized ? 'materialized ' : '') + + 'view ' + + (replace ? 'or replace ' : ''); + const formatColumns = []; + for (const c of columns) { + formatColumns.push( + columnize_(c, this.viewBuilder, this.client, this.bindingsHolder) + ); + } + let sql = + createStatement + this.viewName() + ' (' + formatColumns.join(', ') + ')'; + sql += ' as '; + sql += selectQuery.toString(); + switch (this.single.checkOption) { + case 'default_option': + sql += ' with check option'; + break; + case 'local': + sql += ' with local check option'; + break; + case 'cascaded': + sql += ' with cascaded check option'; + break; + default: + break; + } + this.pushQuery({ + sql, + }); + } + + renameView(from, to) { + throw new Error( + 'rename view is not supported by this dialect (instead drop, then create another view).' + ); + } + + refreshMaterializedView() { + throw new Error('materialized views are not supported by this dialect.'); + } + + alter() { + this.alterView(); + } + + alterView() { + const alterView = this.grouped.alterView || []; + for (let i = 0, l = alterView.length; i < l; i++) { + const statement = alterView[i]; + if (this[statement.method]) { + this[statement.method].apply(this, statement.args); + } else { + this.client.logger.error(`Debug: ${statement.method} does not exist`); + } + } + for (const item in this.single) { + if (typeof this[item] === 'function') this[item](this.single[item]); + } + } + + renameColumn(from, to) { + throw new Error('rename column of views is not supported by this dialect.'); + } + + defaultTo(column, defaultValue) { + throw new Error( + 'change default values of views is not supported by this dialect.' + ); + } + + viewName() { + const name = this.schemaNameRaw + ? `${this.schemaNameRaw}.${this.viewNameRaw}` + : this.viewNameRaw; + + return this.formatter.wrap(name); + } +} + +ViewCompiler.prototype.pushQuery = pushQuery; + +module.exports = ViewCompiler; diff --git a/test/integration2/schema/views.spec.js b/test/integration2/schema/views.spec.js new file mode 100644 index 00000000..f563c3f8 --- /dev/null +++ b/test/integration2/schema/views.spec.js @@ -0,0 +1,275 @@ +'use strict'; + +const { expect } = require('chai'); + +require('lodash'); + +const { isOracle, isSQLite, isMssql } = require('../../util/db-helpers'); +const { getAllDbs, getKnexForDb } = require('../util/knex-instance-provider'); +const logger = require('../../integration/logger'); +const { isMysql, isCockroachDB } = require('../../util/db-helpers.js'); +const { assertNumber } = require('../../util/assertHelper'); + +describe('Views', () => { + getAllDbs().forEach((db) => { + describe(db, () => { + let knex; + + before(async () => { + knex = logger(getKnexForDb(db)); + }); + + after(async () => { + return knex.destroy(); + }); + + describe('view', () => { + beforeEach(async () => { + await knex.schema.dropViewIfExists('view_test'); + await knex.schema.dropTableIfExists('table_view'); + await knex.schema.createTable('table_view', (t) => { + t.string('a'); + t.integer('b'); + }); + await knex('table_view').insert([ + { a: 'test', b: 5 }, + { a: 'test2', b: 12 }, + { a: 'test3', b: 45 }, + ]); + }); + + afterEach(async () => { + await knex.schema.dropViewIfExists('view_test'); + await knex.schema.dropViewIfExists('new_view'); + if (!isMssql(knex) && !isSQLite(knex) && !isMysql(knex)) { + await knex.schema.dropMaterializedViewIfExists('mat_view'); + } + await knex.schema.dropTableIfExists('table_view'); + }); + + it('create view', async () => { + await knex.schema + .createView('view_test', function (view) { + view.columns(['a', 'b']); + view.as( + knex('table_view').select('a', 'b').where('b', '>', '10') + ); + }) + .testSql((tester) => { + tester( + ['pg', 'pg-redshift', 'cockroachdb', 'oracledb'], + [ + 'create view "view_test" ("a", "b") as select "a", "b" from "table_view" where "b" > \'10\'', + ] + ); + tester( + ['sqlite3', 'mysql'], + [ + "create view `view_test` (`a`, `b`) as select `a`, `b` from `table_view` where `b` > '10'", + ] + ); + tester('mssql', [ + "CREATE VIEW [view_test] (a, b) AS select [a], [b] from [table_view] where [b] > '10'", + ]); + }); + + // We test if the select on the view work and if results are good + await knex + .select(['a', 'b']) + .from('view_test') + .then(function (results) { + assertNumber(knex, results[0].b, 12); + assertNumber(knex, results[1].b, 45); + expect(results[0].a).to.be.equal('test2'); + expect(results[1].a).to.be.equal('test3'); + }); + }); + + it('create materialized view', async function () { + if (isMssql(knex) || isSQLite(knex) || isMysql(knex)) { + return this.skip(); + } + await knex.schema + .createMaterializedView('mat_view', function (view) { + view.columns(['a', 'b']); + view.as( + knex('table_view').select('a', 'b').where('b', '>', '10') + ); + }) + .testSql((tester) => { + tester( + ['pg', 'cockroachdb', 'pg-redshift', 'oracledb'], + [ + 'create materialized view "mat_view" ("a", "b") as select "a", "b" from "table_view" where "b" > \'10\'', + ] + ); + }); + + await knex + .select(['a', 'b']) + .from('mat_view') + .then(function (results) { + expect(results[0].a).to.be.equal('test2'); + expect(results[1].a).to.be.equal('test3'); + assertNumber(knex, results[0].b, 12); + assertNumber(knex, results[1].b, 45); + }); + + await knex('table_view').insert([{ a: 'test', b: 32 }]); + + // We test we have same values, because the view is not refreshed + await knex + .select(['a', 'b']) + .from('mat_view') + .then(function (results) { + expect(results[0].a).to.be.equal('test2'); + expect(results[1].a).to.be.equal('test3'); + assertNumber(knex, results[0].b, 12); + assertNumber(knex, results[1].b, 45); + }); + + await knex.schema.refreshMaterializedView('mat_view'); + + // Materialized view is refreshed + await knex + .select(['a', 'b']) + .from('mat_view') + .then(function (results) { + expect(results[0].a).to.be.equal('test2'); + expect(results[1].a).to.be.equal('test3'); + expect(results[2].a).to.be.equal('test'); + assertNumber(knex, results[0].b, 12); + assertNumber(knex, results[1].b, 45); + assertNumber(knex, results[2].b, 32); + }); + await knex.schema.dropMaterializedView('mat_view'); + }); + + it('alter column view', async function () { + if ( + isOracle(knex) || + isSQLite(knex) || + isMysql(knex) || + isCockroachDB(knex) + ) { + return this.skip(); + } + await knex.schema.createView('view_test', function (view) { + view.columns(['a', 'b']); + view.as(knex('table_view').select('a', 'b').where('b', '>', '10')); + }); + + await knex.schema.alterView('view_test', function (view) { + view.column('a').rename('new_a'); + }); + + await knex + .select(['new_a', 'b']) + .from('view_test') + .then(function (results) { + expect(results[0].new_a).to.be.equal('test2'); + expect(results[1].new_a).to.be.equal('test3'); + assertNumber(knex, results[0].b, 12); + assertNumber(knex, results[1].b, 45); + }); + }); + + it('alter view rename', async function () { + if (isOracle(knex) || isSQLite(knex)) { + return this.skip(); + } + await knex.schema.createView('view_test', function (view) { + view.columns(['a', 'b']); + view.as(knex('table_view').select('a', 'b').where('b', '>', '10')); + }); + + await knex.schema.renameView('view_test', 'new_view'); + + await knex + .select(['a', 'b']) + .from('new_view') + .then(function (results) { + expect(results[0].a).to.be.equal('test2'); + expect(results[1].a).to.be.equal('test3'); + assertNumber(knex, results[0].b, 12); + assertNumber(knex, results[1].b, 45); + }); + await knex.schema.dropView('new_view'); + }); + + it('create view with check options', async function () { + if (isMssql(knex) || isCockroachDB(knex) || isSQLite(knex)) { + return this.skip(); + } + + await knex.schema + .createView('view_test', function (view) { + view.columns(['a', 'b']); + view.as( + knex('table_view').select('a', 'b').where('b', '>', '10') + ); + view.checkOption(); + }) + .testSql((tester) => { + tester( + ['oracledb'], + [ + 'create view "view_test" ("a", "b") as select "a", "b" from "table_view" where "b" > \'10\' with check option', + ] + ); + }); + + if (isOracle(knex)) { + return this.skip(); + } + await knex.schema.dropView('view_test'); + await knex.schema + .createView('view_test', function (view) { + view.columns(['a', 'b']); + view.as( + knex('table_view').select('a', 'b').where('b', '>', '10') + ); + view.localCheckOption(); + }) + .testSql((tester) => { + tester( + ['pg', 'cockroachdb', 'pg-redshift'], + [ + 'create view "view_test" ("a", "b") as select "a", "b" from "table_view" where "b" > \'10\' with local check option', + ] + ); + tester( + ['mysql'], + [ + "create view `view_test` (`a`, `b`) as select `a`, `b` from `table_view` where `b` > '10' with local check option", + ] + ); + }); + await knex.schema.dropView('view_test'); + await knex.schema + .createView('view_test', function (view) { + view.columns(['a', 'b']); + view.as( + knex('table_view').select('a', 'b').where('b', '>', '10') + ); + view.cascadedCheckOption(); + }) + .testSql((tester) => { + tester( + ['pg', 'cockroachdb', 'pg-redshift'], + [ + 'create view "view_test" ("a", "b") as select "a", "b" from "table_view" where "b" > \'10\' with cascaded check option', + ] + ); + tester( + ['mysql'], + [ + "create view `view_test` (`a`, `b`) as select `a`, `b` from `table_view` where `b` > '10' with cascaded check option", + ] + ); + }); + }); + }); + }); + }); +}); diff --git a/test/unit/schema-builder/mssql.js b/test/unit/schema-builder/mssql.js index f0ce6ae2..40b75dc9 100644 --- a/test/unit/schema-builder/mssql.js +++ b/test/unit/schema-builder/mssql.js @@ -3,6 +3,7 @@ const expect = require('chai').expect; const sinon = require('sinon'); const MSSQL_Client = require('../../../lib/dialects/mssql'); +const knex = require('../../../knex'); const client = new MSSQL_Client({ client: 'mssql' }); describe('MSSQL SchemaBuilder', function () { @@ -49,6 +50,119 @@ describe('MSSQL SchemaBuilder', function () { ); }); + describe('views', function () { + let knexMssql; + + before(function () { + knexMssql = knex({ + client: 'mssql', + connection: {}, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexMssql('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "CREATE VIEW [adults] (name) AS select [name] from [users] where [age] > '18'" + ); + }); + + it('create view or replace', async function () { + const viewSql = client + .schemaBuilder() + .createViewOrReplace('adults', function (view) { + view.columns(['name']); + view.as(knexMssql('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "CREATE OR ALTER VIEW [adults] (name) AS select [name] from [users] where [age] > '18'" + ); + }); + + it('create view with check options', async function () { + expect(() => { + client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexMssql('users').select('name').where('age', '>', '18')); + view.localCheckOption(); + }) + .toSQL(); + }).to.throw('check option definition is not supported by this dialect.'); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view [users]'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view [myschema].[users]'); + }); + + it('rename column of view', function () { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName'); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal("exec sp_rename ?, ?, 'COLUMN'"); + expect(tableSql[0].bindings[0]).to.equal('[users].oldName'); + expect(tableSql[0].bindings[1]).to.equal('newName'); + }); + + it('rename view', function () { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('exec sp_rename ?, ?'); + expect(tableSql[0].bindings[0]).to.equal('old_view'); + expect(tableSql[0].bindings[1]).to.equal('new_view'); + }); + + it('create materialized view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as(knexMssql('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + + it('refresh view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + }); + it('test basic create table with incrementing without primary key', function () { tableSql = client.schemaBuilder().createTable('users', function (table) { table.increments('id', { primaryKey: false }); diff --git a/test/unit/schema-builder/mysql.js b/test/unit/schema-builder/mysql.js index 0d2a6d35..46957248 100644 --- a/test/unit/schema-builder/mysql.js +++ b/test/unit/schema-builder/mysql.js @@ -3,6 +3,7 @@ const { expect } = require('chai'); const sinon = require('sinon'); const MySQL_Client = require('../../../lib/dialects/mysql'); const MySQL2_Client = require('../../../lib/dialects/mysql2'); +const knex = require('../../../knex'); module.exports = function (dialect) { describe(dialect + ' SchemaBuilder', function () { @@ -92,6 +93,134 @@ module.exports = function (dialect) { ); }); + describe('views', function () { + let knexMysql; + + before(function () { + knexMysql = knex({ + client: 'mysql2', + connection: {}, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexMysql('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "create view `adults` (`name`) as select `name` from `users` where `age` > '18'" + ); + }); + + it('create view or replace', async function () { + const viewSql = client + .schemaBuilder() + .createViewOrReplace('adults', function (view) { + view.columns(['name']); + view.as(knexMysql('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "create view or replace `adults` (`name`) as select `name` from `users` where `age` > '18'" + ); + }); + + it('create view with check options', async function () { + const viewSqlLocalCheck = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexMysql('users').select('name').where('age', '>', '18')); + view.localCheckOption(); + }) + .toSQL(); + equal(1, viewSqlLocalCheck.length); + expect(viewSqlLocalCheck[0].sql).to.equal( + "create view `adults` (`name`) as select `name` from `users` where `age` > '18' with local check option" + ); + + const viewSqlCascadedCheck = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexMysql('users').select('name').where('age', '>', '18')); + view.cascadedCheckOption(); + }) + .toSQL(); + equal(1, viewSqlCascadedCheck.length); + expect(viewSqlCascadedCheck[0].sql).to.equal( + "create view `adults` (`name`) as select `name` from `users` where `age` > '18' with cascaded check option" + ); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view `users`'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view `myschema`.`users`'); + }); + + it('rename and change default of column of view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + }).to.throw('rename column of views is not supported by this dialect.'); + }); + + it('rename view', function () { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'rename table `old_view` to `new_view`' + ); + }); + + it('create materialized view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as( + knexMysql('users').select('name').where('age', '>', '18') + ); + }) + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + + it('refresh view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + }); + it('adding json', function () { tableSql = client .schemaBuilder() diff --git a/test/unit/schema-builder/oracledb.js b/test/unit/schema-builder/oracledb.js index 13201863..1cbde61d 100644 --- a/test/unit/schema-builder/oracledb.js +++ b/test/unit/schema-builder/oracledb.js @@ -4,6 +4,7 @@ const { expect } = require('chai'); const sinon = require('sinon'); const Oracle_Client = require('../../../lib/dialects/oracledb'); +const knex = require('../../../knex'); const client = new Oracle_Client({ client: 'oracledb' }); describe('OracleDb SchemaBuilder', function () { @@ -34,6 +35,136 @@ describe('OracleDb SchemaBuilder', function () { ); }); + describe('views', function () { + let knexOracleDb; + + before(function () { + knexOracleDb = knex({ + client: 'oracledb', + connection: {}, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexOracleDb('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('create view or replace', async function () { + const viewSql = client + .schemaBuilder() + .createViewOrReplace('adults', function (view) { + view.columns(['name']); + view.as(knexOracleDb('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + 'create view or replace "adults" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('create view with check options', async function () { + tableSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexOracleDb('users').select('name').where('age', '>', '18')); + view.checkOption(); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\' with check option' + ); + + expect(() => { + client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as( + knexOracleDb('users').select('name').where('age', '>', '18') + ); + view.cascadedCheckOption(); + }) + .toSQL(); + }).to.throw('check option definition is not supported by this dialect.'); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view "users"'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view "myschema"."users"'); + }); + + it('rename and change default of column of view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + }).to.throw('rename column of views is not supported by this dialect.'); + }); + + it('rename view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + }).to.throw( + 'rename view is not supported by this dialect (instead drop then create another view).' + ); + }); + + it('create materialized view', function () { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as(knexOracleDb('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'create materialized view "mat_view" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('refresh view', function () { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + "BEGIN DBMS_MVIEW.REFRESH('view_to_refresh'); END;" + ); + }); + }); + it('test basic create table if not exists', function () { tableSql = client .schemaBuilder() diff --git a/test/unit/schema-builder/postgres.js b/test/unit/schema-builder/postgres.js index e3751ff3..bb781b62 100644 --- a/test/unit/schema-builder/postgres.js +++ b/test/unit/schema-builder/postgres.js @@ -193,6 +193,144 @@ describe('PostgreSQL SchemaBuilder', function () { expect(tableSql[0].sql).to.equal('drop table "users"'); }); + describe('views', function () { + let knexPg; + + before(function () { + knexPg = knex({ + client: 'postgresql', + version: '10.5', + connection: { + pool: {}, + }, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexPg('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('create view or replace', async function () { + const viewSql = client + .schemaBuilder() + .createViewOrReplace('adults', function (view) { + view.columns(['name']); + view.as(knexPg('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + 'create view or replace "adults" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('create view with check options', async function () { + const viewSqlLocalCheck = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexPg('users').select('name').where('age', '>', '18')); + view.localCheckOption(); + }) + .toSQL(); + equal(1, viewSqlLocalCheck.length); + expect(viewSqlLocalCheck[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\' with local check option' + ); + + const viewSqlCascadedCheck = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexPg('users').select('name').where('age', '>', '18')); + view.cascadedCheckOption(); + }) + .toSQL(); + equal(1, viewSqlCascadedCheck.length); + expect(viewSqlCascadedCheck[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\' with cascaded check option' + ); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view "users"'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view "myschema"."users"'); + }); + + it('rename and change default of column of view', function () { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + equal(2, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'alter view "users" rename "oldName" to "newName"' + ); + expect(tableSql[1].sql).to.equal( + 'alter view "users" alter "oldName" set default 10' + ); + }); + + it('rename view', function () { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'alter view "old_view" rename to "new_view"' + ); + }); + + it('create materialized view', function () { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as(knexPg('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'create materialized view "mat_view" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('refresh view', function () { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'refresh materialized view "view_to_refresh"' + ); + }); + }); + it('drop table with schema', function () { tableSql = client .schemaBuilder() diff --git a/test/unit/schema-builder/redshift.js b/test/unit/schema-builder/redshift.js index 50c68dc7..1beb76d8 100644 --- a/test/unit/schema-builder/redshift.js +++ b/test/unit/schema-builder/redshift.js @@ -6,6 +6,7 @@ const { expect } = require('chai'); let tableSql; const Redshift_Client = require('../../../lib/dialects/redshift'); +const knex = require('../../../knex'); const client = new Redshift_Client({ client: 'redshift' }); const equal = require('assert').equal; @@ -54,6 +55,141 @@ describe('Redshift SchemaBuilder', function () { ); }); + describe('views', function () { + let knexRedShift; + + before(function () { + knexRedShift = knex({ + client: 'redshift', + connection: {}, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexRedShift('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('create view or replace', async function () { + const viewSql = client + .schemaBuilder() + .createViewOrReplace('adults', function (view) { + view.columns(['name']); + view.as(knexRedShift('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + 'create view or replace "adults" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('create view with check options', async function () { + const viewSqlLocalCheck = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexRedShift('users').select('name').where('age', '>', '18')); + view.localCheckOption(); + }) + .toSQL(); + equal(1, viewSqlLocalCheck.length); + expect(viewSqlLocalCheck[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\' with local check option' + ); + + const viewSqlCascadedCheck = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexRedShift('users').select('name').where('age', '>', '18')); + view.cascadedCheckOption(); + }) + .toSQL(); + equal(1, viewSqlCascadedCheck.length); + expect(viewSqlCascadedCheck[0].sql).to.equal( + 'create view "adults" ("name") as select "name" from "users" where "age" > \'18\' with cascaded check option' + ); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view "users"'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view "myschema"."users"'); + }); + + it('rename and change default of column of view', function () { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + equal(2, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'alter view "users" rename "oldName" to "newName"' + ); + expect(tableSql[1].sql).to.equal( + 'alter view "users" alter "oldName" set default 10' + ); + }); + + it('rename view', function () { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'alter view "old_view" rename to "new_view"' + ); + }); + + it('create materialized view', function () { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as(knexRedShift('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'create materialized view "mat_view" ("name") as select "name" from "users" where "age" > \'18\'' + ); + }); + + it('refresh view', function () { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal( + 'refresh materialized view "view_to_refresh"' + ); + }); + }); + it('alter table with schema', function () { tableSql = client .schemaBuilder() diff --git a/test/unit/schema-builder/sqlite3.js b/test/unit/schema-builder/sqlite3.js index c8daa64f..d50a79c0 100644 --- a/test/unit/schema-builder/sqlite3.js +++ b/test/unit/schema-builder/sqlite3.js @@ -18,6 +18,7 @@ const { const _ = require('lodash'); const { equal, deepEqual } = require('assert'); +const knex = require('../../../knex'); describe('SQLite SchemaBuilder', function () { it('basic create table', function () { @@ -49,6 +50,120 @@ describe('SQLite SchemaBuilder', function () { ); }); + describe('views', function () { + let knexSqlite3; + + before(function () { + knexSqlite3 = knex({ + client: 'sqlite3', + connection: { + filename: '', + }, + }); + }); + + it('basic create view', async function () { + const viewSql = client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as(knexSqlite3('users').select('name').where('age', '>', '18')); + }) + .toSQL(); + equal(1, viewSql.length); + expect(viewSql[0].sql).to.equal( + "create view `adults` (`name`) as select `name` from `users` where `age` > '18'" + ); + }); + + it('create view or replace', async function () { + expect(() => { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + }).to.throw('rename column of views is not supported by this dialect.'); + }); + + it('create view with check options', async function () { + expect(() => { + client + .schemaBuilder() + .createView('adults', function (view) { + view.columns(['name']); + view.as( + knexSqlite3('users').select('name').where('age', '>', '18') + ); + view.localCheckOption(); + }) + .toSQL(); + }).to.throw('check option definition is not supported by this dialect.'); + }); + + it('drop view', function () { + tableSql = client.schemaBuilder().dropView('users').toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view `users`'); + }); + + it('drop view with schema', function () { + tableSql = client + .schemaBuilder() + .withSchema('myschema') + .dropView('users') + .toSQL(); + equal(1, tableSql.length); + expect(tableSql[0].sql).to.equal('drop view `myschema`.`users`'); + }); + + it('rename and change default of column of view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .view('users', function (view) { + view.column('oldName').rename('newName').defaultTo('10'); + }) + .toSQL(); + }).to.throw('rename column of views is not supported by this dialect.'); + }); + + it('rename view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .renameView('old_view', 'new_view') + .toSQL(); + }).to.throw( + 'rename view is not supported by this dialect (instead drop then create another view).' + ); + }); + + it('create materialized view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .createMaterializedView('mat_view', function (view) { + view.columns(['name']); + view.as( + knexSqlite3('users').select('name').where('age', '>', '18') + ); + }) + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + + it('refresh view', function () { + expect(() => { + tableSql = client + .schemaBuilder() + .refreshMaterializedView('view_to_refresh') + .toSQL(); + }).to.throw('materialized views are not supported by this dialect.'); + }); + }); + it('create json table', function () { tableSql = client .schemaBuilder() diff --git a/types/index.d.ts b/types/index.d.ts index e413253d..6ea33b2f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1833,6 +1833,35 @@ export declare namespace Knex { // interface SchemaBuilder extends ChainableInterface { + // Views + createView( + viewName: string, + callback: (viewBuilder: ViewBuilder) => any + ): SchemaBuilder; + createViewOrReplace( + viewName: string, + callback: (viewBuilder: ViewBuilder) => any + ): SchemaBuilder; + createMaterializedView( + viewName: string, + callback: (viewBuilder: ViewBuilder) => any + ): SchemaBuilder; + refreshMaterializedView(viewName: string): SchemaBuilder; + dropView(viewName: string): SchemaBuilder; + dropViewIfExists(viewName: string): SchemaBuilder; + dropMaterializedView(viewName: string): SchemaBuilder; + dropMaterializedViewIfExists(viewName: string): SchemaBuilder; + renameView(oldViewName: string, newViewName: string): Promise; + view( + viewName: string, + callback: (viewBuilder: AlterViewBuilder) => any + ): Promise; + alterView( + viewName: string, + callback: (tableBuilder: AlterViewBuilder) => any + ): SchemaBuilder; + + // Tables createTable( tableName: string, callback: (tableBuilder: CreateTableBuilder) => any @@ -1846,8 +1875,6 @@ export declare namespace Knex { tableNameLike: string, callback: (tableBuilder: CreateTableBuilder) => any ): SchemaBuilder; - createSchema(schemaName: string): SchemaBuilder; - createSchemaIfNotExists(schemaName: string): SchemaBuilder; alterTable( tableName: string, callback: (tableBuilder: CreateTableBuilder) => any @@ -1855,16 +1882,22 @@ export declare namespace Knex { renameTable(oldTableName: string, newTableName: string): Promise; dropTable(tableName: string): SchemaBuilder; hasTable(tableName: string): Promise; - hasColumn(tableName: string, columnName: string): Promise; table( tableName: string, callback: (tableBuilder: AlterTableBuilder) => any ): Promise; dropTableIfExists(tableName: string): SchemaBuilder; + + // Schema + createSchema(schemaName: string): SchemaBuilder; + createSchemaIfNotExists(schemaName: string): SchemaBuilder; dropSchema(schemaName: string, cascade?: boolean): SchemaBuilder; dropSchemaIfExists(schemaName: string, cascade?: boolean): SchemaBuilder; - raw(statement: string): SchemaBuilder; withSchema(schemaName: string): SchemaBuilder; + + // Others + hasColumn(tableName: string, columnName: string): Promise; + raw(statement: string): SchemaBuilder; queryContext(context: any): SchemaBuilder; toString(): string; toSQL(): Sql; @@ -1956,6 +1989,15 @@ export declare namespace Knex { queryContext(context: any): TableBuilder; } + interface ViewBuilder { + columns(columns: any): ViewBuilder; + as(selectQuery: QueryBuilder): ViewBuilder; + checkOption(): Promise; + localCheckOption(): Promise; + cascadedCheckOption(): Promise; + queryContext(context: any): ViewBuilder; + } + interface CreateTableBuilder extends TableBuilder { engine(val: string): CreateTableBuilder; charset(val: string): CreateTableBuilder; @@ -1964,6 +2006,16 @@ export declare namespace Knex { } interface AlterTableBuilder extends TableBuilder {} + + interface AlterColumnView extends ViewBuilder { + rename(newName: string): AlterColumnView; + defaultTo(defaultValue: string): AlterColumnView; + } + + interface AlterViewBuilder extends ViewBuilder { + column(column: string): AlterColumnView; + } + type deferrableType = 'not deferrable' | 'immediate' | 'deferred'; interface ColumnBuilder { index(indexName?: string): ColumnBuilder;