Skip to content

Commit

Permalink
Add an ability to specify field aliases on a per-field basis
Browse files Browse the repository at this point in the history
When using joins in `CustomClauseQuery`, if you want to sort by a column on a joined table, you need to include that column in the select fields. However, if you are using aliases, there's no way to add a column not on the main table alias. This adds a new field type beyond just `string` that lets you specify the alias for just that single field, which will override the default alias for the table.
  • Loading branch information
Swahvay committed Feb 16, 2024
1 parent c2ab683 commit 24e6aef
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 66 deletions.
39 changes: 23 additions & 16 deletions ts/src/action/operations.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import { Queryer, SyncQueryer } from "../core/db";
import { Builder, WriteOperation } from "../action";
import { Executor } from "../action/action";
import {
Viewer,
Ent,
ID,
Context,
CreateRowOptions,
Data,
DataOptions,
EditRowOptions,
Ent,
ID,
LoadEntOptions,
Context,
CreateRowOptions,
Viewer,
} from "../core/base";
import { Executor } from "../action/action";
import * as clause from "../core/clause";
import { WriteOperation, Builder } from "../action";
import { ObjectLoader } from "../core/loaders";
import {
getStorageKey,
SQLStatementOperation,
TransformedEdgeUpdateOperation,
} from "../schema/schema";
import { __getGlobalSchema } from "../core/global_schema";
import { Queryer, SyncQueryer } from "../core/db";
import {
AssocEdgeData,
createRow,
Expand All @@ -32,7 +25,14 @@ import {
logQuery,
parameterizedQueryOptions,
} from "../core/ent";
import { __getGlobalSchema } from "../core/global_schema";
import { ObjectLoader } from "../core/loaders";
import { buildQuery } from "../core/query_impl";
import {
SQLStatementOperation,
TransformedEdgeUpdateOperation,
getStorageKey,
} from "../schema/schema";

export interface UpdatedOperation<
TEnt extends Ent<TViewer>,
Expand Down Expand Up @@ -329,7 +329,14 @@ export class EditNodeOperation<

private getReturning() {
if (this.options.loadEntOptions.fields.length) {
return `RETURNING ${this.options.loadEntOptions.fields.join(",")}`;
return `RETURNING ${this.options.loadEntOptions.fields
.map((f) => {
if (typeof f === "object") {
return `${f.alias}.${f.column}`;
}
return f;
})
.join(",")}`;
}
return `RETURNING *`;
}
Expand Down
8 changes: 7 additions & 1 deletion ts/src/core/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,13 @@ export interface DataOptions {

export interface SelectBaseDataOptions extends DataOptions {
// list of fields to read
fields: string[];
fields: (
| string
| {
alias: string;
column: string;
}
)[];
// use this alias to alias the fields instead of the table name or table alias
// takes precedence over tableName and alias
fieldsAlias?: string;
Expand Down
13 changes: 10 additions & 3 deletions ts/src/core/context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Viewer, Data, Loader, LoaderWithLoadMany, QueryOptions } from "./base";
import { IncomingMessage, ServerResponse } from "http";
import { Data, Loader, LoaderWithLoadMany, QueryOptions, Viewer } from "./base";

import { log } from "./logger";
import { Context } from "./base";
import { log } from "./logger";
import { getJoinInfo, getOrderByPhrase } from "./query_impl";

// RequestBasedContext e.g. from an HTTP request with a server/response conponent
Expand Down Expand Up @@ -57,7 +57,14 @@ export class ContextCache {

private getkey(options: QueryOptions): string {
let parts: string[] = [
options.fields.join(","),
options.fields
.map((f) => {
if (typeof f === "object") {
return `${f.alias}.${f.column}`;
}
return f;
})
.join(","),
options.clause.instanceKey(),
];
if (options.orderby) {
Expand Down
72 changes: 44 additions & 28 deletions ts/src/core/ent.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
import DB, {
Dialect,
Queryer,
SyncQueryer,
QueryResult,
QueryResultRow,
} from "./db";
import {
Viewer,
Ent,
ID,
LoadRowsOptions,
LoadRowOptions,
Context,
CreateRowOptions,
Data,
DataOptions,
QueryableDataOptions,
EditRowOptions,
LoadEntOptions,
LoadCustomEntOptions,
EdgeQueryableDataOptions,
Context,
SelectDataOptions,
CreateRowOptions,
QueryDataOptions,
EditRowOptions,
Ent,
EntConstructor,
PrivacyPolicy,
SelectCustomDataOptions,
PrimableLoader,
ID,
LoadCustomEntOptions,
LoadEntOptions,
LoadRowOptions,
LoadRowsOptions,
Loader,
LoaderWithLoadMany,
PrimableLoader,
PrivacyPolicy,
QueryDataOptions,
SelectCustomDataOptions,
SelectDataOptions,
Viewer,
} from "./base";
import DB, {
Dialect,
QueryResult,
QueryResultRow,
Queryer,
SyncQueryer,
} from "./db";

import { applyPrivacyPolicy, applyPrivacyPolicyImpl } from "./privacy";

import * as clause from "./clause";
import { log, logEnabled, logTrace } from "./logger";
import DataLoader from "dataloader";
import * as clause from "./clause";
import { __getGlobalSchema } from "./global_schema";
import { OrderBy, buildQuery, getOrderByPhrase } from "./query_impl";
import { CacheMap } from "./loaders/loader";
import { log, logEnabled, logTrace } from "./logger";
import { OrderBy, buildQuery, getOrderByPhrase } from "./query_impl";

class entCacheMap<TViewer extends Viewer, TEnt extends Ent<TViewer>> {
private m = new Map();
Expand Down Expand Up @@ -926,7 +925,13 @@ interface GroupQueryOptions<T extends Data, K = keyof T> {
// extra clause to join
clause?: clause.Clause<T, K>;
groupColumn: K;
fields: K[];
fields: (
| K
| {
alias: string;
column: K;
}
)[];
values: any[];
orderby?: OrderBy;
limit: number;
Expand All @@ -949,8 +954,19 @@ export function buildGroupQuery<T extends Data = Data, K = keyof T>(

// window functions work in sqlite!
// https://www.sqlite.org/windowfunctions.html
const fieldString = fields
.map((f) => {
if (typeof f === "object") {
// TS doesn't understand that K can only be a string, so we need
// for it to treat f as the object we know it is.
const fObj = f as { alias: string; column: string };
return `${fObj.column}.${fObj.alias}`;
}
return f;
})
.join(",");
return [
`SELECT * FROM (SELECT ${fields.join(",")} OVER (PARTITION BY ${
`SELECT * FROM (SELECT ${fieldString} OVER (PARTITION BY ${
options.groupColumn
} ${orderby}) as row_num FROM ${options.tableName} WHERE ${cls.clause(
1,
Expand Down
20 changes: 13 additions & 7 deletions ts/src/core/loaders/query_loader.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import DataLoader from "dataloader";
import memoizee from "memoizee";
import {
Context,
Data,
EdgeQueryableDataOptions,
Loader,
LoaderFactory,
Data,
PrimableLoader,
} from "../base";
import * as clause from "../clause";
import {
getDefaultLimit,
performRawQuery,
buildGroupQuery,
getDefaultLimit,
loadRows,
performRawQuery,
} from "../ent";
import * as clause from "../clause";
import { logEnabled } from "../logger";
import { OrderBy } from "../query_impl";
import { CacheMap, getCustomLoader, getLoader } from "./loader";
import memoizee from "memoizee";
import { ObjectLoaderFactory } from "./object_loader";
import { OrderBy } from "../query_impl";

function getOrderByLocal(
options: QueryOptions,
Expand Down Expand Up @@ -240,7 +240,13 @@ class QueryLoader<K extends any> implements Loader<K, Data[]> {
}

interface QueryOptions {
fields: string[];
fields: (
| string
| {
alias: string;
column: string;
}
)[];
tableName: string; // or function for assoc_edge. come back to it
// if provided, we'll group queries to the database via this key and this will be the unique id we're querying for
// using window functions or not
Expand Down
17 changes: 10 additions & 7 deletions ts/src/core/query/custom_clause_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ID,
LoadEntOptions,
QueryDataOptions,
SelectBaseDataOptions,
Viewer,
} from "../base";
import { AndOptional, Clause } from "../clause";
Expand Down Expand Up @@ -126,17 +127,19 @@ export class CustomClauseQuery<

async queryRawCount(): Promise<number> {
// sqlite needs as count otherwise it returns count(1)
let fields = ["count(1) as count"];
let fields: SelectBaseDataOptions["fields"] = ["count(1) as count"];
if (this.options.joinBETA) {
const requestedFields = this.options.loadEntOptions.fields;
const firstRequestedField = this.options.loadEntOptions.fields[0];
const alias =
this.options.loadEntOptions.fieldsAlias ??
this.options.loadEntOptions.alias;
if (alias) {
fields = [`count(distinct ${alias}.${requestedFields[0]}) as count`];
} else {
fields = [`count(distinct ${requestedFields[0]}) as count`];
}
const fieldString =
typeof firstRequestedField === "object"
? `${firstRequestedField.alias}.${firstRequestedField.column}`
: alias
? `${alias}.${firstRequestedField}`
: firstRequestedField;
fields = [`count(distinct ${fieldString}) as count`];
}
const row = await loadRow({
...this.options.loadEntOptions,
Expand Down
18 changes: 14 additions & 4 deletions ts/src/core/query_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,20 @@ export function getJoinInfo(

export function buildQuery(options: QueryableDataOptions): string {
const fieldsAlias = options.fieldsAlias ?? options.alias;
const fields =
fieldsAlias && !options.disableFieldsAlias
? options.fields.map((f) => `${fieldsAlias}.${f}`).join(", ")
: options.fields.join(", ");
const fields = options.fields
.map((f) => {
if (typeof f === "object") {
if (!options.disableFieldsAlias) {
return `${f.alias}.${f.column}`;
}
return f.column;
}
if (fieldsAlias && !options.disableFieldsAlias) {
return `${fieldsAlias}.${f}`;
}
return f;
})
.join(", ");

// always start at 1
const parts: string[] = [];
Expand Down

0 comments on commit 24e6aef

Please sign in to comment.