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

Add support for SQL Server working copy #362

Merged
merged 2 commits into from
Mar 6, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sno/base_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class BaseDataset(ImportSource):
"""
Common interface for all datasets - mainly Dataset2, but
there is also Dataset0 and Dataset1 used by `sno upgrade`.

A Dataset instance is immutable since it is a view of a particular git tree.
To get a new version of a dataset, commit the desired changes,
then instantiate a new Dataset instance that references the new git tree.
"""

# Constants that subclasses should generally define.
Expand Down
4 changes: 2 additions & 2 deletions sno/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

def reset_wc_if_needed(repo, target_tree_or_commit, *, discard_changes=False):
"""Resets the working copy to the target if it does not already match, or if discard_changes is True."""
working_copy = WorkingCopy.get(repo, allow_uncreated=True)
working_copy = WorkingCopy.get(repo, allow_uncreated=True, allow_invalid_state=True)
if working_copy is None:
click.echo(
"(Bare sno repository - to create a working copy, use `sno create-workingcopy`)"
Expand Down Expand Up @@ -378,7 +378,7 @@ def create_workingcopy(ctx, discard_changes, wc_path):
wc_path = WorkingCopy.default_path(repo.workdir_path)

if wc_path != old_wc_path:
WorkingCopy.check_valid_creation_path(repo.workdir_path, wc_path)
WorkingCopy.check_valid_creation_path(wc_path, repo.workdir_path)

# Finished sanity checks - start work:
if old_wc and wc_path != old_wc_path:
Expand Down
2 changes: 1 addition & 1 deletion sno/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def clone(

if repo_path.exists() and any(repo_path.iterdir()):
raise InvalidOperation(f'"{repo_path}" isn\'t empty', param_hint="directory")
WorkingCopy.check_valid_creation_path(repo_path, wc_path)
WorkingCopy.check_valid_creation_path(wc_path, repo_path)

if not repo_path.exists():
repo_path.mkdir(parents=True)
Expand Down
23 changes: 21 additions & 2 deletions sno/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ def with_crs_id(self, crs_id):
crs_id_bytes = struct.pack("<i", crs_id)
return Geometry.of(self[:4] + crs_id_bytes + self[8:])

@property
def crs_id(self):
craigds marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns the CRS ID as it is embedded in the GPKG header - before the WKB.
Note that datasets V2 zeroes this field before committing,
so will return zero when called on Geometry where it has been zeroed.
"""
wkb_offset, is_le, crs_id = parse_gpkg_geom(self)
return crs_id

@classmethod
def from_wkt(cls, wkt):
return wkt_to_gpkg_geom(wkt)
Expand Down Expand Up @@ -290,17 +300,26 @@ def gpkg_geom_to_ogr(gpkg_geom, parse_crs=False):
return geom


def wkt_to_gpkg_geom(wkb, **kwargs):
ogr_geom = ogr.CreateGeometryFromWkt(wkb)
def wkt_to_gpkg_geom(wkt, **kwargs):
craigds marked this conversation as resolved.
Show resolved Hide resolved
"""Given a well-known-text string, returns a GPKG Geometry object."""
if wkt is None:
return None

ogr_geom = ogr.CreateGeometryFromWkt(wkt)
return ogr_to_gpkg_geom(ogr_geom, **kwargs)


def wkb_to_gpkg_geom(wkb, **kwargs):
craigds marked this conversation as resolved.
Show resolved Hide resolved
"""Given a well-known-binary bytestring, returns a GPKG Geometry object."""
if wkb is None:
return None

ogr_geom = ogr.CreateGeometryFromWkb(wkb)
return ogr_to_gpkg_geom(ogr_geom, **kwargs)


def hex_wkb_to_gpkg_geom(hex_wkb, **kwargs):
"""Given a hex-encoded well-known-binary bytestring, returns a GPKG Geometry object."""
if hex_wkb is None:
return None

Expand Down
2 changes: 1 addition & 1 deletion sno/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def init(

if repo_path.exists() and any(repo_path.iterdir()):
raise InvalidOperation(f'"{repo_path}" isn\'t empty', param_hint="directory")
WorkingCopy.check_valid_creation_path(repo_path, wc_path)
WorkingCopy.check_valid_creation_path(wc_path, repo_path)

if not repo_path.exists():
repo_path.mkdir(parents=True)
Expand Down
4 changes: 2 additions & 2 deletions sno/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def init_repository(
repo_root_path = repo_root_path.resolve()
cls._ensure_exists_and_empty(repo_root_path)
if not bare:
WorkingCopy.check_valid_creation_path(repo_root_path, wc_path)
WorkingCopy.check_valid_creation_path(wc_path, repo_root_path)

extra_args = []
if initial_branch is not None:
Expand Down Expand Up @@ -224,7 +224,7 @@ def clone_repository(
repo_root_path = repo_root_path.resolve()
cls._ensure_exists_and_empty(repo_root_path)
if not bare:
WorkingCopy.check_valid_creation_path(repo_root_path, wc_path)
WorkingCopy.check_valid_creation_path(wc_path, repo_root_path)

if bare:
sno_repo = cls._create_with_git_command(
Expand Down
61 changes: 61 additions & 0 deletions sno/sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import re
import socket
from urllib.parse import urlsplit, urlunsplit, urlencode, parse_qs


import sqlalchemy
from pysqlite3 import dbapi2 as sqlite
import psycopg2
Expand All @@ -6,6 +11,7 @@

from sno import spatialite_path
from sno.geometry import Geometry
from sno.exceptions import NotFound


def gpkg_engine(path):
Expand Down Expand Up @@ -95,7 +101,62 @@ def _on_connect(psycopg2_conn, connection_record):
geometry_type = new_type((r[0],), "GEOMETRY", _adapt_geometry_from_pg)
register_type(geometry_type, psycopg2_conn)

pgurl = _append_query_to_url(pgurl, {"fallback_application_name": "sno"})

engine = sqlalchemy.create_engine(pgurl, module=psycopg2)
sqlalchemy.event.listen(engine, "connect", _on_connect)

return engine


CANONICAL_SQL_SERVER_SCHEME = "mssql"
INTERNAL_SQL_SERVER_SCHEME = "mssql+pyodbc"


def sqlserver_engine(msurl):
url = urlsplit(msurl)
if url.scheme != CANONICAL_SQL_SERVER_SCHEME:
raise ValueError("Expecting mssql://")

# SQL server driver is fussy - doesn't like localhost, prefers 127.0.0.1
url_netloc = re.sub(r"\blocalhost\b", _replace_with_localhost, url.netloc)

url_query = _append_to_query(
url.query, {"driver": get_sqlserver_driver(), "Application Name": "sno"}
)

msurl = urlunsplit(
[INTERNAL_SQL_SERVER_SCHEME, url_netloc, url.path, url_query, ""]
)

engine = sqlalchemy.create_engine(msurl)
return engine


def get_sqlserver_driver():
import pyodbc

drivers = [
d for d in pyodbc.drivers() if re.search("SQL Server", d, flags=re.IGNORECASE)
]
if not drivers:
drivers = pyodbc.drivers()
if not drivers:
raise NotFound("SQL Server driver was not found")
return sorted(drivers)[-1] # Latest driver


def _replace_with_localhost(*args, **kwargs):
return socket.gethostbyname("localhost")


def _append_query_to_url(uri, new_query_dict):
url = urlsplit(uri)
url_query = _append_to_query(url.query, new_query_dict)
return urlunsplit([url.scheme, url.netloc, url.path, url_query, ""])


def _append_to_query(existing_query, new_query_dict):
query_dict = parse_qs(existing_query)
# ignore new keys if they're already set in the querystring
return urlencode({**new_query_dict, **query_dict})
Loading