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

closing a bunch of issues #53

Merged
merged 12 commits into from
Jan 2, 2023
33 changes: 33 additions & 0 deletions doc/connecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,39 @@ a flag with (-a|--connection_arguments)the connection string as a JSON string. S
%sql -a '{"timeout":10, "mode":"ro"}' sqlite:// SELECT * from work;
```

+++

## Connecting securely

**It is highly recommended** that you do not pass plain credentials.

```{code-cell} ipython3
%load_ext sql
```

One option is to use `getpass`, type your password, build your connection string and pass it to `%sql`:

```{code-cell} ipython3
from getpass import getpass
idomic marked this conversation as resolved.
Show resolved Hide resolved
```

```python
password = getpass()
connection_string = f"postgresql://user:{password}@localhost/database"
%sql $connection_string
```

+++

You may also set the `DATABASE_URL` environment variable, and `%sql` will automatically load it from there:
idomic marked this conversation as resolved.
Show resolved Hide resolved

```python
# without any args, %sql reads from DATABASE_URL
%sql
```

+++

## DSN connections

Alternately, you can store connection info in a configuration file, under a section name chosen to refer to your database.
Expand Down
39 changes: 39 additions & 0 deletions doc/howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,42 @@ Pass existing engines to `%sql`
%%sql
SELECT MYSUM(1, 2)
```

## Connect to a SQLite database with spaces

Currently, due to a limitation in the argument parser, it's not possible to directly connect to SQLite databases whose path contains spaces; however, you can do it by creating the engine first.
edublancas marked this conversation as resolved.
Show resolved Hide resolved

### Setup

```{code-cell} ipython3
%pip install jupysql --quiet
```

```{code-cell} ipython3
%load_ext sql
```

## Connect to db

```{code-cell} ipython3
from sqlalchemy import create_engine

engine = create_engine("sqlite:///my database.db")
```

Add some sample data:

```{code-cell} ipython3
import pandas as pd

_ = pd.DataFrame({"x": range(5)}).to_sql("numbers", engine)
```

```{code-cell} ipython3
%sql engine
```

```{code-cell} ipython3
%%sql
SELECT * FROM numbers
```
2 changes: 1 addition & 1 deletion src/sql/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def connection_list(cls):
template = " * {}"
else:
template = " {}"
result.append(template.format(engine_url.__repr__()))
result.append(template.format(repr(engine_url)))
return "\n".join(result)

@classmethod
Expand Down
1 change: 1 addition & 0 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def ip():
"CREATE TABLE author (first_name, last_name, year_of_death)",
"INSERT INTO author VALUES ('William', 'Shakespeare', 1616)",
"INSERT INTO author VALUES ('Bertold', 'Brecht', 1956)",
"CREATE TABLE empty_table (column INT, another INT)",
],
)
yield ip_session
Expand Down
4 changes: 3 additions & 1 deletion src/tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def sql_magic(ip):
("sqlite://", "", "", "sqlite://", None),
("SELECT * FROM TABLE", "", "SELECT * FROM TABLE\n", "", None),
("SELECT * FROM", "TABLE", "SELECT * FROM\nTABLE", "", None),
("my_var << SELECT * FROM table", "", "SELECT * FROM table\n", "", "my_var"),
("my_var << SELECT *", "FROM table", "SELECT *\nFROM table", "", "my_var"),
("[db]", "", "", "sqlite://", None),
],
Expand All @@ -26,7 +27,8 @@ def sql_magic(ip):
"connection-string",
"sql-query",
"sql-query-in-line-and-cell",
"parsed-var",
"parsed-var-single-line",
"parsed-var-multi-line",
"config",
],
)
Expand Down
15 changes: 15 additions & 0 deletions src/tests/test_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sys
from unittest.mock import Mock

from sqlalchemy.engine import Engine

from sql.connection import Connection


def test_password_isnt_displayed(monkeypatch):
idomic marked this conversation as resolved.
Show resolved Hide resolved
monkeypatch.setitem(sys.modules, "psycopg2", Mock())
monkeypatch.setattr(Engine, "connect", Mock())

Connection.from_connect_str("postgresql://user:topsecret@somedomain.com/db")

