Skip to content

Commit 1d25b61

Browse files
committed
Initial commit
0 parents  commit 1d25b61

File tree

9 files changed

+647
-0
lines changed

9 files changed

+647
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.mypy_cache
2+
.pytest_cache
3+
*.log
4+
dist/
5+
*.egg-info
6+
__pycache__/

.pre-commit-config.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
exclude: .*\.snap
2+
repos:
3+
- repo: https://github.com/pre-commit/pre-commit-hooks
4+
rev: v3.1.0
5+
hooks:
6+
- id: check-yaml
7+
- id: check-json
8+
- id: check-merge-conflict
9+
- id: debug-statements
10+
- id: check-case-conflict
11+
- id: check-toml
12+
- id: end-of-file-fixer
13+
- id: trailing-whitespace
14+
- repo: https://github.com/timothycrosley/isort
15+
rev: 5.4.2
16+
hooks:
17+
- id: isort
18+
args:
19+
- --float-to-top
20+
- repo: https://github.com/humitos/mirrors-autoflake
21+
rev: v1.3
22+
hooks:
23+
- id: autoflake
24+
args:
25+
- --in-place
26+
- --remove-all-unused-imports
27+
- repo: https://github.com/asottile/pyupgrade
28+
rev: v2.7.0
29+
hooks:
30+
- id: pyupgrade
31+
args:
32+
- --py3-plus
33+
- repo: https://github.com/psf/black
34+
rev: b59a524
35+
hooks:
36+
- id: black
37+
- repo: https://github.com/pre-commit/mirrors-mypy
38+
rev: v0.782
39+
hooks:
40+
- id: mypy
41+
- repo: https://gitlab.com/pycqa/flake8
42+
rev: '' # pick a git hash / tag to point to
43+
hooks:
44+
- id: flake8
45+
additional_dependencies:
46+
- flake8-aaa
47+
args:
48+
- --jobs=1
49+
- --extend-ignore=W503,E203,E501

duckdb_engine/__init__.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import re
2+
from dataclasses import dataclass
3+
from typing import List
4+
5+
import duckdb
6+
from sqlalchemy.dialects.postgresql import dialect
7+
8+
9+
class DBAPI:
10+
paramstyle = "qmark"
11+
12+
class Error(Exception):
13+
pass
14+
15+
16+
DNE = re.compile(r"Catalog: ([^ ]+) with name ([^ ]+) does not exist!")
17+
18+
19+
@dataclass
20+
class ConnectionWrapper:
21+
c: duckdb.DuckDBPyConnection
22+
23+
def cursor(self):
24+
return self
25+
26+
@property
27+
def connection(self):
28+
return self
29+
30+
def close(self):
31+
pass
32+
33+
@property
34+
def rowcount(self):
35+
return self.c.rowcount
36+
37+
def execute(self, statement, parameters):
38+
try:
39+
self.c.execute(statement, parameters)
40+
self.c.commit()
41+
return self
42+
43+
except Exception as e:
44+
m = DNE.match(e.args[0])
45+
if m:
46+
raise Exception(m.groups())
47+
raise e
48+
49+
def fetchone(self):
50+
return self.c.fetchone()
51+
52+
def commit(self):
53+
self.c.commit()
54+
55+
description: List[str] = []
56+
57+
notices: List[str] = []
58+
59+
60+
class Dialect(dialect):
61+
_has_events = False
62+
identifier_preparer = None
63+
64+
def connect(self, *args, **kwargs):
65+
return ConnectionWrapper(duckdb.connect(*args, **kwargs))
66+
67+
def on_connect(self):
68+
pass
69+
70+
def do_execute(self, cursor, statement, parameters, context):
71+
if "FOREIGN" in statement:
72+
# doesn't support foreign key constraints
73+
statement = statement.strip().splitlines()
74+
statement = [line for line in statement if "FOREIGN" not in line]
75+
statement[-2] = statement[-2].strip().strip(",")
76+
statement = "\n".join(statement)
77+
78+
super().do_execute(cursor, statement, parameters, context)
79+
80+
# if "CREATE SEQUENCE" in statement:
81+
# seque = statement.split(" ")[2].strip('"')
82+
# print(cursor.execute(f"SELECT nextval('{seque}');", ()).fetchone())
83+
84+
def has_table(self, connection, table_name, schema=None):
85+
return False
86+
87+
def has_sequence(self, connection, sequence_name, schema=None):
88+
return False
89+
90+
def has_type(self, connection, type_name, schema=None):
91+
return False
92+
93+
@staticmethod
94+
def dbapi():
95+
return DBAPI
96+
97+
def create_connect_args(self, u):
98+
return (), {"database": str(u).split("///")[1]}
99+
100+
def initialize(self, connection):
101+
pass
102+
103+
def do_rollback(self, connection):
104+
pass
105+
106+
@classmethod
107+
def get_dialect_cls(cls, u):
108+
return cls

duckdb_engine/conftest.py

Whitespace-only changes.

duckdb_engine/tests/__init__.py

Whitespace-only changes.

duckdb_engine/tests/conftest.py

Whitespace-only changes.

duckdb_engine/tests/test_basic.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from pytest import fixture
2+
from sqlalchemy import Column, Integer, Sequence, String, create_engine
3+
from sqlalchemy.engine.url import registry
4+
from sqlalchemy.ext.declarative import declarative_base
5+
from sqlalchemy.orm import sessionmaker
6+
7+
8+
@fixture
9+
def engine():
10+
registry.register("duckdb", "duckdb_engine", "Dialect")
11+
12+
ewng = create_engine("duckdb:///:memory:")
13+
Base.metadata.create_all(ewng)
14+
return ewng
15+
16+
17+
Base = declarative_base()
18+
19+
20+
class FakeModel(Base): # type: ignore
21+
__tablename__ = "fake"
22+
23+
id = Column(Integer, Sequence("fakemodel_id_sequence"), primary_key=True)
24+
name = Column(String)
25+
26+
27+
@fixture
28+
def Session(engine):
29+
return sessionmaker(bind=engine)
30+
31+
32+
@fixture
33+
def session(Session):
34+
return Session()
35+
36+
37+
def test_basic(session):
38+
session.add(FakeModel(name="Frank"))
39+
session.commit()
40+
41+
frank = session.query(FakeModel).one() # act
42+
43+
assert frank.name == "Frank"

0 commit comments

Comments
 (0)