Skip to content

Commit

Permalink
feat: support AS OF queries for CockroachDB
Browse files Browse the repository at this point in the history
  • Loading branch information
charsleysa committed Sep 20, 2024
1 parent bd5434a commit 22128b0
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 1 deletion.
7 changes: 7 additions & 0 deletions lib/dialect/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -208,6 +209,8 @@ export abstract class Dialect<ConfigType> {
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':
Expand Down Expand Up @@ -446,6 +449,10 @@ export abstract class Dialect<ConfigType> {
}
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(', ')];
Expand Down
4 changes: 4 additions & 0 deletions lib/dialect/mssql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.');
}
Expand Down
4 changes: 4 additions & 0 deletions lib/dialect/mysql.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -67,6 +68,9 @@ export class Mysql extends Dialect<any> {
}
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 */
Expand Down
5 changes: 5 additions & 0 deletions lib/dialect/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -95,6 +96,10 @@ export class Oracle extends Dialect<any> {
}
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 = '';
Expand Down
4 changes: 4 additions & 0 deletions lib/dialect/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion lib/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
7 changes: 7 additions & 0 deletions lib/node/asOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Node } from './node.js';

export class AsOfNode extends Node {
constructor() {
super('AS OF');
}
}
6 changes: 6 additions & 0 deletions lib/node/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(args: (T | T[])[]): T[] => {
Expand Down Expand Up @@ -152,6 +153,11 @@ export class Query<T> 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());
Expand Down
32 changes: 32 additions & 0 deletions test/dialects/as-of-tests.ts
Original file line number Diff line number Diff line change
@@ -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\''
}
});

0 comments on commit 22128b0

Please sign in to comment.