Skip to content

Commit

Permalink
Merge pull request #362 from koordinates/sqlserver
Browse files Browse the repository at this point in the history
Add support for SQL Server working copy
  • Loading branch information
olsen232 authored Mar 6, 2021
2 parents afa7eeb + 1c39cad commit 4f12a22
Show file tree
Hide file tree
Showing 19 changed files with 1,611 additions and 360 deletions.
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):
"""
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):
"""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):
"""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

0 comments on commit 4f12a22

Please sign in to comment.