diff --git a/CHANGES.md b/CHANGES.md index 22b2f830..736da75d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ # Changelog ## Unreleased +- BREAKING: Types: The generic `Float` without a precision now compiles to + `DOUBLE` instead of `FLOAT`, CrateDB's 4-byte `REAL`, which rounded Python + floats to 24 bits. This matches SQLAlchemy's documented behaviour on + backends that distinguish the two. `Float(precision<=24)`, `REAL` and the + uppercase `FLOAT` type still render 4-byte columns - Types: Fixed `CLOB`, `NCHAR`, `NVARCHAR`, `DATETIME`, and `DATE` compiling to type names CrateDB cannot parse. They now map to `STRING`, `CHAR`, `VARCHAR`, and `TIMESTAMP` respectively, matching their generic lower-case counterparts diff --git a/src/sqlalchemy_cratedb/compiler.py b/src/sqlalchemy_cratedb/compiler.py index 3856dc68..15e4a83a 100644 --- a/src/sqlalchemy_cratedb/compiler.py +++ b/src/sqlalchemy_cratedb/compiler.py @@ -328,6 +328,20 @@ def visit_FLOAT(self, type_, **kw): return "DOUBLE" return "FLOAT" + def visit_float(self, type_, **kw): + """ + The generic `Float` without a precision is an 8-byte `DOUBLE`. + + CrateDB's `FLOAT` is the 4-byte `REAL`, which rounds Python floats to + 24 bits of precision. SQLAlchemy documents a precision-less `Float` as + 8-byte on backends that distinguish the two, such as PostgreSQL and + MSSQL. A 4-byte column is still available through `Float(precision=24)` + or lower, `REAL`, or the uppercase `FLOAT` type. + """ + if type_.precision is None: + return "DOUBLE" + return self.visit_FLOAT(type_, **kw) + def visit_JSON(self, type_, **kw): return "OBJECT" diff --git a/tests/compiler_test.py b/tests/compiler_test.py index cfcf360d..18d47d29 100644 --- a/tests/compiler_test.py +++ b/tests/compiler_test.py @@ -23,12 +23,14 @@ from unittest import TestCase, mock, skipIf from unittest.mock import MagicMock, patch +import pytest import sqlalchemy as sa from crate.client.cursor import Cursor from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.sql import Update, text from sqlalchemy_cratedb.compiler import crate_before_execute +from sqlalchemy_cratedb.dialect import CrateDialect from tests.settings import crate_host from tests.util import ExtraAssertions @@ -605,3 +607,26 @@ def test_ddl_with_jsonb_columns(self): """), ) + + +FLOAT_DDL_CASES = [ + (sa.Float(), "DOUBLE"), + (sa.Float(asdecimal=True), "DOUBLE"), + (sa.Float(precision=53), "DOUBLE"), + (sa.Float(precision=25), "DOUBLE"), + (sa.Float(precision=24), "FLOAT"), + (sa.Float(precision=10), "FLOAT"), + (sa.FLOAT(), "FLOAT"), + (sa.REAL(), "REAL"), +] +# `sa.Double` is only available on SQLAlchemy>=2.0. +if hasattr(sa, "Double"): + FLOAT_DDL_CASES.append((sa.Double(), "DOUBLE")) + + +@pytest.mark.parametrize("type_,expected", FLOAT_DDL_CASES) +def test_float_ddl(type_, expected): + """ + A precision-less generic `Float` is 8-byte; 4-byte types stay available. + """ + assert type_.compile(dialect=CrateDialect()) == expected diff --git a/tests/create_table_test.py b/tests/create_table_test.py index e0fc7b45..8123a83f 100644 --- a/tests/create_table_test.py +++ b/tests/create_table_test.py @@ -73,7 +73,7 @@ class User(self.Base): "\n\tshort_col SHORT, " "\n\tdatetime_col TIMESTAMP WITHOUT TIME ZONE, " "\n\tdate_col TIMESTAMP, " - "\n\tfloat_col FLOAT, " + "\n\tfloat_col DOUBLE, " "\n\tdecimal_col NUMERIC(10, 2), " "\n\tPRIMARY KEY (string_col)\n)\n\n" ),