-
Notifications
You must be signed in to change notification settings - Fork 14.4k
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
Conversation
use it for storing shared queries
f595ee3
to
0d7ab39
Compare
fc01575
to
412bb17
Compare
@@ -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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 thanks !
7eb71c6
to
17fb74e
Compare
17fb74e
to
7719e5a
Compare
def upgrade(): | ||
### commands auto generated by Alembic - please adjust! ### | ||
op.create_table('keyvalue', | ||
sa.Column('created_on', sa.DateTime(), nullable=True), |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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""" |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ^^
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>/") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
b6eb346
to
3c037f5
Compare
d2671d4
to
42946f5
Compare
className="btn btn-primary btn-xs" | ||
<button | ||
className="btn btn-link btn-xs" | ||
onClick={this.openQuery.bind(this, q.schema, q.dbId, q.sql)} |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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?
42946f5
to
70622fc
Compare
const queryLink = window.location.pathname + '?' + queryString; | ||
getShortUrl(queryLink, callback); | ||
const sharedQuery = { | ||
dbId: qe.dbId, |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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 ?
4106b0a
to
537f2ba
Compare
$.ajax({ | ||
type: 'GET', | ||
url: `/kv/${urlId}`, | ||
success(data) { |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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...
537f2ba
to
d64131c
Compare
} | ||
} | ||
popNewTab(newQuery) { | ||
queryCount++; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
d64131c
to
766a76a
Compare
return Response( | ||
__("Error: data can not be empty"), status=404) | ||
base_view = request.form.get('baseView') | ||
if not base_view: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto, assertEquals
766a76a
to
570e8b2
Compare
url: '/kv/store/', | ||
async: false, | ||
data: { | ||
baseView: 'Superset.sqllab', |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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
LGTM once @ascott comment is resolved |
570e8b2
to
1d77b69
Compare
1d77b69
to
d509e95
Compare
LGTM 🚢 |
* 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
Issue:
Solution:
needs-review @ascott @mistercrunch @bkyryliuk