Skip to content

Commit a6a6801

Browse files
author
James Robinson
authored
Fix jinjasql+sqla vs object attribute dereferencing notation at jinjasql level. (#80)
* Fix jinjasql+sqla vs object attribute dereferencing notation at jinjasql level. * Switch to use poetry to run flake8, black. isort, safety so that CICD and local dev uses same linter versions.
1 parent 62f0582 commit a6a6801

File tree

6 files changed

+193
-70
lines changed

6 files changed

+193
-70
lines changed

.github/workflows/main.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,36 @@ jobs:
4343
python-version: ${{ matrix.python-version }}
4444
cache: poetry
4545

46-
- name: Install tox
47-
run: pip install tox
46+
- name: Install dependencies
47+
shell: bash
48+
run: poetry install
4849

4950
- name: Install CockroachDB
5051
uses: ./.github/actions/setup-cockroachdb
5152

5253
# Run all of the tests
5354
- name: Flake8 - Style guide enforcement
54-
run: tox -e flake8
55+
run: poetry run flake8 noteable_magics/ tests/ --count --show-source --statistics --benchmark
5556
if: ${{ matrix.python-version == 3.9 }}
5657

5758
- name: Black - Format check
58-
run: tox -e black-check
59+
run: poetry run black --diff noteable_magics/ tests/
5960
if: ${{ matrix.python-version == 3.9 }}
6061

6162
- name: Isort - Import format check
62-
run: tox -e isort-check
63+
run: poetry run isort --diff --check noteable_magics/ tests/
6364
if: ${{ matrix.python-version == 3.9 }}
6465

6566
- name: Safety - Python dependency vulnerability check
66-
run: tox -e safety
67+
run: poetry export --without-hashes -f requirements.txt | poetry run safety check --stdin -i 51457 -i 51668
6768
if: ${{ matrix.python-version == 3.9 }}
6869

6970
- name: Set env
7071
id: vars
7172
run: PY_VER=${{ matrix.python-version }}; echo ::set-output name=tox_py::${PY_VER//.}
7273

74+
- name: Install tox for running tests with
75+
run: pip install tox
76+
7377
- name: Pytest - Unit tests
7478
run: tox -e py${{ steps.vars.outputs.tox_py }}

noteable_magics/sql/run.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,31 @@ def _commit(conn, config):
112112
pass # not all engines can commit
113113

114114

115-
jinja_sql = JinjaSql(param_style='named')
115+
jinja_sql = JinjaSql(param_style='numeric')
116+
117+
##
118+
# Why we use JinjaSql in numeric mode when feeding into SQLA text() expressions.
119+
#
120+
# We originally used 'named' format, which worked great in combination with SQLA text() parameter
121+
# syntax, up until trying to do object attribute dereferencing in jinja expressions
122+
# ("select id from foo where id = {{myobj.id}}"), at which time the resulting de-templated query
123+
# and bind params would be "select id from foo where id = :myobj.id" and {'myobj.id': 1}, which was
124+
# at least self-consistent until SQLA text() tries to convert that into the database driver's preferred parameter format
125+
# (say, pyformat) and would end up with *closing the parens too early* due to seeing the non-identifier
126+
# token '.': "select id from foo where id = %(myobj)s.id" with bind dict {'myobj.id': 1}, and croak
127+
# due to not finding exactly 'myobj' as key in the bind dict (had it made it past that, the query
128+
# still would have failed because the trailing '.id' would have been presented to the DB, which was never
129+
# the intent).
130+
#
131+
# So, to ensure that we pass only brain-dead-simple paramater expansion needs into the SQL text() expression,
132+
# we now drive jinja in numeric mode, so that it replaces templated variable expressions with just
133+
# a colon and a number ("select id from foo where id = :1"). It hands us back a list instead of a dict
134+
# as the bind parameters, but is easy enough to transform that list [12] into the dict of
135+
# string keys -> values ({'1': 12}) which SQLA's text() + conn.execute() expect for such a text() template.
136+
#
137+
# People don't much combine JinjaSQL on top of SQLA, in that they solve 'conditional composed SQL'
138+
# completely differently. We're special I guess?
139+
##
116140

117141

118142
def run(conn, sql, config, user_namespace, skip_boxing_scalar_result: bool):
@@ -123,9 +147,14 @@ def run(conn, sql, config, user_namespace, skip_boxing_scalar_result: bool):
123147
if first_word == "begin":
124148
raise Exception("ipython_sql does not support transactions")
125149

126-
query, bind_params = jinja_sql.prepare_query(statement, user_namespace)
150+
query, bind_list = jinja_sql.prepare_query(statement, user_namespace)
151+
152+
# Convert bind_list from positional list to dict per needs of a paramaterized text()
153+
# construct.
154+
bind_dict = {str(idx + 1): elem for (idx, elem) in enumerate(bind_list)}
155+
127156
txt = sqlalchemy.sql.text(query)
128-
result = conn.session.execute(txt, bind_params)
157+
result = conn.session.execute(txt, bind_dict)
129158

130159
_commit(conn=conn, config=config)
131160

poetry.lock

Lines changed: 130 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ setuptools = "^65.3.0"
5656
tox = "^3.23.1"
5757
managed-service-fixtures = "^0.1.5"
5858
requests-mock = "^1.10.0"
59+
safety = "^2.3.5"
5960

6061

6162
[build-system]

tests/test_sql_magic.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,25 @@ def test_basic_query(self, sql_magic, ipython_shell, a_value, expected_b_value):
220220
assert isinstance(results, int)
221221
assert results == expected_b_value
222222

223+
def test_query_dereferencing_object_attribute(self, sql_magic, ipython_shell):
224+
class MyClass:
225+
id: int
226+
227+
def __init__(self, id: int):
228+
self.id = id
229+
230+
mc = MyClass(1)
231+
232+
ipython_shell.user_ns['mc'] = mc
233+
234+
## jinjasql expansion including object attribute dereference!
235+
results = sql_magic.execute(
236+
COCKROACH_HANDLE + '\n#scalar select b from int_table where a = {{mc.id}}'
237+
)
238+
239+
assert isinstance(results, int)
240+
assert results == 2
241+
223242
def test_insert_into(self, sql_magic, ipython_shell, capsys):
224243
"""Create a one-off table, then test inserting into from python variables interpolated by jinja"""
225244

0 commit comments

Comments
 (0)