Skip to content

Commit

Permalink
Add Global/Local Secondary Index creation support (#15)
Browse files Browse the repository at this point in the history
* add Index creation function

Co-authored-by: lucemans <[email protected]>

* Make test script run build first

* Fix return type of selectOneFrom

Co-authored-by: lucemans <[email protected]>

* Remove forgotten example comment

Co-authored-by: Luc <[email protected]>

Co-authored-by: lucemans <[email protected]>
  • Loading branch information
svemat01 and lucemans authored Jan 1, 2022
1 parent 1431972 commit f1da4f2
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 37 deletions.
29 changes: 29 additions & 0 deletions __tests__/createIndexRaw.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createIndexRaw, createLocalIndexRaw } from '../lib';

it('Can create a basic table', async () => {
expect(
createIndexRaw<{ buildings: { name: string, city: string } }, 'buildings', 'name' | 'city'>(
'scyllo',
'buildings',
'buildings_city',
'city'
)
).toEqual({
args: [],
query: 'CREATE INDEX IF NOT EXISTS buildings_city ON scyllo.buildings (city)',
});
});

it('Can create a basic table if not exists', async () => {
expect(
createLocalIndexRaw<{ buildings: { name: string, city: string } }, 'buildings', 'name' | 'city'>(
'scyllo',
'buildings',
'buildings_index',
'name',
'city'
)).toEqual({
args: [],
query: 'CREATE INDEX IF NOT EXISTS buildings_index ON scyllo.buildings ((name), city)',
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"typescript-eslint": "^0.0.1-alpha.0"
},
"scripts": {
"test": "jest",
"test": "yarn build && jest",
"build": "tsc",
"lint": "eslint -c .eslintrc.json --ext .ts ./src",
"pub": "yarn build && yarn publish --access public"
Expand Down
195 changes: 162 additions & 33 deletions src/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,167 @@ import { TableScheme } from './ScylloClient';
import { toScyllo } from './ScylloTranslator';

export type QueryBuild = {
query: string,
args: any[]
query: string;
args: any[];
};

export const selectFromRaw = <TableMap extends TableScheme, F extends keyof TableMap>(keyspace: string, table: F, select: '*' | (keyof TableMap[F])[], criteria?: { [key in keyof TableMap[F]]?: TableMap[F][key] | string }, extra?: string): QueryBuild => ({
query: `SELECT ${select == '*' ? select : select.join(',')} FROM ${keyspace}.${table} ${criteria && Object.keys(criteria).length > 0 ? ('WHERE ' + Object.keys(criteria).map(crit => crit + '=?').join(' AND ')) : ''} ${extra || ''}`.trim(),
args: [...(criteria ? Object.values(criteria) : [])]
});

export const selectOneFromRaw = <TableMap extends TableScheme, F extends keyof TableMap>(keyspace: string, table: F, select: '*' | (keyof TableMap[F])[], criteria?: { [key in keyof TableMap[F]]?: TableMap[F][key] | string }, extra?: string): QueryBuild => ({
query: `SELECT ${select == '*' ? select : select.join(',')} FROM ${keyspace}.${table} ${criteria && Object.keys(criteria).length > 0 ? ('WHERE ' + Object.keys(criteria).map(crit => crit + '=?').join(' AND ')) : ''} LIMIT 1 ${extra || ''}`.trim(),
args: [...(criteria ? Object.values(criteria) : [])]
});

export const insertIntoRaw = <TableMap extends TableScheme, F extends keyof TableMap>(keyspace: string, table: F, obj: Partial<TableMap[F]>): QueryBuild => ({
query: `INSERT INTO ${keyspace}.${table} (${Object.keys(obj).join(', ')}) VALUES (${Object.keys(obj).map(() => '?').join(', ')})`,
args: Object.values(obj).map(toScyllo)
});

export const updateRaw = <TableMap extends TableScheme, TableName extends keyof TableMap, ColumnName extends keyof TableMap[TableName]>(keyspace: string, table: TableName, obj: Partial<TableMap[TableName]>, criteria: { [key in ColumnName]?: TableMap[TableName][key] | string }): QueryBuild => ({
query: `UPDATE ${keyspace}.${table} SET ${Object.keys(obj).map(it => it + '=?').join(',')} ${criteria && Object.keys(criteria).length > 0 ? 'WHERE ' + Object.keys(criteria).map(crit => crit += '=?').join(' AND ') : ''}`.trim(),
args: [...Object.values(obj), ...(criteria ? Object.values(criteria) : [])]
});

export const deleteFromRaw = <TableMap extends TableScheme, TableName extends keyof TableMap, ColumnName extends keyof TableMap[TableName]>(keyspace: string, table: TableName, fields: '*' | ColumnName[], criteria: { [key in ColumnName]?: TableMap[TableName][key] | string }, extra?: string): QueryBuild => ({
query: `DELETE ${fields == '*' ? '' : fields.join(',')} ${keyspace}.${table} ${criteria && Object.keys(criteria).length > 0 ? ('WHERE ' + Object.keys(criteria).map(crit => crit + '=?').join(' AND ')) : ''} ${extra || ''}`.trim(),
args: [...(criteria ? Object.values(criteria) : [])]
});

export type ColumnType = { type: keyof typeof types.dataTypes, test?: string };

export const createTableRaw = <TableMap extends TableScheme, F extends keyof TableMap>(keyspace: string, table: F, createIfNotExists: boolean, columns: { [key in keyof TableMap[F]]: ColumnType }, partition: [keyof TableMap[F], keyof TableMap[F]] | keyof TableMap[F], clustering?: (keyof TableMap[F])[]): QueryBuild => ({
query: `CREATE TABLE${createIfNotExists ? ' IF NOT EXISTS' : ''} ${keyspace}.${table} (${(Object.keys(columns) as (keyof TableMap[F])[]).map((a) => a + ' ' + columns[a].type).join(',')}, PRIMARY KEY (${partition instanceof Array ? '(' + partition.join(',') + ')' : partition}${clustering ? `, ${clustering.join(',')}` : ''}))`,
args: []
});
export const selectFromRaw = <
TableMap extends TableScheme,
F extends keyof TableMap,
>(
keyspace: string,
table: F,
select: '*' | (keyof TableMap[F])[],
criteria?: { [key in keyof TableMap[F]]?: TableMap[F][key] | string },
extra?: string,
): QueryBuild => ({
query: `SELECT ${
select == '*' ? select : select.join(',')
} FROM ${keyspace}.${table} ${
criteria && Object.keys(criteria).length > 0
? 'WHERE ' +
Object.keys(criteria)
.map((crit) => crit + '=?')
.join(' AND ')
: ''
} ${extra || ''}`.trim(),
args: [...(criteria ? Object.values(criteria) : [])],
});

export const selectOneFromRaw = <
TableMap extends TableScheme,
F extends keyof TableMap,
>(
keyspace: string,
table: F,
select: '*' | (keyof TableMap[F])[],
criteria?: { [key in keyof TableMap[F]]?: TableMap[F][key] | string },
extra?: string,
): QueryBuild => ({
query: `SELECT ${
select == '*' ? select : select.join(',')
} FROM ${keyspace}.${table} ${
criteria && Object.keys(criteria).length > 0
? 'WHERE ' +
Object.keys(criteria)
.map((crit) => crit + '=?')
.join(' AND ')
: ''
} LIMIT 1 ${extra || ''}`.trim(),
args: [...(criteria ? Object.values(criteria) : [])],
});

export const insertIntoRaw = <
TableMap extends TableScheme,
F extends keyof TableMap,
>(
keyspace: string,
table: F,
obj: Partial<TableMap[F]>,
): QueryBuild => ({
query: `INSERT INTO ${keyspace}.${table} (${Object.keys(obj).join(
', ',
)}) VALUES (${Object.keys(obj)
.map(() => '?')
.join(', ')})`,
args: Object.values(obj).map(toScyllo),
});

export const updateRaw = <
TableMap extends TableScheme,
TableName extends keyof TableMap,
ColumnName extends keyof TableMap[TableName],
>(
keyspace: string,
table: TableName,
obj: Partial<TableMap[TableName]>,
criteria: { [key in ColumnName]?: TableMap[TableName][key] | string },
): QueryBuild => ({
query: `UPDATE ${keyspace}.${table} SET ${Object.keys(obj)
.map((it) => it + '=?')
.join(',')} ${
criteria && Object.keys(criteria).length > 0
? 'WHERE ' +
Object.keys(criteria)
.map((crit) => (crit += '=?'))
.join(' AND ')
: ''
}`.trim(),
args: [...Object.values(obj), ...(criteria ? Object.values(criteria) : [])],
});

export const deleteFromRaw = <
TableMap extends TableScheme,
TableName extends keyof TableMap,
ColumnName extends keyof TableMap[TableName],
>(
keyspace: string,
table: TableName,
fields: '*' | ColumnName[],
criteria: { [key in ColumnName]?: TableMap[TableName][key] | string },
extra?: string,
): QueryBuild => ({
query: `DELETE ${
fields == '*' ? '' : fields.join(',')
} ${keyspace}.${table} ${
criteria && Object.keys(criteria).length > 0
? 'WHERE ' +
Object.keys(criteria)
.map((crit) => crit + '=?')
.join(' AND ')
: ''
} ${extra || ''}`.trim(),
args: [...(criteria ? Object.values(criteria) : [])],
});

export type ColumnType = { type: keyof typeof types.dataTypes; test?: string };

export const createTableRaw = <
TableMap extends TableScheme,
F extends keyof TableMap,
>(
keyspace: string,
table: F,
createIfNotExists: boolean,
columns: { [key in keyof TableMap[F]]: ColumnType },
partition: [keyof TableMap[F], keyof TableMap[F]] | keyof TableMap[F],
clustering?: (keyof TableMap[F])[],
): QueryBuild => ({
query: `CREATE TABLE${
createIfNotExists ? ' IF NOT EXISTS' : ''
} ${keyspace}.${table} (${(Object.keys(columns) as (keyof TableMap[F])[])
.map((a) => a + ' ' + columns[a].type)
.join(',')}, PRIMARY KEY (${
partition instanceof Array ? '(' + partition.join(',') + ')' : partition
}${clustering ? `, ${clustering.join(',')}` : ''}))`,
args: [],
});

export const createIndexRaw = <
TableMap extends TableScheme,
Table extends keyof TableMap,
ColumnName extends keyof TableMap[Table],
>(
keyspace: string,
table: Table,
materialized_name: string,
column_to_index: ColumnName,
): QueryBuild => ({
query: `CREATE INDEX IF NOT EXISTS ${materialized_name} ON ${keyspace}.${table} (${column_to_index})`,
args: [],
});

export const createLocalIndexRaw = <
TableMap extends TableScheme,
Table extends keyof TableMap,
ColumnName extends keyof TableMap[Table],
>(
keyspace: string,
table: Table,
materialized_name: string,
primary_column: ColumnName,
column_to_index: ColumnName,
): QueryBuild => ({
query: `CREATE INDEX IF NOT EXISTS ${materialized_name} ON ${keyspace}.${table} ((${primary_column}), ${column_to_index})`,
args: [],
});
38 changes: 35 additions & 3 deletions src/ScylloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { LogMethod } from '@lvksh/logger';
import { Client, DseClientOptions as CassandraConfig, types } from 'cassandra-driver';

import {
createIndexRaw,
createLocalIndexRaw,
createTableRaw,
deleteFromRaw,
insertIntoRaw,
QueryBuild,
updateRaw,
} from './';
updateRaw} from './';
import { selectFromRaw, selectOneFromRaw } from './QueryBuilder';
import { fromScyllo, ValidDataType } from './ScylloTranslator';

Expand Down Expand Up @@ -124,7 +125,7 @@ export class ScylloClient<Tables extends TableScheme> {
select: '*' | ColumnName[],
criteria?: { [key in keyof Tables[Table]]?: Tables[Table][key] | string },
extra?: string
): Promise<Pick<Tables[Table], ColumnName>> {
): Promise<Pick<Tables[Table], ColumnName> | undefined> {
const query = selectOneFromRaw<Tables, Table>(
this.keyspace,
table,
Expand Down Expand Up @@ -242,4 +243,35 @@ export class ScylloClient<Tables extends TableScheme> {
`CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH replication = {'class':'${replicationClass}', 'replication_factor' : ${replicationFactor}};`
);
}

/**
* Create a Global Secondary Index
*
* https://docs.scylladb.com/using-scylla/secondary-indexes/
*/
async createIndex<Table extends keyof Tables, ColumnName extends keyof Tables[Table]>(
table: Table,
materialized_name: string,
column_to_index: ColumnName
): Promise<types.ResultSet> {
const query = createIndexRaw<Tables, Table, ColumnName>(this.keyspace, table, materialized_name, column_to_index);

return await this.query(query);
}

/**
* Create a Local Secondary Index
*
* https://docs.scylladb.com/using-scylla/local-secondary-indexes/
*/
async createLocalIndex<Table extends keyof Tables, ColumnName extends keyof Tables[Table]>(
table: Table,
materialized_name: string,
primary_column: ColumnName,
column_to_index: ColumnName
): Promise<types.ResultSet> {
const query = createLocalIndexRaw<Tables, Table, ColumnName>(this.keyspace, table, materialized_name, primary_column, column_to_index);

return await this.query(query);
}
}

0 comments on commit f1da4f2

Please sign in to comment.