Coverage for gws-app/gws/lib/cql/builder.py: 97%
223 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Build database expressions from CQL2 parse trees."""
3from typing import Any, cast
5import operator
7import gws
8import gws.lib.crs
9import gws.lib.datetimex as dtx
10import gws.lib.sa as sa
12from .parser import Node, C
15class BuildError(Exception):
16 """A parse tree node or function is not supported by the builder, or its arguments are invalid."""
18 pass
21class Builder:
22 """Walks a CQL2 parse tree and dispatches nodes to handler methods.
24 Node types go to ``build_<type>`` methods, standard functions to ``func_<name>`` methods
25 (names are lowercased). This class implements no node types; subclasses add handlers.
26 """
28 def get_method(self, name):
29 """Find a handler method.
31 Args:
32 name: Method name, case-insensitive.
34 Returns:
35 The method, or ``None`` if the builder does not implement it.
36 """
38 return getattr(self, name.lower(), None)
40 def build(self, e):
41 """Build an expression from a parse tree node.
43 Calls ``build_<type>`` with the node arguments, or ``build_operator`` for operators without one.
45 Args:
46 e: Parse tree node.
48 Returns:
49 The expression returned by the handler.
51 Raises:
52 BuildError: If the node type is not implemented.
53 """
55 fn = self.get_method('build_' + e[0])
56 if fn:
57 return fn(e[1:])
59 if e[0] in C.OPERATORS:
60 return self.build_operator(e[0], e[1:])
62 raise BuildError(f'CQL: node {e[0]!r} not implemented')
64 def build_operator(self, op, args):
65 """Build an operator expression.
67 Args:
68 op: Operator, e.g. ``>`` or ``+``.
69 args: Operand nodes.
71 Returns:
72 The expression. Not implemented in this class.
74 Raises:
75 BuildError: Always, in this class.
76 """
78 raise BuildError(f'CQL: operator {op!r} not implemented')
80 def build_function(self, args):
81 """Build a standard function call by calling ``func_<name>`` with the argument nodes.
83 Args:
84 args: Function name, followed by the argument nodes.
86 Returns:
87 The expression returned by the handler.
89 Raises:
90 BuildError: If the function is not implemented.
91 """
93 # [FUNCTION, name, arg1, arg2, ...]
95 fn = self.get_method('func_' + args[0])
96 if fn:
97 return fn(args[1:])
99 raise BuildError(f'CQL: function {args[0]!r} not implemented')
101 def build_user_function(self, args):
102 """Build a non-standard function call. Subclasses handle their own functions here.
104 Args:
105 args: Function name as written, followed by the argument nodes.
107 Returns:
108 The expression. Not implemented in this class.
110 Raises:
111 BuildError: Always, in this class.
112 """
114 # [USER_FUNCTION, name, arg1, arg2, ...]
116 raise BuildError(f'CQL: function {args[0]!r} not implemented')
118 def value(self, e) -> Any:
119 """Unwrap a literal node into a plain python value.
121 Args:
122 e: Literal node. Array nodes are unwrapped recursively.
124 Returns:
125 The value, a list for arrays.
127 Raises:
128 BuildError: If the node is not a literal.
129 """
131 if e[0] == Node.ARRAY:
132 return [self.value(a) for a in e[1:]]
133 if e[0] in C.LITERALS:
134 return e[1]
135 raise BuildError(f'CQL: expected a literal, got {e[0]!r}')
138class SqlBuilder(Builder):
139 """Builds SQLAlchemy expressions for a postgis table.
141 Names refer to table columns, literals become bound parameters, geometries are in WGS84.
142 All standard CQL2 functions are implemented, user functions are not.
143 """
145 _binary_ops = {
146 '>': operator.gt,
147 '<': operator.lt,
148 '>=': operator.ge,
149 '<=': operator.le,
150 '=': operator.eq,
151 '!=': operator.ne,
152 '<>': operator.ne,
153 '*': operator.mul,
154 '/': operator.truediv,
155 '+': operator.add,
156 '-': operator.sub,
157 '%': operator.mod,
158 # sqlalchemy columns don't support '**'
159 '^': sa.func.power,
160 }
162 def __init__(self, table: sa.Table):
163 """Create a builder.
165 Args:
166 table: Table whose columns are referenced by names.
167 """
168 self.table = table
170 def build_operator(self, op, args):
171 fn = self._binary_ops.get(op)
172 if fn:
173 a, b = args
174 return fn(self.build(a), self.build(b))
176 return super().build_operator(op, args)
178 def build_name(self, args):
179 """Build a column reference.
181 Args:
182 args: Name parts, only the first is used as the column name.
184 Returns:
185 The table column.
187 Raises:
188 BuildError: If the table has no such column.
189 """
190 col = self.table.c.get(args[0])
191 if col is None:
192 raise BuildError(f'CQL: unknown column {args[0]!r}')
193 return col
195 def build_array(self, args):
196 """Build an array.
198 Args:
199 args: Element nodes.
201 Returns:
202 A list of expressions.
203 """
204 return [self.build(a) for a in args]
206 def build_bool(self, args):
207 """Build a boolean literal.
209 Args:
210 args: A list with the value.
212 Returns:
213 A bound parameter.
214 """
215 return self.literal(args[0])
217 def build_float(self, args):
218 """Build a float literal.
220 Args:
221 args: A list with the value.
223 Returns:
224 A bound parameter.
225 """
226 return self.literal(args[0])
228 def build_int(self, args):
229 """Build a integer literal.
231 Args:
232 args: A list with the value.
234 Returns:
235 A bound parameter.
236 """
237 return self.literal(args[0])
239 def build_string(self, args):
240 """Build a string literal.
242 Args:
243 args: A list with the value.
245 Returns:
246 A bound parameter.
247 """
248 return self.literal(args[0])
250 def build_date(self, args):
251 """Build a date literal.
253 Args:
254 args: A list with the date value.
256 Returns:
257 A ``DATE`` expression.
258 """
259 return sa.cast(args[0], sa.DATE())
261 def build_timestamp(self, args):
262 """Build a timestamp literal.
264 Args:
265 args: A list with the datetime value.
267 Returns:
268 A ``timestamptz`` expression.
269 """
270 return sa.cast(args[0], sa.TIMESTAMP(timezone=True))
272 def build_wkt(self, args):
273 """Build a geometry literal.
275 Args:
276 args: A list with the WKT string.
278 Returns:
279 A geometry in WGS84.
280 """
281 return sa.func.ST_GeomFromText(args[0], gws.lib.crs.WGS84.srid)
283 def build_bbox(self, args):
284 """Build a bounding box geometry.
286 Args:
287 args: Four numbers: min x, min y, max x, max y.
289 Returns:
290 A rectangle geometry in WGS84.
291 """
292 minx, miny, maxx, maxy = args
293 return sa.func.ST_MakeEnvelope(minx, miny, maxx, maxy, gws.lib.crs.WGS84.srid)
295 ##
297 def build_and(self, args):
298 """Build a conjunction.
300 Args:
301 args: Operand nodes.
303 Returns:
304 An ``AND`` expression.
305 """
306 return sa.and_(*[cast(sa.BinaryExpression, self.build(a)) for a in args])
308 def build_or(self, args):
309 """Build a disjunction.
311 Args:
312 args: Operand nodes.
314 Returns:
315 An ``OR`` expression.
316 """
317 return sa.or_(*[cast(sa.BinaryExpression, self.build(a)) for a in args])
319 def build_not(self, args):
320 """Build a negation.
322 Args:
323 args: A list with the operand node.
325 Returns:
326 A ``NOT`` expression.
327 """
328 return sa.not_(cast(sa.BinaryExpression, self.build(args[0])))
330 def build_between(self, args):
331 """Build a ``BETWEEN`` predicate.
333 Args:
334 args: The value, lower bound and upper bound nodes.
336 Returns:
337 A ``BETWEEN`` expression.
338 """
339 col = self.build(args[0])
340 a = self.build(args[1])
341 b = self.build(args[2])
342 return col.between(a, b)
344 def build_not_between(self, args):
345 """Build a ``NOT BETWEEN`` predicate.
347 Args:
348 args: The value, lower bound and upper bound nodes.
350 Returns:
351 A negated ``BETWEEN`` expression.
352 """
353 return sa.not_(self.build_between(args))
355 def build_in(self, args):
356 """Build an ``IN`` predicate.
358 Args:
359 args: The value node, followed by the list item nodes.
361 Returns:
362 An ``IN`` expression.
363 """
364 col = self.build(args[0])
365 ls = [self.build(a) for a in args[1:]]
366 return col.in_(ls)
368 def build_not_in(self, args):
369 """Build a ``NOT IN`` predicate.
371 Args:
372 args: The value node, followed by the list item nodes.
374 Returns:
375 A negated ``IN`` expression.
376 """
377 return sa.not_(self.build_in(args))
379 def build_like(self, args):
380 """Build a ``LIKE`` predicate.
382 Args:
383 args: The value and pattern nodes.
385 Returns:
386 A ``LIKE`` expression.
387 """
388 col = self.build(args[0])
389 return col.like(self.build(args[1]))
391 def build_not_like(self, args):
392 """Build a ``NOT LIKE`` predicate.
394 Args:
395 args: The value and pattern nodes.
397 Returns:
398 A negated ``LIKE`` expression.
399 """
400 return sa.not_(self.build_like(args))
402 def build_is_null(self, args):
403 """Build an ``IS NULL`` predicate.
405 Args:
406 args: A list with the value node.
408 Returns:
409 An ``IS NULL`` expression.
410 """
411 col = self.build(args[0])
412 return col.is_(None)
414 def build_not_null(self, args):
415 """Build an ``IS NOT NULL`` predicate.
417 Args:
418 args: A list with the value node.
420 Returns:
421 An ``IS NOT NULL`` expression.
422 """
423 col = self.build(args[0])
424 return col.isnot(None)
426 ##
428 def func_s_intersects(self, args):
429 """Build the ``S_INTERSECTS`` spatial predicate.
431 Args:
432 args: Two geometry nodes.
434 Returns:
435 An ``ST_Intersects`` expression.
436 """
437 return sa.func.ST_Intersects(self.build(args[0]), self.build(args[1]))
439 def func_s_contains(self, args):
440 """Build the ``S_CONTAINS`` spatial predicate.
442 Args:
443 args: Two geometry nodes.
445 Returns:
446 An ``ST_Contains`` expression.
447 """
448 return sa.func.ST_Contains(self.build(args[0]), self.build(args[1]))
450 def func_s_crosses(self, args):
451 """Build the ``S_CROSSES`` spatial predicate.
453 Args:
454 args: Two geometry nodes.
456 Returns:
457 An ``ST_Crosses`` expression.
458 """
459 return sa.func.ST_Crosses(self.build(args[0]), self.build(args[1]))
461 def func_s_disjoint(self, args):
462 """Build the ``S_DISJOINT`` spatial predicate.
464 Args:
465 args: Two geometry nodes.
467 Returns:
468 An ``ST_Disjoint`` expression.
469 """
470 return sa.func.ST_Disjoint(self.build(args[0]), self.build(args[1]))
472 def func_s_equals(self, args):
473 """Build the ``S_EQUALS`` spatial predicate.
475 Args:
476 args: Two geometry nodes.
478 Returns:
479 An ``ST_Equals`` expression.
480 """
481 return sa.func.ST_Equals(self.build(args[0]), self.build(args[1]))
483 def func_s_overlaps(self, args):
484 """Build the ``S_OVERLAPS`` spatial predicate.
486 Args:
487 args: Two geometry nodes.
489 Returns:
490 An ``ST_Overlaps`` expression.
491 """
492 return sa.func.ST_Overlaps(self.build(args[0]), self.build(args[1]))
494 def func_s_touches(self, args):
495 """Build the ``S_TOUCHES`` spatial predicate.
497 Args:
498 args: Two geometry nodes.
500 Returns:
501 An ``ST_Touches`` expression.
502 """
503 return sa.func.ST_Touches(self.build(args[0]), self.build(args[1]))
505 def func_s_within(self, args):
506 """Build the ``S_WITHIN`` spatial predicate.
508 Args:
509 args: Two geometry nodes.
511 Returns:
512 An ``ST_Within`` expression.
513 """
514 return sa.func.ST_Within(self.build(args[0]), self.build(args[1]))
516 ##
518 def func_casei(self, args):
519 """Build the ``CASEI`` function.
521 Args:
522 args: A string node.
524 Returns:
525 The lowercased value.
526 """
527 return sa.func.lower(self.build(args[0]))
529 def func_accenti(self, args):
530 """Build the ``ACCENTI`` function.
532 Args:
533 args: A string node.
535 Returns:
536 The value without accents, using the postgres ``unaccent`` extension.
537 """
538 return sa.func.unaccent(self.build(args[0]))
540 ##
542 def func_bbox(self, args):
543 """Build the ``BBOX`` function.
545 Args:
546 args: Four number literal nodes: min x, min y, max x, max y.
548 Returns:
549 A rectangle geometry in WGS84.
551 Raises:
552 BuildError: If an argument is not a literal.
553 """
554 return self.build_bbox([self.value(a) for a in args])
556 def func_timestamp(self, args):
557 """Build the ``TIMESTAMP`` function.
559 Args:
560 args: An ISO date-time string literal node.
562 Returns:
563 A ``timestamptz`` expression.
565 Raises:
566 BuildError: If the argument is not a literal.
567 """
568 dt = dtx.from_iso_string(self.value(args[0]), 'UTC')
569 return sa.cast(dt, sa.TIMESTAMP(timezone=True))
571 def func_date(self, args):
572 """Build the ``DATE`` function.
574 Args:
575 args: An ISO date string literal node.
577 Returns:
578 A ``DATE`` expression.
580 Raises:
581 BuildError: If the argument is not a literal.
582 """
583 dt = dtx.from_iso_string(self.value(args[0]), 'UTC')
584 return sa.cast(dt, sa.DATE())
586 def func_interval(self, args):
587 """Reject ``INTERVAL`` outside of temporal predicates.
589 Args:
590 args: Interval bound nodes.
592 Raises:
593 BuildError: Always.
594 """
595 raise BuildError('CQL: INTERVAL is only allowed in temporal predicates')
597 ##
599 def func_t_equals(self, args):
600 """Build the ``T_EQUALS`` temporal predicate.
602 Args:
603 args: Two temporal nodes, instants or intervals.
605 Returns:
606 A condition that is true if both ranges are equal.
607 """
608 a, b = self.temporal_pair(args)
609 return a == b
611 def func_t_after(self, args):
612 """Build the ``T_AFTER`` temporal predicate.
614 Args:
615 args: Two temporal nodes, instants or intervals.
617 Returns:
618 A condition that is true if the first range starts after the second ends.
619 """
620 a, b = self.temporal_pair(args)
621 return sa.func.lower(a) > sa.func.upper(b)
623 def func_t_before(self, args):
624 """Build the ``T_BEFORE`` temporal predicate.
626 Args:
627 args: Two temporal nodes, instants or intervals.
629 Returns:
630 A condition that is true if the first range ends before the second starts.
631 """
632 a, b = self.temporal_pair(args)
633 return sa.func.upper(a) < sa.func.lower(b)
635 def func_t_meets(self, args):
636 """Build the ``T_MEETS`` temporal predicate.
638 Args:
639 args: Two temporal nodes, instants or intervals.
641 Returns:
642 A condition that is true if the first range ends where the second starts.
643 """
644 a, b = self.temporal_pair(args)
645 return sa.func.upper(a) == sa.func.lower(b)
647 def func_t_metby(self, args):
648 """Build the ``T_METBY`` temporal predicate.
650 Args:
651 args: Two temporal nodes, instants or intervals.
653 Returns:
654 A condition that is true if the first range starts where the second ends.
655 """
656 a, b = self.temporal_pair(args)
657 return sa.func.lower(a) == sa.func.upper(b)
659 def func_t_during(self, args):
660 """Build the ``T_DURING`` temporal predicate.
662 Args:
663 args: Two temporal nodes, instants or intervals.
665 Returns:
666 A condition that is true if the first range lies strictly inside the second.
667 """
668 a, b = self.temporal_pair(args)
669 return sa.and_(
670 sa.func.lower(a) > sa.func.lower(b),
671 sa.func.upper(a) < sa.func.upper(b),
672 )
674 def func_t_contains(self, args):
675 """Build the ``T_CONTAINS`` temporal predicate.
677 Args:
678 args: Two temporal nodes, instants or intervals.
680 Returns:
681 A condition that is true if the second range lies strictly inside the first.
682 """
683 a, b = self.temporal_pair(args)
684 return sa.and_(
685 sa.func.lower(a) < sa.func.lower(b),
686 sa.func.upper(a) > sa.func.upper(b),
687 )
689 def func_t_overlaps(self, args):
690 """Build the ``T_OVERLAPS`` temporal predicate.
692 Args:
693 args: Two temporal nodes, instants or intervals.
695 Returns:
696 A condition that is true if the first range starts before the second and ends inside it.
697 """
698 a, b = self.temporal_pair(args)
699 return sa.and_(
700 sa.func.lower(a) < sa.func.lower(b),
701 sa.func.upper(a) > sa.func.lower(b),
702 sa.func.upper(a) < sa.func.upper(b),
703 )
705 def func_t_overlappedby(self, args):
706 """Build the ``T_OVERLAPPEDBY`` temporal predicate.
708 Args:
709 args: Two temporal nodes, instants or intervals.
711 Returns:
712 A condition that is true if the first range starts inside the second and ends after it.
713 """
714 a, b = self.temporal_pair(args)
715 return sa.and_(
716 sa.func.lower(a) > sa.func.lower(b),
717 sa.func.lower(a) < sa.func.upper(b),
718 sa.func.upper(a) > sa.func.upper(b),
719 )
721 def func_t_starts(self, args):
722 """Build the ``T_STARTS`` temporal predicate.
724 Args:
725 args: Two temporal nodes, instants or intervals.
727 Returns:
728 A condition that is true if both ranges start together and the first ends earlier.
729 """
730 a, b = self.temporal_pair(args)
731 return sa.and_(
732 sa.func.lower(a) == sa.func.lower(b),
733 sa.func.upper(a) < sa.func.upper(b),
734 )
736 def func_t_startedby(self, args):
737 """Build the ``T_STARTEDBY`` temporal predicate.
739 Args:
740 args: Two temporal nodes, instants or intervals.
742 Returns:
743 A condition that is true if both ranges start together and the first ends later.
744 """
745 a, b = self.temporal_pair(args)
746 return sa.and_(
747 sa.func.lower(a) == sa.func.lower(b),
748 sa.func.upper(a) > sa.func.upper(b),
749 )
751 def func_t_finishes(self, args):
752 """Build the ``T_FINISHES`` temporal predicate.
754 Args:
755 args: Two temporal nodes, instants or intervals.
757 Returns:
758 A condition that is true if both ranges end together and the first starts later.
759 """
760 a, b = self.temporal_pair(args)
761 return sa.and_(
762 sa.func.upper(a) == sa.func.upper(b),
763 sa.func.lower(a) > sa.func.lower(b),
764 )
766 def func_t_finishedby(self, args):
767 """Build the ``T_FINISHEDBY`` temporal predicate.
769 Args:
770 args: Two temporal nodes, instants or intervals.
772 Returns:
773 A condition that is true if both ranges end together and the first starts earlier.
774 """
775 a, b = self.temporal_pair(args)
776 return sa.and_(
777 sa.func.upper(a) == sa.func.upper(b),
778 sa.func.lower(a) < sa.func.lower(b),
779 )
781 def func_t_intersects(self, args):
782 """Build the ``T_INTERSECTS`` temporal predicate.
784 Args:
785 args: Two temporal nodes, instants or intervals.
787 Returns:
788 A condition that is true if the ranges have a common point.
789 """
790 a, b = self.temporal_pair(args)
791 return a.op('&&')(b)
793 def func_t_disjoint(self, args):
794 """Build the ``T_DISJOINT`` temporal predicate.
796 Args:
797 args: Two temporal nodes, instants or intervals.
799 Returns:
800 A condition that is true if the ranges have no common point.
801 """
802 a, b = self.temporal_pair(args)
803 return sa.not_(a.op('&&')(b))
805 ##
807 def func_a_equals(self, args):
808 """Build the ``A_EQUALS`` array predicate.
810 Args:
811 args: Two array nodes.
813 Returns:
814 A condition that is true if both arrays contain the same elements, ignoring order and duplicates.
815 """
816 a, b = self.array_pair(args)
817 return sa.and_(a.op('@>')(b), a.op('<@')(b))
819 def func_a_contains(self, args):
820 """Build the ``A_CONTAINS`` array predicate.
822 Args:
823 args: Two array nodes.
825 Returns:
826 A condition that is true if the first array contains all elements of the second.
827 """
828 a, b = self.array_pair(args)
829 return a.op('@>')(b)
831 def func_a_containedby(self, args):
832 """Build the ``A_CONTAINEDBY`` array predicate.
834 Args:
835 args: Two array nodes.
837 Returns:
838 A condition that is true if all elements of the first array are in the second.
839 """
840 a, b = self.array_pair(args)
841 return a.op('<@')(b)
843 def func_a_overlaps(self, args):
844 """Build the ``A_OVERLAPS`` array predicate.
846 Args:
847 args: Two array nodes.
849 Returns:
850 A condition that is true if the arrays have a common element.
851 """
852 a, b = self.array_pair(args)
853 return a.op('&&')(b)
855 ##
857 def temporal_pair(self, args):
858 """Coerce both operands of a temporal predicate to ranges.
860 Args:
861 args: Two temporal nodes.
863 Returns:
864 A tuple of two ``tstzrange`` expressions.
865 """
867 return self.temporal_range(args[0]), self.temporal_range(args[1])
869 def temporal_range(self, e):
870 """Coerce a temporal expression to a ``tstzrange``, an instant becomes a degenerate range.
872 A null bound is unbounded in postgres, therefore null inputs must yield a null range,
873 otherwise a null column would match everything.
875 Args:
876 e: An ``INTERVAL`` function node, or a node that builds a timestamp.
878 Returns:
879 A ``tstzrange`` expression with inclusive bounds, null if a bound is null.
880 """
882 if e[0] == Node.FUNCTION and e[1] == 'interval':
883 lo = self.temporal_bound(e[2], '-infinity')
884 hi = self.temporal_bound(e[3], 'infinity')
885 else:
886 lo = hi = self.timestamp_value(e)
888 return sa.case(
889 (sa.or_(lo.is_(None), hi.is_(None)), sa.null()),
890 else_=sa.func.tstzrange(lo, hi, '[]'),
891 )
893 def temporal_bound(self, e, unbounded):
894 """Build an interval bound.
896 Args:
897 e: Bound node. The string ``'..'`` means an open bound.
898 unbounded: Value for an open bound, ``-infinity`` or ``infinity``.
900 Returns:
901 A ``timestamptz`` expression.
902 """
904 if e[0] == Node.STRING and e[1] == '..':
905 return sa.cast(sa.literal(unbounded), sa.TIMESTAMP(timezone=True))
906 return self.timestamp_value(e)
908 def timestamp_value(self, e):
909 """Coerce an expression to a ``timestamptz``, naive values are assumed to be UTC.
911 Args:
912 e: Node.
914 Returns:
915 A ``timestamptz`` expression.
916 """
918 x = self.build(e)
919 typ = getattr(x, 'type', None)
920 if isinstance(typ, sa.TIMESTAMP) and typ.timezone:
921 return x
922 return sa.cast(sa.func.timezone('UTC', sa.cast(x, sa.TIMESTAMP())), sa.TIMESTAMP(timezone=True))
924 ##
926 def literal(self, val):
927 """Wrap a python value as a bound parameter.
929 Args:
930 val: Value.
932 Returns:
933 A literal expression.
934 """
936 return sa.literal(val)
938 ##
940 def array_pair(self, args):
941 """Coerce both operands of an array predicate to arrays.
943 Args:
944 args: Two array nodes.
946 Returns:
947 A tuple of two array expressions.
948 """
950 return self.array_operand(args[0]), self.array_operand(args[1])
952 def array_operand(self, e):
953 """Build an array expression, an array literal becoming a typed parameter.
955 Args:
956 e: Array literal or another node.
958 Returns:
959 An array expression.
960 """
962 if e[0] != Node.ARRAY:
963 return self.build(e)
964 vals = self.value(e)
965 return sa.literal(vals, sa.ARRAY(self.array_element_type(vals)))
967 def array_element_type(self, vals):
968 """Infer the element type of an array literal from its first element.
970 Args:
971 vals: Array values.
973 Returns:
974 An SQLAlchemy type, ``Text`` for an empty array or non-numeric values.
975 """
977 if not vals:
978 return sa.Text()
979 v = vals[0]
980 if isinstance(v, bool):
981 return sa.Boolean()
982 if isinstance(v, int):
983 return sa.Integer()
984 if isinstance(v, float):
985 return sa.Float()
986 return sa.Text()