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.
@@ -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
+ )