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

Use a key-value store model for sharing long queries #1951

Merged
merged 6 commits into from
Jan 27, 2017

Conversation

vera-liu
Copy link
Contributor

@vera-liu vera-liu commented Jan 11, 2017

Issue:

  • with extremely long queries, share query link would break as it exceeds maximal number of characters permitted in a url

Solution:

  • adding a new key-value store model for us to store text associated with IDs
  • store query string in key-value store, use id in search params in url
  • make asynchronous call to get query string after TabbedSqlEditors is mounted
  • added tests

needs-review @ascott @mistercrunch @bkyryliuk

@vera-liu vera-liu force-pushed the vliu_share_long_query branch 2 times, most recently from f595ee3 to 0d7ab39 Compare January 11, 2017 19:45
@vera-liu vera-liu changed the title Use a key-value store model for sharing long queries [WIP] Use a key-value store model for sharing long queries Jan 11, 2017
@vera-liu vera-liu force-pushed the vliu_share_long_query branch from fc01575 to 412bb17 Compare January 11, 2017 22:17
@@ -1747,7 +1775,7 @@ def tables(self, db_id, schema):
)
tables = [t for t in database.all_table_names(schema) if
self.datasource_access_by_name(database, t, schema=schema)]
views = [v for v in database.all_table_names(schema) if
views = [v for v in database.all_view_names(schema) if
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a minor typo, figured it's convenient just to change it here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thanks !

@vera-liu vera-liu force-pushed the vliu_share_long_query branch 2 times, most recently from 7eb71c6 to 17fb74e Compare January 11, 2017 22:47
@vera-liu vera-liu force-pushed the vliu_share_long_query branch from 17fb74e to 7719e5a Compare January 11, 2017 23:24
@vera-liu vera-liu changed the title [WIP] Use a key-value store model for sharing long queries Use a key-value store model for sharing long queries Jan 11, 2017
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('keyvalue',
sa.Column('created_on', sa.DateTime(), nullable=True),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need audit on it

@@ -210,6 +210,15 @@ class Url(Model, AuditMixinNullable):
url = Column(Text)


class KeyValue(Model, AuditMixinNullable):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think you need this: AuditMixinNullable

@@ -1104,6 +1104,34 @@ def ping():
return "OK"


class KV(BaseSupersetView):

"""used for storing and retrieving key value pairs"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/used/Used

obj = models.KeyValue(value=value)
db.session.add(obj)
db.session.commit()
return("http://{request.headers[Host]}/{base_url}?id={obj.id}".format(
Copy link
Member

@bkyryliuk bkyryliuk Jan 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can u add a comment what's happening here, I do not fully understand?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_url is for generalizing this model. If we directly return /kv/id like we do in shortener, sqllab redux app won't catch it on pre-rendering because url is not marking to /sqllab. Adding base_url here hooks it to the right url with the right redux app we want

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if it's safe to return it that way. urls in flask should be handled by url_for() function and redirects - via redirect function.
@mistercrunch ^^

Copy link
Contributor Author

@vera-liu vera-liu Jan 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoiding redirects is why we opened this PR in the first place, because we want to avoid including queries in url params. We return the url in the same pattern in shortener: https://github.com/airbnb/superset/blob/cdbd2f850706825f16a08afc40682237d6f6d54f/superset/views.py#L1127

Copy link
Member

@bkyryliuk bkyryliuk Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vera-liu - at least this is not very secure as headers can be spoofed, let's chat about the approach in person.

request=request, base_url=base_url, obj=obj))

@log_this
@expose("/<key_id>/")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get or post?

resp = self.client.post('/kv/store/', data=dict(data=json.dumps(data)))
kv = db.session.query(models.KeyValue).first()
kv_value = kv.value
assert 'this is a test' in kv_value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use self.assertIn

kv_value = kv.value
assert 'this is a test' in kv_value
kv_id = 'id={}'.format(kv.id)
assert kv_id in resp.data.decode('utf-8')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@@ -256,6 +256,19 @@ def test_shortner(self):
resp = self.client.post('/r/shortner/', data=data)
assert '/r/' in resp.data.decode('utf-8')

def test_kv(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test corner cases: with null base_url and null data

@vera-liu vera-liu force-pushed the vliu_share_long_query branch 2 times, most recently from b6eb346 to 3c037f5 Compare January 23, 2017 18:00
@vera-liu vera-liu force-pushed the vliu_share_long_query branch 2 times, most recently from d2671d4 to 42946f5 Compare January 24, 2017 23:44
className="btn btn-primary btn-xs"
<button
className="btn btn-link btn-xs"
onClick={this.openQuery.bind(this, q.schema, q.dbId, q.sql)}
Copy link

@ascott ascott Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openQuery should be called with the args in this order openQuery(dbId, schema, sql)
https://github.com/airbnb/superset/pull/1951/files#diff-080302e539cb3e2903e1456909802fddR44

@@ -28,12 +29,32 @@ let queryCount = 1;
class TabbedSqlEditors extends React.PureComponent {
constructor(props) {
super(props);
const uri = window.location.toString();
const cleanUri = '/superset/sqllab';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would sqlLabUrl make more sense than cleanUri here?

@vera-liu vera-liu force-pushed the vliu_share_long_query branch from 42946f5 to 70622fc Compare January 25, 2017 01:30
const queryLink = window.location.pathname + '?' + queryString;
getShortUrl(queryLink, callback);
const sharedQuery = {
dbId: qe.dbId,
Copy link
Member

@bkyryliuk bkyryliuk Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we store query object inside qe ?

Copy link
Contributor Author

@vera-liu vera-liu Jan 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryEditor is already pretty big, I don't think we need this temp query in redux store

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qe already has those variables as you pull them from it.
Question was will it make sense to package them into the query object this PR introduces this concept.

const query = search.substring(1);
if (search) {
const queryString = search.substring(1);
const urlId = getParamFromQuery(queryString, 'id');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we plan to do it generic, could u move this function to utils ?

@vera-liu vera-liu force-pushed the vliu_share_long_query branch 2 times, most recently from 4106b0a to 537f2ba Compare January 26, 2017 21:44
$.ajax({
type: 'GET',
url: `/kv/${urlId}`,
success(data) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could get rid of
const self = this;

if you use a fat arrow method for success

if (urlId) {
  $.ajax({
    type: 'GET',
    url: `/kv/${urlId}`,
    success: (data) => {
      this.popNewTab(JSON.parse(data));
    },
  });
}

const urlId = getParamFromQuery(queryString, 'id');
const self = this;
if (urlId) {
$.ajax({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to me that this fetching should probably be contained in an action...

@vera-liu vera-liu force-pushed the vliu_share_long_query branch from 537f2ba to d64131c Compare January 26, 2017 22:21
}
}
popNewTab(newQuery) {
queryCount++;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is queryCount used for?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

title: Untitled Query ${queryCount},

obj = models.KeyValue(value=value)
db.session.add(obj)
db.session.commit()
return url_for(base_view, id=obj.id, _external=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try / catch around url_for

@vera-liu vera-liu force-pushed the vliu_share_long_query branch from d64131c to 766a76a Compare January 26, 2017 22:57
return Response(
__("Error: data can not be empty"), status=404)
base_view = request.form.get('baseView')
if not base_view:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's not check on base_view and data validity, just try to fetch the value and return the exception message in case of error

base_view = request.form.get('baseView')
if not base_view:
return Response(
__("Error: please provide a base view"), status=404)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use json_error instead

kv = db.session.query(models.KeyValue).filter_by(id=key_id).one()
except Exception as e:
logging.exception(e)
return Response(
Copy link
Member

@bkyryliuk bkyryliuk Jan 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return json_error(utils.error_msg_from_exception(e), status=404)


value = {'data': 'this is a test'}
resp = self.client.post('/kv/store/', data=dict(data=json.dumps(value)))
self.assertEqual(resp.status_code, 200)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/ data={'data': 'this is a test'}

self.assertEqual(resp.status_code, 200)
kv = db.session.query(models.KeyValue).first()
kv_value = kv.value
self.assertIn('this is a test', kv_value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.assertEquals


resp = self.client.get('/kv/{}/'.format(kv.id))
self.assertEqual(resp.status_code, 200)
self.assertIn('this is a test', resp.data.decode('utf-8'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, assertEquals

@vera-liu vera-liu force-pushed the vliu_share_long_query branch from 766a76a to 570e8b2 Compare January 26, 2017 23:33
url: '/kv/store/',
async: false,
data: {
baseView: 'Superset.sqllab',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to pass baseView anymore with your new changes...

self.login(username='admin')

try:
resp = self.client.post('/kv/store/', data=dict())
Copy link
Member

@bkyryliuk bkyryliuk Jan 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resp her should be 500

@bkyryliuk
Copy link
Member

LGTM once @ascott comment is resolved

@vera-liu vera-liu force-pushed the vliu_share_long_query branch from 570e8b2 to 1d77b69 Compare January 27, 2017 00:22
@vera-liu vera-liu force-pushed the vliu_share_long_query branch from 1d77b69 to d509e95 Compare January 27, 2017 00:25
@ascott
Copy link

ascott commented Jan 27, 2017

LGTM 🚢

@vera-liu vera-liu merged commit 8580662 into apache:master Jan 27, 2017
SalehHindi pushed a commit to SalehHindi/superset that referenced this pull request Jun 9, 2017
* Add KeyValue model for storing id-value pairs
use it for storing shared queries

* Change string to text and added test

* Put getQueryLink in one place

* Changed migration down version

* Changes based on comments

* Update bcf3126872fc_add_keyvalue.py
@mistercrunch mistercrunch added 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 0.16.0 labels Feb 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 0.16.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants