sqlalchemy-hardcharger 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlalchemy_hardcharger/__init__.py +65 -0
- sqlalchemy_hardcharger/_version.py +1 -0
- sqlalchemy_hardcharger/alembic.py +127 -0
- sqlalchemy_hardcharger/dialect.py +1469 -0
- sqlalchemy_hardcharger/provision.py +54 -0
- sqlalchemy_hardcharger/py.typed +0 -0
- sqlalchemy_hardcharger/requirements.py +114 -0
- sqlalchemy_hardcharger/types.py +860 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/METADATA +480 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/RECORD +15 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/WHEEL +5 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/entry_points.txt +3 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/licenses/LICENSE +201 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/licenses/NOTICE +9 -0
- sqlalchemy_hardcharger-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""SQLAlchemy dialect for Hardcharger, GizmoData's DuckDB 2.0 server.
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import create_engine, text
|
|
4
|
+
|
|
5
|
+
engine = create_engine("hardcharger://user:password@localhost:31337?tls=false")
|
|
6
|
+
with engine.connect() as conn:
|
|
7
|
+
print(conn.execute(text("SELECT 42 AS answer")).scalar())
|
|
8
|
+
|
|
9
|
+
Installing the package registers the ``hardcharger://`` URL scheme with SQLAlchemy.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
13
|
+
|
|
14
|
+
from ._version import __version__
|
|
15
|
+
from .dialect import HardchargerConnection, HardchargerDialect
|
|
16
|
+
from .types import (
|
|
17
|
+
BIGNUM,
|
|
18
|
+
BIT,
|
|
19
|
+
GEOMETRY,
|
|
20
|
+
HUGEINT,
|
|
21
|
+
TIME_NS,
|
|
22
|
+
TIMESTAMP_MS,
|
|
23
|
+
TIMESTAMP_NS,
|
|
24
|
+
TIMESTAMP_S,
|
|
25
|
+
TINYINT,
|
|
26
|
+
UBIGINT,
|
|
27
|
+
UHUGEINT,
|
|
28
|
+
UINTEGER,
|
|
29
|
+
USMALLINT,
|
|
30
|
+
UTINYINT,
|
|
31
|
+
VARIANT,
|
|
32
|
+
Map,
|
|
33
|
+
Struct,
|
|
34
|
+
Union,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try: # Alembic is optional: register the migration support only where it is installed.
|
|
38
|
+
from . import alembic as _alembic # noqa: F401
|
|
39
|
+
except ImportError: # pragma: no cover
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"BIGNUM",
|
|
44
|
+
"BIT",
|
|
45
|
+
"GEOMETRY",
|
|
46
|
+
"HUGEINT",
|
|
47
|
+
"TIMESTAMP_MS",
|
|
48
|
+
"TIMESTAMP_NS",
|
|
49
|
+
"TIMESTAMP_S",
|
|
50
|
+
"TIME_NS",
|
|
51
|
+
"TINYINT",
|
|
52
|
+
"UBIGINT",
|
|
53
|
+
"UHUGEINT",
|
|
54
|
+
"UINTEGER",
|
|
55
|
+
"USMALLINT",
|
|
56
|
+
"UTINYINT",
|
|
57
|
+
"VARIANT",
|
|
58
|
+
"HardchargerConnection",
|
|
59
|
+
"HardchargerDialect",
|
|
60
|
+
"Map",
|
|
61
|
+
"Struct",
|
|
62
|
+
"Union",
|
|
63
|
+
"__version__",
|
|
64
|
+
"insert",
|
|
65
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Alembic support: tells Alembic how to run migrations on the ``hardcharger`` dialect.
|
|
2
|
+
|
|
3
|
+
Importing ``sqlalchemy_hardcharger`` registers this when Alembic is installed, so a migration
|
|
4
|
+
environment needs no extra line. DuckDB runs DDL inside transactions, so each migration is
|
|
5
|
+
applied atomically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from alembic.ddl.base import ColumnComment, format_column_name, format_table_name
|
|
14
|
+
from alembic.ddl.impl import DefaultImpl
|
|
15
|
+
from sqlalchemy import exc, schema, text
|
|
16
|
+
from sqlalchemy import types as sqltypes
|
|
17
|
+
from sqlalchemy.ext.compiler import compiles
|
|
18
|
+
|
|
19
|
+
from .dialect import SEQUENCE_COMMENT_PREFIX
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HardchargerImpl(DefaultImpl):
|
|
23
|
+
__dialect__ = "hardcharger"
|
|
24
|
+
transactional_ddl = True
|
|
25
|
+
|
|
26
|
+
# -- autogenerate ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
def _stored_type(self, type_: Any) -> str:
|
|
29
|
+
"""The type as DuckDB keeps it, as text: what a declared type turns into once a table is
|
|
30
|
+
created (VARCHAR has no length, TEXT is VARCHAR, NUMERIC is DECIMAL, ...)."""
|
|
31
|
+
rendered = self.dialect.type_compiler_instance.process(type_).upper()
|
|
32
|
+
rendered = re.sub(r"\s+", "", rendered)
|
|
33
|
+
rendered = re.sub(r"\b(?:VAR)?CHAR(?:\(\d+\))?", "VARCHAR", rendered)
|
|
34
|
+
rendered = re.sub(r"\bTEXT\b", "VARCHAR", rendered)
|
|
35
|
+
rendered = re.sub(r"\bNUMERIC\b", "DECIMAL", rendered)
|
|
36
|
+
rendered = re.sub(r"\bDECIMAL(?!\()", "DECIMAL(18,3)", rendered)
|
|
37
|
+
rendered = re.sub(r"\bFLOAT\b", "REAL", rendered)
|
|
38
|
+
return rendered
|
|
39
|
+
|
|
40
|
+
def compare_type(self, inspector_column: Any, metadata_column: Any) -> bool:
|
|
41
|
+
"""True when the column's type in the database differs from the declared one."""
|
|
42
|
+
database, declared = inspector_column.type, metadata_column.type
|
|
43
|
+
if isinstance(declared, sqltypes.Enum) and declared.native_enum:
|
|
44
|
+
# A named enum reflects as its labels: the labels are what can differ.
|
|
45
|
+
return not (
|
|
46
|
+
isinstance(database, sqltypes.Enum) and list(database.enums) == list(declared.enums)
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
return self._stored_type(type_=database) != self._stored_type(type_=declared)
|
|
50
|
+
except exc.CompileError:
|
|
51
|
+
return False # a type that can't be written as DDL can't be compared either
|
|
52
|
+
|
|
53
|
+
def correct_for_autogen_constraints(
|
|
54
|
+
self,
|
|
55
|
+
conn_uniques: Any,
|
|
56
|
+
conn_indexes: Any,
|
|
57
|
+
metadata_unique_constraints: Any,
|
|
58
|
+
metadata_indexes: Any,
|
|
59
|
+
) -> None:
|
|
60
|
+
# DuckDB names constraints itself and doesn't keep a declared name, so a unique
|
|
61
|
+
# constraint over the same columns is the same constraint, whatever its name.
|
|
62
|
+
by_columns = {tuple(c.name for c in found.columns): found for found in conn_uniques}
|
|
63
|
+
for declared in metadata_unique_constraints:
|
|
64
|
+
found = by_columns.get(tuple(c.name for c in declared.columns))
|
|
65
|
+
if found is not None:
|
|
66
|
+
found.name = declared.name
|
|
67
|
+
|
|
68
|
+
# -- operations -----------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def drop_table(self, table: Any, **kw: Any) -> None:
|
|
71
|
+
# The table's key may be numbered by a sequence the dialect created with it. Alembic
|
|
72
|
+
# describes a table to drop by name alone (and the table may have been renamed since),
|
|
73
|
+
# so ask the database which sequences are this table's own before the table goes.
|
|
74
|
+
sequences = [] if self.as_sql else self._own_sequences(table=table)
|
|
75
|
+
super().drop_table(table, **kw)
|
|
76
|
+
for sequence in sequences:
|
|
77
|
+
self._exec(text(f"DROP SEQUENCE IF EXISTS {sequence}"))
|
|
78
|
+
|
|
79
|
+
def _own_sequences(self, table: Any) -> list[str]:
|
|
80
|
+
"""Sequences the dialect made for this table's columns, that no other table uses."""
|
|
81
|
+
connection, dialect = self.connection, self.dialect
|
|
82
|
+
if connection is None:
|
|
83
|
+
return []
|
|
84
|
+
try:
|
|
85
|
+
defaults = {
|
|
86
|
+
column["default"]
|
|
87
|
+
for column in dialect.get_columns(connection, table.name, schema=table.schema)
|
|
88
|
+
if column["default"]
|
|
89
|
+
}
|
|
90
|
+
except exc.NoSuchTableError:
|
|
91
|
+
return []
|
|
92
|
+
where, params = dialect._scope(schema=table.schema) # type: ignore[attr-defined]
|
|
93
|
+
marked = connection.execute(
|
|
94
|
+
text(f"SELECT sequence_name FROM duckdb_sequences() WHERE {where} AND comment LIKE :p"),
|
|
95
|
+
{**params, "p": f"{SEQUENCE_COMMENT_PREFIX}%"},
|
|
96
|
+
).scalars()
|
|
97
|
+
preparer = dialect.identifier_preparer
|
|
98
|
+
found = []
|
|
99
|
+
for name in marked:
|
|
100
|
+
sequence = schema.Sequence(name, schema=table.schema)
|
|
101
|
+
default = "nextval(%s)" % preparer.format_sequence_literal(sequence=sequence) # type: ignore[attr-defined]
|
|
102
|
+
if default not in defaults:
|
|
103
|
+
continue
|
|
104
|
+
others = connection.execute(
|
|
105
|
+
text(
|
|
106
|
+
f"SELECT count(*) FROM duckdb_columns() WHERE {where} "
|
|
107
|
+
"AND column_default = :hc_default AND lower(table_name) <> lower(:hc_table)"
|
|
108
|
+
),
|
|
109
|
+
{**params, "hc_default": default, "hc_table": table.name},
|
|
110
|
+
).scalar()
|
|
111
|
+
if not others:
|
|
112
|
+
found.append(preparer.format_sequence(sequence))
|
|
113
|
+
return found
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@compiles(ColumnComment, "hardcharger")
|
|
117
|
+
def _column_comment(element: Any, compiler: Any, **kw: Any) -> str:
|
|
118
|
+
comment = (
|
|
119
|
+
"NULL"
|
|
120
|
+
if element.comment is None
|
|
121
|
+
else compiler.sql_compiler.render_literal_value(element.comment, sqltypes.String())
|
|
122
|
+
)
|
|
123
|
+
return "COMMENT ON COLUMN %s.%s IS %s" % (
|
|
124
|
+
format_table_name(compiler, element.table_name, element.schema),
|
|
125
|
+
format_column_name(compiler, element.column_name),
|
|
126
|
+
comment,
|
|
127
|
+
)
|