Coverage for gws-app/gws/lib/cql/builder.py: 97%
225 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +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_neg(self, args):
196 """Build a negation.
198 Args:
199 args: A list with the operand node.
201 Returns:
202 The negated expression.
203 """
204 return -self.build(args[0])
206 def build_array(self, args):
207 """Build an array.
209 Args:
210 args: Element nodes.
212 Returns:
213 A list of expressions.
214 """
215 return [self.build(a) for a in args]
217 def build_bool(self, args):
218 """Build a boolean 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_float(self, args):
229 """Build a float 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_int(self, args):
240 """Build a integer 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_string(self, args):
251 """Build a string literal.
253 Args:
254 args: A list with the value.
256 Returns:
257 A bound parameter.
258 """
259 return self.literal(args[0])
261 def build_date(self, args):
262 """Build a date literal.
264 Args:
265 args: A list with the date value.
267 Returns:
268 A ``DATE`` expression.
269 """
270 return sa.cast(args[0], sa.DATE())
272 def build_timestamp(self, args):
273 """Build a timestamp literal.
275 Args:
276 args: A list with the datetime value.
278 Returns:
279 A ``timestamptz`` expression.
280 """
281 return sa.cast(args[0], sa.TIMESTAMP(timezone=True))
283 def build_wkt(self, args):
284 """Build a geometry literal.
286 Args:
287 args: A list with the WKT string.
289 Returns:
290 A geometry in WGS84.
291 """
292 return sa.func.ST_GeomFromText(args[0], gws.lib.crs.WGS84.srid)
294 def build_bbox(self, args):
295 """Build a bounding box geometry.
297 Args:
298 args: Four numbers: min x, min y, max x, max y.
300 Returns:
301 A rectangle geometry in WGS84.
302 """
303 minx, miny, maxx, maxy = args
304 return sa.func.ST_MakeEnvelope(minx, miny, maxx, maxy, gws.lib.crs.WGS84.srid)
306 ##
308 def build_and(self, args):
309 """Build a conjunction.
311 Args:
312 args: Operand nodes.
314 Returns:
315 An ``AND`` expression.
316 """
317 return sa.and_(*[cast(sa.BinaryExpression, self.build(a)) for a in args])
319 def build_or(self, args):
320 """Build a disjunction.
322 Args:
323 args: Operand nodes.
325 Returns:
326 An ``OR`` expression.
327 """
328 return sa.or_(*[cast(sa.BinaryExpression, self.build(a)) for a in args])
330 def build_not(self, args):
331 """Build a negation.
333 Args:
334 args: A list with the operand node.
336 Returns:
337 A ``NOT`` expression.
338 """
339 return sa.not_(cast(sa.BinaryExpression, self.build(args[0])))
341 def build_between(self, args):
342 """Build a ``BETWEEN`` predicate.
344 Args:
345 args: The value, lower bound and upper bound nodes.
347 Returns:
348 A ``BETWEEN`` expression.
349 """
350 col = self.build(args[0])
351 a = self.build(args[1])
352 b = self.build(args[2])
353 return col.between(a, b)
355 def build_not_between(self, args):
356 """Build a ``NOT BETWEEN`` predicate.
358 Args:
359 args: The value, lower bound and upper bound nodes.
361 Returns:
362 A negated ``BETWEEN`` expression.
363 """
364 return sa.not_(self.build_between(args))
366 def build_in(self, args):
367 """Build an ``IN`` predicate.
369 Args:
370 args: The value node, followed by the list item nodes.
372 Returns:
373 An ``IN`` expression.
374 """
375 col = self.build(args[0])
376 ls = [self.build(a) for a in args[1:]]
377 return col.in_(ls)
379 def build_not_in(self, args):
380 """Build a ``NOT IN`` predicate.
382 Args:
383 args: The value node, followed by the list item nodes.
385 Returns:
386 A negated ``IN`` expression.
387 """
388 return sa.not_(self.build_in(args))
390 def build_like(self, args):
391 """Build a ``LIKE`` predicate.
393 Args:
394 args: The value and pattern nodes.
396 Returns:
397 A ``LIKE`` expression.
398 """
399 col = self.build(args[0])
400 return col.like(self.build(args[1]))
402 def build_not_like(self, args):
403 """Build a ``NOT LIKE`` predicate.
405 Args:
406 args: The value and pattern nodes.
408 Returns:
409 A negated ``LIKE`` expression.
410 """
411 return sa.not_(self.build_like(args))
413 def build_is_null(self, args):
414 """Build an ``IS NULL`` predicate.
416 Args:
417 args: A list with the value node.
419 Returns:
420 An ``IS NULL`` expression.
421 """
422 col = self.build(args[0])
423 return col.is_(None)
425 def build_not_null(self, args):
426 """Build an ``IS NOT NULL`` predicate.
428 Args:
429 args: A list with the value node.
431 Returns:
432 An ``IS NOT NULL`` expression.
433 """
434 col = self.build(args[0])
435 return col.isnot(None)
437 ##
439 def func_s_intersects(self, args):
440 """Build the ``S_INTERSECTS`` spatial predicate.
442 Args:
443 args: Two geometry nodes.
445 Returns:
446 An ``ST_Intersects`` expression.
447 """
448 return sa.func.ST_Intersects(self.build(args[0]), self.build(args[1]))
450 def func_s_contains(self, args):
451 """Build the ``S_CONTAINS`` spatial predicate.
453 Args:
454 args: Two geometry nodes.
456 Returns:
457 An ``ST_Contains`` expression.
458 """
459 return sa.func.ST_Contains(self.build(args[0]), self.build(args[1]))
461 def func_s_crosses(self, args):
462 """Build the ``S_CROSSES`` spatial predicate.
464 Args:
465 args: Two geometry nodes.
467 Returns:
468 An ``ST_Crosses`` expression.
469 """
470 return sa.func.ST_Crosses(self.build(args[0]), self.build(args[1]))
472 def func_s_disjoint(self, args):
473 """Build the ``S_DISJOINT`` spatial predicate.
475 Args:
476 args: Two geometry nodes.
478 Returns:
479 An ``ST_Disjoint`` expression.
480 """
481 return sa.func.ST_Disjoint(self.build(args[0]), self.build(args[1]))
483 def func_s_equals(self, args):
484 """Build the ``S_EQUALS`` spatial predicate.
486 Args:
487 args: Two geometry nodes.
489 Returns:
490 An ``ST_Equals`` expression.
491 """
492 return sa.func.ST_Equals(self.build(args[0]), self.build(args[1]))
494 def func_s_overlaps(self, args):
495 """Build the ``S_OVERLAPS`` spatial predicate.
497 Args:
498 args: Two geometry nodes.
500 Returns:
501 An ``ST_Overlaps`` expression.
502 """
503 return sa.func.ST_Overlaps(self.build(args[0]), self.build(args[1]))
505 def func_s_touches(self, args):
506 """Build the ``S_TOUCHES`` spatial predicate.
508 Args:
509 args: Two geometry nodes.
511 Returns:
512 An ``ST_Touches`` expression.
513 """
514 return sa.func.ST_Touches(self.build(args[0]), self.build(args[1]))
516 def func_s_within(self, args):
517 """Build the ``S_WITHIN`` spatial predicate.
519 Args:
520 args: Two geometry nodes.
522 Returns:
523 An ``ST_Within`` expression.
524 """
525 return sa.func.ST_Within(self.build(args[0]), self.build(args[1]))
527 ##
529 def func_casei(self, args):
530 """Build the ``CASEI`` function.
532 Args:
533 args: A string node.
535 Returns:
536 The lowercased value.
537 """
538 return sa.func.lower(self.build(args[0]))
540 def func_accenti(self, args):
541 """Build the ``ACCENTI`` function.
543 Args:
544 args: A string node.
546 Returns:
547 The value without accents, using the postgres ``unaccent`` extension.
548 """
549 return sa.func.unaccent(self.build(args[0]))
551 ##
553 def func_bbox(self, args):
554 """Build the ``BBOX`` function.
556 Args:
557 args: Four number literal nodes: min x, min y, max x, max y.
559 Returns:
560 A rectangle geometry in WGS84.
562 Raises:
563 BuildError: If an argument is not a literal.
564 """
565 return self.build_bbox([self.value(a) for a in args])
567 def func_timestamp(self, args):
568 """Build the ``TIMESTAMP`` function.
570 Args:
571 args: An ISO date-time string literal node.
573 Returns:
574 A ``timestamptz`` expression.
576 Raises:
577 BuildError: If the argument is not a literal.
578 """
579 dt = dtx.from_iso_string(self.value(args[0]), 'UTC')
580 return sa.cast(dt, sa.TIMESTAMP(timezone=True))
582 def func_date(self, args):
583 """Build the ``DATE`` function.
585 Args:
586 args: An ISO date string literal node.
588 Returns:
589 A ``DATE`` expression.
591 Raises:
592 BuildError: If the argument is not a literal.
593 """
594 dt = dtx.from_iso_string(self.value(args[0]), 'UTC')
595 return sa.cast(dt, sa.DATE())
597 def func_interval(self, args):
598 """Reject ``INTERVAL`` outside of temporal predicates.
600 Args:
601 args: Interval bound nodes.
603 Raises:
604 BuildError: Always.
605 """
606 raise BuildError('CQL: INTERVAL is only allowed in temporal predicates')
608 ##
610 def func_t_equals(self, args):
611 """Build the ``T_EQUALS`` temporal predicate.
613 Args:
614 args: Two temporal nodes, instants or intervals.
616 Returns:
617 A condition that is true if both ranges are equal.
618 """
619 a, b = self.temporal_pair(args)
620 return a == b
622 def func_t_after(self, args):
623 """Build the ``T_AFTER`` temporal predicate.
625 Args:
626 args: Two temporal nodes, instants or intervals.
628 Returns:
629 A condition that is true if the first range starts after the second ends.
630 """
631 a, b = self.temporal_pair(args)
632 return sa.func.lower(a) > sa.func.upper(b)
634 def func_t_before(self, args):
635 """Build the ``T_BEFORE`` temporal predicate.
637 Args:
638 args: Two temporal nodes, instants or intervals.
640 Returns:
641 A condition that is true if the first range ends before the second starts.
642 """
643 a, b = self.temporal_pair(args)
644 return sa.func.upper(a) < sa.func.lower(b)
646 def func_t_meets(self, args):
647 """Build the ``T_MEETS`` temporal predicate.
649 Args:
650 args: Two temporal nodes, instants or intervals.
652 Returns:
653 A condition that is true if the first range ends where the second starts.
654 """
655 a, b = self.temporal_pair(args)
656 return sa.func.upper(a) == sa.func.lower(b)
658 def func_t_metby(self, args):
659 """Build the ``T_METBY`` temporal predicate.
661 Args:
662 args: Two temporal nodes, instants or intervals.
664 Returns:
665 A condition that is true if the first range starts where the second ends.
666 """
667 a, b = self.temporal_pair(args)
668 return sa.func.lower(a) == sa.func.upper(b)
670 def func_t_during(self, args):
671 """Build the ``T_DURING`` temporal predicate.
673 Args:
674 args: Two temporal nodes, instants or intervals.
676 Returns:
677 A condition that is true if the first range lies strictly inside the second.
678 """
679 a, b = self.temporal_pair(args)
680 return sa.and_(
681 sa.func.lower(a) > sa.func.lower(b),
682 sa.func.upper(a) < sa.func.upper(b),
683 )
685 def func_t_contains(self, args):
686 """Build the ``T_CONTAINS`` temporal predicate.
688 Args:
689 args: Two temporal nodes, instants or intervals.
691 Returns:
692 A condition that is true if the second range lies strictly inside the first.
693 """
694 a, b = self.temporal_pair(args)
695 return sa.and_(
696 sa.func.lower(a) < sa.func.lower(b),
697 sa.func.upper(a) > sa.func.upper(b),
698 )
700 def func_t_overlaps(self, args):
701 """Build the ``T_OVERLAPS`` temporal predicate.
703 Args:
704 args: Two temporal nodes, instants or intervals.
706 Returns:
707 A condition that is true if the first range starts before the second and ends inside it.
708 """
709 a, b = self.temporal_pair(args)
710 return sa.and_(
711 sa.func.lower(a) < sa.func.lower(b),
712 sa.func.upper(a) > sa.func.lower(b),
713 sa.func.upper(a) < sa.func.upper(b),
714 )
716 def func_t_overlappedby(self, args):
717 """Build the ``T_OVERLAPPEDBY`` temporal predicate.
719 Args:
720 args: Two temporal nodes, instants or intervals.
722 Returns:
723 A condition that is true if the first range starts inside the second and ends after it.
724 """
725 a, b = self.temporal_pair(args)
726 return sa.and_(
727 sa.func.lower(a) > sa.func.lower(b),
728 sa.func.lower(a) < sa.func.upper(b),
729 sa.func.upper(a) > sa.func.upper(b),
730 )
732 def func_t_starts(self, args):
733 """Build the ``T_STARTS`` temporal predicate.
735 Args:
736 args: Two temporal nodes, instants or intervals.
738 Returns:
739 A condition that is true if both ranges start together and the first ends earlier.
740 """
741 a, b = self.temporal_pair(args)
742 return sa.and_(
743 sa.func.lower(a) == sa.func.lower(b),
744 sa.func.upper(a) < sa.func.upper(b),
745 )
747 def func_t_startedby(self, args):
748 """Build the ``T_STARTEDBY`` temporal predicate.
750 Args:
751 args: Two temporal nodes, instants or intervals.
753 Returns:
754 A condition that is true if both ranges start together and the first ends later.
755 """
756 a, b = self.temporal_pair(args)
757 return sa.and_(
758 sa.func.lower(a) == sa.func.lower(b),
759 sa.func.upper(a) > sa.func.upper(b),
760 )
762 def func_t_finishes(self, args):
763 """Build the ``T_FINISHES`` temporal predicate.
765 Args:
766 args: Two temporal nodes, instants or intervals.
768 Returns:
769 A condition that is true if both ranges end together and the first starts later.
770 """
771 a, b = self.temporal_pair(args)
772 return sa.and_(
773 sa.func.upper(a) == sa.func.upper(b),
774 sa.func.lower(a) > sa.func.lower(b),
775 )
777 def func_t_finishedby(self, args):
778 """Build the ``T_FINISHEDBY`` temporal predicate.
780 Args:
781 args: Two temporal nodes, instants or intervals.
783 Returns:
784 A condition that is true if both ranges end together and the first starts earlier.
785 """
786 a, b = self.temporal_pair(args)
787 return sa.and_(
788 sa.func.upper(a) == sa.func.upper(b),
789 sa.func.lower(a) < sa.func.lower(b),
790 )
792 def func_t_intersects(self, args):
793 """Build the ``T_INTERSECTS`` temporal predicate.
795 Args:
796 args: Two temporal nodes, instants or intervals.
798 Returns:
799 A condition that is true if the ranges have a common point.
800 """
801 a, b = self.temporal_pair(args)
802 return a.op('&&')(b)
804 def func_t_disjoint(self, args):
805 """Build the ``T_DISJOINT`` temporal predicate.
807 Args:
808 args: Two temporal nodes, instants or intervals.
810 Returns:
811 A condition that is true if the ranges have no common point.
812 """
813 a, b = self.temporal_pair(args)
814 return sa.not_(a.op('&&')(b))
816 ##
818 def func_a_equals(self, args):
819 """Build the ``A_EQUALS`` array predicate.
821 Args:
822 args: Two array nodes.
824 Returns:
825 A condition that is true if both arrays contain the same elements, ignoring order and duplicates.
826 """
827 a, b = self.array_pair(args)
828 return sa.and_(a.op('@>')(b), a.op('<@')(b))
830 def func_a_contains(self, args):
831 """Build the ``A_CONTAINS`` array predicate.
833 Args:
834 args: Two array nodes.
836 Returns:
837 A condition that is true if the first array contains all elements of the second.
838 """
839 a, b = self.array_pair(args)
840 return a.op('@>')(b)
842 def func_a_containedby(self, args):
843 """Build the ``A_CONTAINEDBY`` array predicate.
845 Args:
846 args: Two array nodes.
848 Returns:
849 A condition that is true if all elements of the first array are in the second.
850 """
851 a, b = self.array_pair(args)
852 return a.op('<@')(b)
854 def func_a_overlaps(self, args):
855 """Build the ``A_OVERLAPS`` array predicate.
857 Args:
858 args: Two array nodes.
860 Returns:
861 A condition that is true if the arrays have a common element.
862 """
863 a, b = self.array_pair(args)
864 return a.op('&&')(b)
866 ##
868 def temporal_pair(self, args):
869 """Coerce both operands of a temporal predicate to ranges.
871 Args:
872 args: Two temporal nodes.
874 Returns:
875 A tuple of two ``tstzrange`` expressions.
876 """
878 return self.temporal_range(args[0]), self.temporal_range(args[1])
880 def temporal_range(self, e):
881 """Coerce a temporal expression to a ``tstzrange``, an instant becomes a degenerate range.
883 A null bound is unbounded in postgres, therefore null inputs must yield a null range,
884 otherwise a null column would match everything.
886 Args:
887 e: An ``INTERVAL`` function node, or a node that builds a timestamp.
889 Returns:
890 A ``tstzrange`` expression with inclusive bounds, null if a bound is null.
891 """
893 if e[0] == Node.FUNCTION and e[1] == 'interval':
894 lo = self.temporal_bound(e[2], '-infinity')
895 hi = self.temporal_bound(e[3], 'infinity')
896 else:
897 lo = hi = self.timestamp_value(e)
899 return sa.case(
900 (sa.or_(lo.is_(None), hi.is_(None)), sa.null()),
901 else_=sa.func.tstzrange(lo, hi, '[]'),
902 )
904 def temporal_bound(self, e, unbounded):
905 """Build an interval bound.
907 Args:
908 e: Bound node. The string ``'..'`` means an open bound.
909 unbounded: Value for an open bound, ``-infinity`` or ``infinity``.
911 Returns:
912 A ``timestamptz`` expression.
913 """
915 if e[0] == Node.STRING and e[1] == '..':
916 return sa.cast(sa.literal(unbounded), sa.TIMESTAMP(timezone=True))
917 return self.timestamp_value(e)
919 def timestamp_value(self, e):
920 """Coerce an expression to a ``timestamptz``, naive values are assumed to be UTC.
922 Args:
923 e: Node.
925 Returns:
926 A ``timestamptz`` expression.
927 """
929 x = self.build(e)
930 typ = getattr(x, 'type', None)
931 if isinstance(typ, sa.TIMESTAMP) and typ.timezone:
932 return x
933 return sa.cast(sa.func.timezone('UTC', sa.cast(x, sa.TIMESTAMP())), sa.TIMESTAMP(timezone=True))
935 ##
937 def literal(self, val):
938 """Wrap a python value as a bound parameter.
940 Args:
941 val: Value.
943 Returns:
944 A literal expression.
945 """
947 return sa.literal(val)
949 ##
951 def array_pair(self, args):
952 """Coerce both operands of an array predicate to arrays.
954 Args:
955 args: Two array nodes.
957 Returns:
958 A tuple of two array expressions.
959 """
961 return self.array_operand(args[0]), self.array_operand(args[1])
963 def array_operand(self, e):
964 """Build an array expression, an array literal becoming a typed parameter.
966 Args:
967 e: Array literal or another node.
969 Returns:
970 An array expression.
971 """
973 if e[0] != Node.ARRAY:
974 return self.build(e)
975 vals = self.value(e)
976 return sa.literal(vals, sa.ARRAY(self.array_element_type(vals)))
978 def array_element_type(self, vals):
979 """Infer the element type of an array literal from its first element.
981 Args:
982 vals: Array values.
984 Returns:
985 An SQLAlchemy type, ``Text`` for an empty array or non-numeric values.
986 """
988 if not vals:
989 return sa.Text()
990 v = vals[0]
991 if isinstance(v, bool):
992 return sa.Boolean()
993 if isinstance(v, int):
994 return sa.Integer()
995 if isinstance(v, float):
996 return sa.Float()
997 return sa.Text()