assert "topsecret" not in Connection.connection_list()
71 changes: 54 additions & 17 deletions src/tests/test_magic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
import os.path
import re
import tempfile
Expand All @@ -6,6 +7,7 @@
import pytest
from sqlalchemy import create_engine

from sql.connection import Connection
from conftest import runsql


Expand Down Expand Up @@ -179,25 +181,25 @@ def test_connection_args_double_quotes(ip):
# assert 'Shakespeare' in str(persisted)


@pytest.mark.parametrize("value", ["None", "0"])
def test_displaylimit_disabled(ip, value):
ip.run_line_magic("config", "SqlMagic.autolimit = None")

ip.run_line_magic("config", f"SqlMagic.displaylimit = {value}")
result = runsql(ip, "SELECT * FROM author;")

assert "Brecht" in result._repr_html_()
assert "Shakespeare" in result._repr_html_()


def test_displaylimit(ip):
ip.run_line_magic("config", "SqlMagic.autolimit = None")
ip.run_line_magic("config", "SqlMagic.displaylimit = None")
result = runsql(
ip,
"SELECT * FROM (VALUES ('apple'), ('banana'), ('cherry')) "
"AS Result ORDER BY 1;",
)
assert "apple" in result._repr_html_()
assert "banana" in result._repr_html_()
assert "cherry" in result._repr_html_()

ip.run_line_magic("config", "SqlMagic.displaylimit = 1")
result = runsql(
ip,
"SELECT * FROM (VALUES ('apple'), ('banana'), ('cherry')) "
"AS Result ORDER BY 1;",
)
assert "apple" in result._repr_html_()
assert "cherry" not in result._repr_html_()
result = runsql(ip, "SELECT * FROM author ORDER BY first_name;")

assert "Brecht" in result._repr_html_()
assert "Shakespeare" not in result._repr_html_()


def test_column_local_vars(ip):
Expand Down Expand Up @@ -368,14 +370,41 @@ def test_json_in_select(ip):
assert result == [('{"greeting": "Farewell sweet {person}"}',)]


def test_close_connection(ip):
def test_closed_connections_are_no_longer_listed(ip):
connections = runsql(ip, "%sql -l")
connection_name = list(connections)[0]
runsql(ip, f"%sql -x {connection_name}")
connections_afterward = runsql(ip, "%sql -l")
assert connection_name not in connections_afterward


def test_close_connection(ip, tmp_empty):
# open two connections
ip.run_cell("%sql sqlite:///one.db")
ip.run_cell("%sql sqlite:///two.db")

# close them
ip.run_cell("%sql -x sqlite:///one.db")
ip.run_cell("%sql --close sqlite:///two.db")

assert "sqlite:///one.db" not in Connection.connections
assert "sqlite:///two.db" not in Connection.connections


def test_column_names_visible(ip, tmp_empty):
res = ip.run_line_magic("sql", "SELECT * FROM empty_table")

assert "<th>column</th>" in res._repr_html_()
assert "<th>another</th>" in res._repr_html_()


@pytest.mark.xfail(reason="known parse @ parser.py error")
def test_sqlite_path_with_spaces(ip, tmp_empty):
idomic marked this conversation as resolved.
Show resolved Hide resolved
ip.run_cell("%sql sqlite:///some database.db")

assert Path("some database.db").is_file()


def test_pass_existing_engine(ip, tmp_empty):
ip.user_global_ns["my_engine"] = create_engine("sqlite:///my.db")
ip.run_line_magic("sql", " my_engine ")
Expand All @@ -396,9 +425,17 @@ def test_pass_existing_engine(ip, tmp_empty):

# theres some weird shared state with this one, moving it to the end
def test_autolimit(ip):
# test table has two rows
ip.run_line_magic("config", "SqlMagic.autolimit = 0")
result = runsql(ip, "SELECT * FROM test;")
assert len(result) == 2

# test table has two rows
ip.run_line_magic("config", "SqlMagic.autolimit = None")
result = runsql(ip, "SELECT * FROM test;")
assert len(result) == 2

# test setting autolimit to 1
ip.run_line_magic("config", "SqlMagic.autolimit = 1")
result = runsql(ip, "SELECT * FROM test;")
assert len(result) == 1