Skip to content

Commit

Permalink
Merge pull request #104 from shwinnn/include-views
Browse files Browse the repository at this point in the history
include list of views
  • Loading branch information
pbugnion committed Dec 31, 2019
2 parents 6d89021 + e4e64fb commit ec0d605
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 40 deletions.
94 changes: 69 additions & 25 deletions __tests__/src/api/databaseStructure.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
getDatabaseStructure,
DatabaseStructureResponse
DatabaseStructureResponse,
DatabaseObjects
} from '../../../src/api';

import { ServerConnection } from '@jupyterlab/services';
Expand All @@ -15,38 +16,65 @@ jest.mock('@jupyterlab/services', () => ({
}));

namespace Fixtures {
export const success = {
export const databaseWithViews: DatabaseObjects = {
tables: ['t1', 't2'],
views: ['v1', 'v2']
};

export const successResponseBody: DatabaseStructureResponse.Type = {
responseType: 'success',
responseData: {
tables: ['t1', 't2'],
views: ['v1', 'v2']
}
};

export const databaseWithoutViews: DatabaseObjects = {
tables: ['t1', 't2'],
views: []
};

export const successWithoutViewsResponseBody: DatabaseStructureResponse.Type = {
responseType: 'success',
responseData: {
tables: ['t1', 't2']
}
};

export const successResponse = new Response(JSON.stringify(success));
export const successWithViewsEmptyResponseBody: DatabaseStructureResponse.Type = {
responseType: 'success',
responseData: {
tables: ['t1', 't2'],
views: []
}
};

export const error = {
export const errorResponseBody = {
responseType: 'error',
responseData: {
message: 'some message'
}
};

export const errorResponse = new Response(JSON.stringify(error));
export const mockServerWithResponse = (responseBody: Object) => {
const response: Response = new Response(JSON.stringify(responseBody));
return jest.fn(() => Promise.resolve(response));
};
}

describe('getDatabaseStructure', () => {
const testCases: Array<Array<any>> = [
['success', Fixtures.success],
['error', Fixtures.error]
['success', Fixtures.successResponseBody],
['error', Fixtures.errorResponseBody]
];

it.each(testCases)('valid %#: %s', async (_, response) => {
ServerConnection.makeRequest = jest.fn(() =>
Promise.resolve(new Response(JSON.stringify(response)))
it.each(testCases)('valid %#: %s', async (_, responseBody) => {
ServerConnection.makeRequest = Fixtures.mockServerWithResponse(
responseBody
);

const result = await getDatabaseStructure('connectionUrl');
expect(result).toEqual(response);
expect(result).toEqual(responseBody);
const expectedUrl = 'https://example.com/jupyterlab-sql/database';
const expectedRequest = {
method: 'POST',
Expand All @@ -60,32 +88,48 @@ describe('getDatabaseStructure', () => {
);
});

it('matching on success', async () => {
ServerConnection.makeRequest = jest.fn(() =>
Promise.resolve(Fixtures.successResponse)
);
const successTestCases: Array<Array<any>> = [
['with views', Fixtures.databaseWithViews, Fixtures.successResponseBody],
[
'no views',
Fixtures.databaseWithoutViews,
Fixtures.successWithoutViewsResponseBody
],
[
'empty views',
Fixtures.databaseWithoutViews,
Fixtures.successWithViewsEmptyResponseBody
]
];
it.each(successTestCases)(
'matching on success %#: %s',
async (_, expected, responseBody) => {
ServerConnection.makeRequest = Fixtures.mockServerWithResponse(
responseBody
);

const result = await getDatabaseStructure('connectionUrl');
const result = await getDatabaseStructure('connectionUrl');

const mockOnSuccess = jest.fn();
DatabaseStructureResponse.match(result, mockOnSuccess, jest.fn());
const mockOnSuccess = jest.fn();
DatabaseStructureResponse.match(result, mockOnSuccess, jest.fn());

expect(mockOnSuccess).toHaveBeenCalledWith(
Fixtures.success.responseData.tables
);
});
expect(mockOnSuccess).toHaveBeenCalledWith(expected);
}
);

it('matching on error', async () => {
ServerConnection.makeRequest = jest.fn(() =>
Promise.resolve(Fixtures.errorResponse)
ServerConnection.makeRequest = Fixtures.mockServerWithResponse(
Fixtures.errorResponseBody
);

const result = await getDatabaseStructure('connectionUrl');

const mockOnError = jest.fn();
DatabaseStructureResponse.match(result, jest.fn(), mockOnError);

expect(mockOnError).toHaveBeenCalledWith(Fixtures.error.responseData);
expect(mockOnError).toHaveBeenCalledWith(
Fixtures.errorResponseBody.responseData
);
});

it('bad http status code', async () => {
Expand Down
16 changes: 11 additions & 5 deletions jupyterlab_sql/executor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from sqlalchemy import create_engine
from sqlalchemy.sql import select, text, table
from sqlalchemy import create_engine, inspect
from sqlalchemy.pool import StaticPool
from sqlalchemy.sql import select, table, text

from .serializer import make_row_serializable
from .cache import Cache
from .connection_url import is_sqlite, is_mysql, has_database
from .serializer import make_row_serializable
from .models import DatabaseObjects


class InvalidConnectionUrl(Exception):
Expand All @@ -31,15 +32,20 @@ class Executor:
def __init__(self):
self._sqlite_engine_cache = Cache()

def get_table_names(self, connection_url):
def get_database_objects(self, connection_url):
if is_mysql(connection_url) and not has_database(connection_url):
raise InvalidConnectionUrl(
"You need to specify a database name in the connection "
"URL for MySQL databases. Use, for instance, "
"`mysql://localhost/employees`."
)
engine = self._get_engine(connection_url)
return engine.table_names()
inspector = inspect(engine)
database_objects = DatabaseObjects(
tables=inspector.get_table_names(),
views=inspector.get_view_names(),
)
return database_objects

def execute_query(self, connection_url, query):
engine = self._get_engine(connection_url)
Expand Down
8 changes: 5 additions & 3 deletions jupyterlab_sql/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ async def post(self):
with self.decoded_request() as connection_url:
ioloop = tornado.ioloop.IOLoop.current()
try:
tables = await ioloop.run_in_executor(
None, self.get_table_names, connection_url
database_objects = await ioloop.run_in_executor(
None, self._executor.get_database_objects, connection_url
)
response = responses.success_with_database_objects(
database_objects
)
response = responses.success_with_tables(tables)
except Exception as e:
response = responses.error(str(e))
return self.finish(json.dumps(response))
Expand Down
4 changes: 4 additions & 0 deletions jupyterlab_sql/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from collections import namedtuple


DatabaseObjects = namedtuple("DatabaseObjects", ["tables", "views"])
8 changes: 6 additions & 2 deletions jupyterlab_sql/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def success_no_rows():
return response


def success_with_tables(tables):
response = {"responseType": "success", "responseData": {"tables": tables}}
def success_with_database_objects(database_objects):
response_data = {
"tables": database_objects.tables,
"views": database_objects.views,
}
response = {"responseType": "success", "responseData": response_data}
return response
12 changes: 12 additions & 0 deletions jupyterlab_sql/tests/test_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from jupyterlab_sql.models import DatabaseObjects
from jupyterlab_sql.responses import success_with_database_objects


def test_success_with_database_objects():
database_objects = DatabaseObjects(tables=["t1", "t2"], views=["v1", "v2"])
response = success_with_database_objects(database_objects)
expected = {
"responseType": "success",
"responseData": {"tables": ["t1", "t2"], "views": ["v1", "v2"]},
}
assert response == expected
20 changes: 17 additions & 3 deletions src/api/databaseStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export async function getDatabaseStructure(
return data;
}

export interface DatabaseObjects {
tables: Array<string>;
views: Array<string>;
}

export namespace DatabaseStructureResponse {
export type Type = ErrorResponse | SuccessResponse;

Expand All @@ -33,6 +38,7 @@ export namespace DatabaseStructureResponse {

type SuccessResponseData = {
tables: Array<string>;
views?: Array<string>;
};

type ErrorResponseData = {
Expand All @@ -57,14 +63,22 @@ export namespace DatabaseStructureResponse {

export function match<U>(
response: Type,
onSuccess: (tables: Array<string>) => U,
onSuccess: (_: DatabaseObjects) => U,
onError: (_: ErrorResponseData) => U
) {
if (response.responseType === 'error') {
return onError(response.responseData);
} else if (response.responseType === 'success') {
const { tables } = response.responseData;
return onSuccess(tables);
const { responseData } = response;
const tables = responseData.tables;
// Backwards compatibility with server: views can be null or undefined.
// Remove in versions 4.x.
const views = responseData.views || [];
const databaseObjects: DatabaseObjects = {
tables,
views
};
return onSuccess(databaseObjects);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/databaseSummary/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ class Content extends SingletonPanel {
this._disposeWidgets();
Api.DatabaseStructureResponse.match(
response,
tables => {
const model = new DatabaseSummaryModel(tables);
({ tables, views }) => {
const model = new DatabaseSummaryModel([...tables, ...views]);
this.widget = DatabaseSummaryWidget.withModel(model);
model.navigateToCustomQuery.connect(() => {
this._customQueryClicked.emit(void 0);
Expand Down

0 comments on commit ec0d605

Please sign in to comment.