Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of MATERIALIZED and NOT MATERIALIZED with WITH/CTE #4940

Merged
merged 4 commits into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/dialects/postgres/query/pg-querybuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,34 @@ module.exports = class QueryBuilder_PostgreSQL extends QueryBuilder {
this._single.using = tables;
return this;
}

withMaterialized(alias, statementOrColumnList, nothingOrStatement) {
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(
alias,
statementOrColumnList,
nothingOrStatement,
true
);
}

withNotMaterialized(alias, statementOrColumnList, nothingOrStatement) {
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(
alias,
statementOrColumnList,
nothingOrStatement,
false
);
}
};
5 changes: 5 additions & 0 deletions lib/dialects/sqlite3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const TableCompiler = require('./schema/sqlite-tablecompiler');
const ViewCompiler = require('./schema/sqlite-viewcompiler');
const SQLite3_DDL = require('./schema/ddl');
const Formatter = require('../../formatter');
const QueryBuilder = require('./query/sqlite-querybuilder');

class Client_SQLite3 extends Client {
constructor(config) {
Expand Down Expand Up @@ -44,6 +45,10 @@ class Client_SQLite3 extends Client {
return new SqliteQueryCompiler(this, builder, formatter);
}

queryBuilder() {
return new QueryBuilder(this);
}

viewCompiler(builder, formatter) {
return new ViewCompiler(this, builder, formatter);
}
Expand Down
33 changes: 33 additions & 0 deletions lib/dialects/sqlite3/query/sqlite-querybuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const QueryBuilder = require('../../../query/querybuilder.js');

module.exports = class QueryBuilder_SQLite3 extends QueryBuilder {
withMaterialized(alias, statementOrColumnList, nothingOrStatement) {
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(
alias,
statementOrColumnList,
nothingOrStatement,
true
);
}

withNotMaterialized(alias, statementOrColumnList, nothingOrStatement) {
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(
alias,
statementOrColumnList,
nothingOrStatement,
false
);
}
};
110 changes: 62 additions & 48 deletions lib/query/querybuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,33 +118,90 @@ class Builder extends EventEmitter {

// With
// ------
isValidStatementArg(statement) {
return (
typeof statement === 'function' ||
statement instanceof Builder ||
(statement && statement.isRawInstance)
);
}

_validateWithArgs(alias, statementOrColumnList, nothingOrStatement, method) {
const [query, columnList] =
typeof nothingOrStatement === 'undefined'
? [statementOrColumnList, undefined]
: [nothingOrStatement, statementOrColumnList];
if (typeof alias !== 'string') {
throw new Error(`${method}() first argument must be a string`);
}

if (this.isValidStatementArg(query) && typeof columnList === 'undefined') {
// Validated as two-arg variant (alias, statement).
return;
}

// Attempt to interpret as three-arg variant (alias, columnList, statement).
const isNonEmptyNameList =
Array.isArray(columnList) &&
columnList.length > 0 &&
columnList.every((it) => typeof it === 'string');
if (!isNonEmptyNameList) {
throw new Error(
`${method}() second argument must be a statement or non-empty column name list.`
);
}

if (this.isValidStatementArg(query)) {
return;
}
throw new Error(
`${method}() third argument must be a function / QueryBuilder or a raw when its second argument is a column name list`
);
}

with(alias, statementOrColumnList, nothingOrStatement) {
validateWithArgs(alias, statementOrColumnList, nothingOrStatement, 'with');
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(alias, statementOrColumnList, nothingOrStatement);
}

withMaterialized(alias, statementOrColumnList, nothingOrStatement) {
throw new Error('With materialized is not supported by this dialect');
}

withNotMaterialized(alias, statementOrColumnList, nothingOrStatement) {
throw new Error('With materialized is not supported by this dialect');
}

// Helper for compiling any advanced `with` queries.
withWrapped(alias, statementOrColumnList, nothingOrStatement) {
withWrapped(alias, statementOrColumnList, nothingOrStatement, materialized) {
const [query, columnList] =
typeof nothingOrStatement === 'undefined'
? [statementOrColumnList, undefined]
: [nothingOrStatement, statementOrColumnList];
this._statements.push({
const statement = {
grouping: 'with',
type: 'withWrapped',
alias: alias,
columnList,
value: query,
});
};
if (materialized !== undefined) {
statement.materialized = materialized;
}
this._statements.push(statement);
return this;
}

// With Recursive
// ------

withRecursive(alias, statementOrColumnList, nothingOrStatement) {
validateWithArgs(
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
Expand Down Expand Up @@ -1656,49 +1713,6 @@ class Builder extends EventEmitter {
}
}

const isValidStatementArg = (statement) =>
typeof statement === 'function' ||
statement instanceof Builder ||
(statement && statement.isRawInstance);

const validateWithArgs = function (
alias,
statementOrColumnList,
nothingOrStatement,
method
) {
const [query, columnList] =
typeof nothingOrStatement === 'undefined'
? [statementOrColumnList, undefined]
: [nothingOrStatement, statementOrColumnList];
if (typeof alias !== 'string') {
throw new Error(`${method}() first argument must be a string`);
}

if (isValidStatementArg(query) && typeof columnList === 'undefined') {
// Validated as two-arg variant (alias, statement).
return;
}

// Attempt to interpret as three-arg variant (alias, columnList, statement).
const isNonEmptyNameList =
Array.isArray(columnList) &&
columnList.length > 0 &&
columnList.every((it) => typeof it === 'string');
if (!isNonEmptyNameList) {
throw new Error(
`${method}() second argument must be a statement or non-empty column name list.`
);
}

if (isValidStatementArg(query)) {
return;
}
throw new Error(
`${method}() third argument must be a function / QueryBuilder or a raw when its second argument is a column name list`
);
};

Builder.prototype.select = Builder.prototype.columns;
Builder.prototype.column = Builder.prototype.columns;
Builder.prototype.andWhereNot = Builder.prototype.whereNot;
Expand Down
10 changes: 9 additions & 1 deletion lib/query/querycompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,12 @@ class QueryCompiler {
) +
')'
: '';
const materialized =
statement.materialized === undefined
? ''
: statement.materialized
? 'materialized '
: 'not materialized ';
return (
(val &&
columnize_(
Expand All @@ -1179,7 +1185,9 @@ class QueryCompiler {
this.bindingsHolder
) +
columnList +
' as (' +
' as ' +
materialized +
'(' +
val +
')') ||
''
Expand Down
28 changes: 28 additions & 0 deletions test/integration2/query/select/selects.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,34 @@ describe('Selects', function () {
);
});

describe('with (not) materialized tests', () => {
before(async function () {
if (!isPostgreSQL(knex) && !isSQLite(knex)) {
return this.skip();
}
await knex('test_default_table').truncate();
await knex('test_default_table').insert([
{ string: 'something', tinyint: 1 },
]);
});

it('with materialized', async function () {
const materialized = await knex('t')
.withMaterialized('t', knex('test_default_table'))
.from('t')
.first();
expect(materialized.tinyint).to.equal(1);
});

it('with not materialized', async function () {
const notMaterialized = await knex('t')
.withNotMaterialized('t', knex('test_default_table'))
.from('t')
.first();
expect(notMaterialized.tinyint).to.equal(1);
});
});

describe('json selections', () => {
before(async () => {
await knex.schema.dropTableIfExists('cities');
Expand Down
4 changes: 3 additions & 1 deletion test/unit/knex.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,9 @@ describe('knex', () => {
await knex('some_nonexisten_table')
.select()
.catch((err) => {
expect(err.stack.split('\n')[1]).to.match(/at createQueryBuilder \(/); // the index 1 might need adjustment if the code is refactored
expect(err.stack.split('\n')[1]).to.match(
/at Object.queryBuilder \(/
); // the index 1 might need adjustment if the code is refactored
expect(typeof err.originalStack).to.equal('string');
});

Expand Down
34 changes: 34 additions & 0 deletions test/unit/query/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -9482,6 +9482,40 @@ describe('QueryBuilder', () => {
);
});

it("wrapped 'withMaterialized' clause update", () => {
testsql(
qb()
.withMaterialized('withClause', function () {
this.select('foo').from('users');
})
.update({ foo: 'updatedFoo' })
.where('email', '=', 'foo')
.from('users'),
{
sqlite3:
'with `withClause` as materialized (select `foo` from `users`) update `users` set `foo` = ? where `email` = ?',
pg: 'with "withClause" as materialized (select "foo" from "users") update "users" set "foo" = ? where "email" = ?',
}
);
});

it("wrapped 'withNotMaterialized' clause update", () => {
testsql(
qb()
.withNotMaterialized('withClause', function () {
this.select('foo').from('users');
})
.update({ foo: 'updatedFoo' })
.where('email', '=', 'foo')
.from('users'),
{
sqlite3:
'with `withClause` as not materialized (select `foo` from `users`) update `users` set `foo` = ? where `email` = ?',
pg: 'with "withClause" as not materialized (select "foo" from "users") update "users" set "foo" = ? where "email" = ?',
}
);
});

it("wrapped 'with' clause delete", () => {
testsql(
qb()
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@ export declare namespace Knex {

// Withs
with: With<TRecord, TResult>;
withMaterialized: With<TRecord, TResult>;
withNotMaterialized: With<TRecord, TResult>;
withRecursive: With<TRecord, TResult>;
withRaw: WithRaw<TRecord, TResult>;
withSchema: WithSchema<TRecord, TResult>;
Expand Down