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

attributes for select query #5

Merged
merged 3 commits into from
Dec 28, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 71 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Minimalistic JSON database.

[![Build Status](https://github.com/yusufshakeel/minivium/actions/workflows/ci.yml/badge.svg)](https://github.com/yusufshakeel/minivium/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yusufshakeel/minivium)
[![npm version](https://img.shields.io/badge/npm-0.1.16-blue.svg)](https://www.npmjs.com/package/minivium)
[![npm version](https://img.shields.io/badge/npm-0.1.17-blue.svg)](https://www.npmjs.com/package/minivium)
[![npm Downloads](https://img.shields.io/npm/dm/minivium.svg)](https://www.npmjs.com/package/minivium)

![img.webp](assets/img.webp)
Expand All @@ -20,7 +20,7 @@ Minimalistic JSON database.
* [Install the package](#install-the-package)
* [Import](#import)
* [Create schema registry and data directory](#create-schema-registry-and-data-directory)
* [Attributes for columns](#attributes-for-columns)
* [Collection](#Collection)
* [Minivium reference](#minivium-reference)
* [Initialise the collections](#initialise-the-collections)
* [Drop collection](#drop-collection)
Expand All @@ -30,6 +30,8 @@ Minimalistic JSON database.
* [Select](#select)
* [Update](#update)
* [Delete](#delete)
* [Attributes](#attributes)
* [Alias for attributes](#alias-for-attributes)
* [Operators for where clause](#operators-for-where-clause)
* [Equal](#equal-eq)
* [Not equal](#not-equal-noteq)
Expand Down Expand Up @@ -100,7 +102,17 @@ const schemaRegistry = new SchemaRegistry({
});
```

#### Attributes for columns
#### Collection

A collection (similar to a table in PostgreSQL) consists of a name and columns.


| Attribute | Purpose |
|-----------|--------------------------------------------------------------------------------------------|
| name | Name of the collection |
| columns | Array of columns. Can be set to empty array `[]` if we don't want strict schema structure. |

Attributes for columns.

| Attribute | Purpose |
|------------|---------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -138,6 +150,25 @@ db.dropAllCollections();

## Query

Syntax `query.type(collectionName, [data], [option])`

| Particular | Purpose |
|------------------|---------------------------------------------------------------------|
| `type` | This represents the query type. Example `insert` query. |
| `collectionName` | This is the name of the collection we want the query to run on. |
| `[data]` | This represents the data to [insert](#insert) or [update](#update). |
| `[option]` | (Optional) This is the option for the query. |

#### Option

| Option | Purpose |
|----------------|----------------------------------------------------------------------------------------------------------|
| `[where]` | This represents the where clause and it consists of the common [operators](#operators-for-where-clause). |
| `[limit]` | This helps in selecting the first N rows. Refer [limit](#limit) |
| `[offset]` | This helps in skipping M rows. Refer [offset](#offset) |
| `[attributes]` | This helps in selecting specific columns and also to give alias. Refer [attributes](#attributes) |


### Insert

Syntax `insert(collectionName, dataToInsert)`
Expand Down Expand Up @@ -203,6 +234,43 @@ const deletedRowCount = db.query.delete(

This behavior is similar to databases like PostgreSQL.

## Attributes

Set the `attributes` option to pick the desired columns.

```js
db.query.select('users', { attributes: ['id', 'username'] });
```

SQL equivalent

```sql
select id, username from users;
```

## Alias for attributes

Pass `[columnName, aliasName]` tuple in the `attributes` option to set an alias for a column.

```js
db.query.select(
'users',
{
attributes: [
'id',
'username',
['score', 'totalScore']
]
}
);
```

SQL equivalent

```sql
select id, username, score as totalScore from users;
```

## Operators for where clause

Minivium comes with a simple query language that is inspired by Sequalize and Mango Query.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minivium",
"version": "0.1.16",
"version": "0.1.17",
"description": "Minimalistic JSON database.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
57 changes: 42 additions & 15 deletions src/core/Query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { SchemaRegistry } from './SchemaRegistry';
import { FileSync } from './File';
import { genId } from '../utils/id';
import { QueryOption, SelectQueryOption } from '../types/query';
import { QueryOption, SelectQueryAttribute, SelectQueryOption } from '../types/query';
import { filter } from '../helpers/filter';
import { columnsViolatingUniqueConstraint } from '../helpers/unique';
import { selectAttributes } from '../helpers/select';

export class Query {
private readonly schemaRegistry: SchemaRegistry;
Expand Down Expand Up @@ -85,37 +86,63 @@ export class Query {
return dataToInsert.id;
}

select(collectionName: string, option?: SelectQueryOption): any[] {
this.collectionExists(collectionName);

const { limit, offset } = option || {};
private validateAttributes(collectionName: string, attributes: SelectQueryAttribute[]) {
if (attributes.length) {
const columnNames = this.schemaRegistry.getColumnNames(collectionName);
const columnsNamesToSelect = attributes.map(v => {
if (Array.isArray(v)) {
return v[0];
}
return v;
});
const isAllValidColumnNames = columnsNamesToSelect.filter(v => {
return !columnNames.includes(v);
});
if(isAllValidColumnNames.length) {
throw new Error(`Invalid column names passed in attributes: ${isAllValidColumnNames.join(', ')}`);
}
}
}

private validateLimitAndOffset(limit: number | undefined, offset: number | undefined) {
if (limit! < 0) {
throw new Error('Limit must not be negative');
}

if (offset! < 0) {
throw new Error('Offset must not be negative');
}
}

select(collectionName: string, option?: SelectQueryOption): any[] {
this.collectionExists(collectionName);

const { limit, offset, attributes } = option || {};

this.validateLimitAndOffset(limit, offset);

if(limit === 0) {
return [];
}

const rowsAfterWhereClauseFilter =
filter(this.readCollectionContent(collectionName), option?.where);
if (attributes) {
this.validateAttributes(collectionName, attributes);
}

let selectedRows = filter(this.readCollectionContent(collectionName), option?.where);

if (limit !== undefined && offset !== undefined) {
return rowsAfterWhereClauseFilter.slice(offset, offset + limit);
selectedRows = selectedRows.slice(offset, offset + limit);
} else if (offset !== undefined) {
selectedRows = selectedRows.slice(offset);
} else if (limit !== undefined) {
selectedRows = selectedRows.slice(0, limit);
}
if (offset !== undefined) {
return rowsAfterWhereClauseFilter.slice(offset);
}
if (limit !== undefined) {
return rowsAfterWhereClauseFilter.slice(0, limit);

if (attributes?.length) {
return selectAttributes(attributes, selectedRows);
}

return rowsAfterWhereClauseFilter;
return selectedRows;
}

update(collectionName: string, data: object, option?: QueryOption): number {
Expand Down
19 changes: 19 additions & 0 deletions src/helpers/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SelectQueryAttribute } from '../types/query';

export function selectAttributes(
attributes: SelectQueryAttribute[],
allRows: any[]
): any[] {
return allRows.map((row: any) => {
return attributes.reduce((acc: any, attribute: string | [string, string]) => {
if (typeof attribute === 'string') {
acc[attribute] = row[attribute];
}
if (Array.isArray(attribute)) {
const [currentName, newName] = attribute as [string, string];
acc[newName] = row[currentName];
}
return acc;
}, {} as any);
});
}
3 changes: 3 additions & 0 deletions src/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ export type QueryOption = {
where: Filter;
};

export type SelectQueryAttribute = (string | [string, string]);

export type SelectQueryOption = {
where?: Filter;
limit?: number;
offset?: number;
attributes?: SelectQueryAttribute[];
};
25 changes: 25 additions & 0 deletions test/unit/core/Query.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,5 +373,30 @@ describe('Query', () => {
]);
});
});

describe('attributes', () => {
it('should throw error when unknown column names passed in attributes', () => {
expect(() => {
query.select('users', { attributes: ['col1', ['col2', 'alias']] });
}).toThrow('Invalid column names passed in attributes: col1, col2');
});

it('should return specified columns as per the passed attributes', () => {
mockFileSyncInstance.readSync.mockReturnValue(
'[{"id":"193fce9d5cb","username":"yusuf","password":"123456"},' +
'{"id":"193fce9df12","username":"john","password":"123456","phoneNumber":"123"},' +
'{"id":"193fce9ea27","username":"jane","password":"123456"}]'
);
const result = query.select(
'users',
{ attributes: ['id', 'username', ['phoneNumber', 'contact']] }
);
expect(result).toStrictEqual([
{ id: '193fce9d5cb', username: 'yusuf', contact: undefined },
{ id: '193fce9df12', username: 'john', contact: '123' },
{ id: '193fce9ea27', username: 'jane', contact: undefined }
]);
});
});
});
});
36 changes: 36 additions & 0 deletions test/unit/helpers/select.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { users } from '../../testdata/users';
import { selectAttributes } from '../../../src/helpers/select';

describe('select', () => {
describe('selectAttributes', () => {
it('should return specified columns', () => {
expect(selectAttributes(['id', 'name', 'isOnline'], users)).toStrictEqual([
{ id: 1, name: 'john', isOnline: false },
{ id: 2, name: 'jane', isOnline: true },
{ id: 3, name: 'tom', isOnline: false },
{ id: 4, name: 'jerry', isOnline: true },
{ id: 5, name: 'bruce', isOnline: true }
]);
});

it('should return specified columns with custom names', () => {
expect(selectAttributes(['id', ['name', 'username'], 'isOnline'], users)).toStrictEqual([
{ id: 1, username: 'john', isOnline: false },
{ id: 2, username: 'jane', isOnline: true },
{ id: 3, username: 'tom', isOnline: false },
{ id: 4, username: 'jerry', isOnline: true },
{ id: 5, username: 'bruce', isOnline: true }
]);
});

it('should return undefined value for unknown column', () => {
expect(selectAttributes(['id', 'username', 'isOnline'], users)).toStrictEqual([
{ id: 1, username: undefined, isOnline: false },
{ id: 2, username: undefined, isOnline: true },
{ id: 3, username: undefined, isOnline: false },
{ id: 4, username: undefined, isOnline: true },
{ id: 5, username: undefined, isOnline: true }
]);
});
});
});
Loading