diff --git a/lib/dialect/dialect.ts b/lib/dialect/dialect.ts index 1ea4886..d867b9f 100644 --- a/lib/dialect/dialect.ts +++ b/lib/dialect/dialect.ts @@ -6,6 +6,7 @@ import { AddColumnNode } from '../node/addColumn.js'; import { AliasNode } from '../node/alias.js'; import { AlterNode } from '../node/alter.js'; import { ArrayCallNode } from '../node/arrayCall.js'; +import { AsOfNode } from '../node/asOf.js'; import { AtNode } from '../node/at.js'; import { BinaryNode } from '../node/binary.js'; import { CascadeNode } from '../node/cascade.js'; @@ -208,6 +209,8 @@ export abstract class Dialect { return this.visitCast(node as CastNode); case 'FROM': return this.visitFrom(node as FromNode); + case 'AS OF': + return this.visitAsOf(node as AsOfNode); case 'WHERE': return this.visitWhere(node as WhereNode); case 'ORDER BY': @@ -446,6 +449,10 @@ export abstract class Dialect { } return result; } + public visitAsOf(asOfNode: AsOfNode): string[] { + const result = ['AS OF SYSTEM TIME', ...asOfNode.nodes.flatMap(this.visit.bind(this))]; + return result; + } public visitWhere(whereNode: WhereNode): string[] { this.visitingWhere = true; const result = ['WHERE', whereNode.nodes.map(this.visit.bind(this)).join(', ')]; diff --git a/lib/dialect/mssql.ts b/lib/dialect/mssql.ts index 8d497b6..5e4cdf9 100644 --- a/lib/dialect/mssql.ts +++ b/lib/dialect/mssql.ts @@ -4,6 +4,7 @@ import assert from 'assert'; import { AlterNode } from '../node/alter.js'; +import { AsOfNode } from '../node/asOf.js'; import { BinaryNode } from '../node/binary.js'; import { CaseNode } from '../node/case.js'; import { ColumnNode } from '../node/column.js'; @@ -47,6 +48,9 @@ export class Mssql extends Dialect<{ questionMarkParameterPlaceholder?: boolean } return `@${index}`; } + public visitAsOf(asOfNode: AsOfNode): string[] { + throw new Error('Mssql does not support AS OF.'); + } public visitReplace(replaceNode: ReplaceNode): string[] { throw new Error('Mssql does not support REPLACE.'); } diff --git a/lib/dialect/mysql.ts b/lib/dialect/mysql.ts index da5850e..71ed235 100644 --- a/lib/dialect/mysql.ts +++ b/lib/dialect/mysql.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import isNumber from 'lodash/isNumber.js'; +import { AsOfNode } from '../node/asOf.js'; import { BinaryNode } from '../node/binary.js'; import { ColumnNode } from '../node/column.js'; import { CreateNode } from '../node/create.js'; @@ -67,6 +68,9 @@ export class Mysql extends Dialect { } return result; } + public visitAsOf(asOfNode: AsOfNode): string[] { + throw new Error('Mysql does not support AS OF.'); + } public visitOnDuplicate(onDuplicateNode: OnDuplicateNode): string[] { const params: string[] = []; /* jshint boss: true */ diff --git a/lib/dialect/oracle.ts b/lib/dialect/oracle.ts index e768a30..cd3d6c5 100644 --- a/lib/dialect/oracle.ts +++ b/lib/dialect/oracle.ts @@ -2,6 +2,7 @@ import assert from 'assert'; import { AliasNode } from '../node/alias.js'; import { AlterNode } from '../node/alter.js'; +import { AsOfNode } from '../node/asOf.js'; import { BinaryNode } from '../node/binary.js'; import { CascadeNode } from '../node/cascade.js'; import { CaseNode } from '../node/case.js'; @@ -95,6 +96,10 @@ export class Oracle extends Dialect { } return super.visitAlter(alterNode); } + public visitAsOf(asOfNode: AsOfNode): string[] { + const result = ['AS OF TIMESTAMP', ...asOfNode.nodes.flatMap(this.visit.bind(this))]; + return result; + } public visitTable(tableNode: TableNode): string[] { const table = tableNode.table; let txt = ''; diff --git a/lib/dialect/sqlite.ts b/lib/dialect/sqlite.ts index 27cd0dc..37e063d 100644 --- a/lib/dialect/sqlite.ts +++ b/lib/dialect/sqlite.ts @@ -2,6 +2,7 @@ import assert from 'assert'; import isArray from 'lodash/isArray.js'; import { AddColumnNode } from '../node/addColumn.js'; +import { AsOfNode } from '../node/asOf.js'; import { BinaryNode } from '../node/binary.js'; import { CascadeNode } from '../node/cascade.js'; import { CreateIndexNode } from '../node/createIndex.js'; @@ -48,6 +49,9 @@ export class Sqlite extends Dialect<{ dateTimeMillis?: boolean }> { } return value; } + public visitAsOf(asOfNode: AsOfNode): string[] { + throw new Error('SQLite does not support AS OF.'); + } public visitReplace(replaceNode: ReplaceNode): string[] { // don't use table.column for replaces this.visitedReplace = true; diff --git a/lib/functions.ts b/lib/functions.ts index 714598d..44ac2f4 100644 --- a/lib/functions.ts +++ b/lib/functions.ts @@ -74,13 +74,22 @@ const jsonbFunctions = [ 'JSONB_AGG' ] as const; +// time travel timestamp functions available to cockroachdb +const timeTravelFunctions = [ + 'STATEMENT_TIMESTAMP', + 'FOLLOWER_READ_TIMESTAMP', + 'WITH_MIN_TIMESTAMP', + 'WITH_MAX_STALENESS' +] as const; + const standardFunctionNames = [ ...aggregateFunctions, ...scalarFunctions, ...hstoreFunctions, ...textsearchFunctions, ...dateFunctions, - ...jsonbFunctions + ...jsonbFunctions, + ...timeTravelFunctions ] as const; type StandardFunctions = { diff --git a/lib/node/asOf.ts b/lib/node/asOf.ts new file mode 100644 index 0000000..685b464 --- /dev/null +++ b/lib/node/asOf.ts @@ -0,0 +1,7 @@ +import { Node } from './node.js'; + +export class AsOfNode extends Node { + constructor() { + super('AS OF'); + } +} diff --git a/lib/node/query.ts b/lib/node/query.ts index bd59604..18cafe0 100644 --- a/lib/node/query.ts +++ b/lib/node/query.ts @@ -49,6 +49,7 @@ import { TruncateNode } from './truncate.js'; import { UpdateNode } from './update.js'; import { ValueExpressionBaseNode } from './_internal.js'; import { WhereNode } from './where.js'; +import { AsOfNode } from './asOf.js'; // get the first element of an arguments if it is an array, else return arguments as an array const getArrayOrArgsAsArray = (args: (T | T[])[]): T[] => { @@ -152,6 +153,11 @@ export class Query extends ValueExpressionBaseNode { return this; } + public asOf(node: INodeable | string): this { + this.add(new AsOfNode().add(nodeableOrTextNode(node))); + return this; + } + public leftJoin(other: INodeable): JoinNode { assert(this.type === 'SUBQUERY', 'leftJoin() can only be used on a subQuery'); return new JoinNode('LEFT', this, other.toNode()); diff --git a/test/dialects/as-of-tests.ts b/test/dialects/as-of-tests.ts new file mode 100644 index 0000000..7f40965 --- /dev/null +++ b/test/dialects/as-of-tests.ts @@ -0,0 +1,32 @@ +import * as Harness from './support.js'; +import { Sql } from '../../dist/lib.js'; +const post = Harness.definePostTable(); +const user = Harness.defineUserTable(); +const instance = new Sql('postgres'); + +Harness.test({ + query: user + .select(user.star()) + .from(user) + .asOf(instance.functions.FOLLOWER_READ_TIMESTAMP()), + pg: { + text: 'SELECT "user".* FROM "user" AS OF SYSTEM TIME FOLLOWER_READ_TIMESTAMP()', + string: 'SELECT "user".* FROM "user" AS OF SYSTEM TIME FOLLOWER_READ_TIMESTAMP()' + }, + oracle: { + text: 'SELECT "user".* FROM "user" AS OF TIMESTAMP FOLLOWER_READ_TIMESTAMP()', + string: 'SELECT "user".* FROM "user" AS OF TIMESTAMP FOLLOWER_READ_TIMESTAMP()' + } +}); + +Harness.test({ + query: user.select(user.star()).from([user, post]).asOf('\'-10s\''), + pg: { + text: 'SELECT "user".* FROM "user" , "post" AS OF SYSTEM TIME \'-10s\'', + string: 'SELECT "user".* FROM "user" , "post" AS OF SYSTEM TIME \'-10s\'' + }, + oracle: { + text: 'SELECT "user".* FROM "user" , "post" AS OF TIMESTAMP \'-10s\'', + string: 'SELECT "user".* FROM "user" , "post" AS OF TIMESTAMP \'-10s\'' + } +});