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

include list of views #104

Merged
merged 8 commits into from
Dec 31, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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