Repository navigation
A boolean is a condition on a dialect without boolean values - #3147
Conversation
The `none` filter, the empty filter and a contradictory string filter compile to a constant, and so does `in` over an empty or null given. The constants were the keywords true and false written as strings, which a dialect without boolean values cannot run; the literals true and false already go through sqlBoolean, and now these do too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: James Estes <james.estes@credibledata.com>
… wants SQL has two places for a boolean, a condition (WHERE, ON, CASE WHEN, the operands of AND, OR and NOT) and a value, and on a dialect with no boolean type they do not mix: a comparison is legal only as a condition, a bit only as a value. The compiler carries the wanted form down the expression tree and crosses the boundary through the dialect, spells a literal in the wanted form, and writes Malloy's NULL rules for not, != and !~ through the dialect too. The translator no longer wraps a declared boolean in CASE. On every dialect with a boolean type the generated SQL is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: James Estes <james.estes@credibledata.com>
resultBoolean returns 1/0 for every booleanType but 'supported'; the matcher converted a number back to a boolean only for 'simulated'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: James Estes <james.estes@credibledata.com>
…es needs a value On a dialect with booleanType 'none' a boolean is a condition and never a value. A boolean as a result column, a function argument, a null test, in ??, in a cast, compared with = or !=, matched with a filter expression or as the result of a pick is an error at that expression, naming the value to write instead; select: * leaves a boolean field out with a warning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
…t boolean values Nothing converts a condition into a 1/0 or back: a declared boolean is its comparison, and the translator refuses a boolean where a value is wanted. Malloy's not, != and !~ are true where SQL says NULL; Dialect.sqlNot and sqlNullIsTrue spell that, as a condition on a dialect with booleanType 'none' and as today's COALESCE everywhere else. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
A comparison selected as a result column is a boolean value, which a dialect without boolean values refuses. The tests whose subject was the comparison now assert it through a pick on every dialect; the tests whose subject is a boolean column are gated on booleanType. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
|
This is a fun one, I was afraid the "no boolean value" would be a huge stress to the malloy internals but it is actually coming out pretty clean. Claude suggested some areas which are not quite done yet and here's what we came up with. I thought about making a WN, but instead the doc for this design is now on main as Places a boolean still becomes a value without an error
Error messages
Tests
Docs
|
…iew or case subject The refusal returns an error-typed value which its caller returns, so an enclosing expression stays quiet and one mistake logs one error, at the expression which made it. Two booleans compared with any operator, a boolean field named as the whole view, and a boolean case subject are refused too. The wildcard warning names a joined field by its path and never a measure; the null-test hint says what to write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
…alue A pick cannot tell false from NULL, and starts_with, ends_with, is_inf and is_nan are COALESCEd to false on purpose. The concat of a boolean is its own gated test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
…t it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
MySQL hands a boolean back as 0 or 1, so the null-input tests compare through resultBoolean as the tests they replace did, and the concat of a boolean keeps the trino and presto gate the inline case had. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
|
All eleven are in, on top of a merge of main, and CI is green. Places a boolean still becomes a value without an error
Error messages
Tests
Docs
One question on the doc. It says the translator-errors list covers every case, and items 1 and 3 add two it does not name: a boolean field as the whole view, and a boolean |
|
I'm ok with not updating the "spec" .. those two don't need mentioning. i'm in final review stages, will merge if it clears. |
|
CI passed and I had Claude do a clean re-review, and, annoyingly, it found more things. We are close, I am sorry to keep doing this but I do want these changes.
|
…a raw function Any comparison with a boolean operand, including a boolean among the choices of an in-list, is refused at the boolean side. The raw function path checks its arguments as the overloaded path does. Unary minus logs its type error, so a filter on it no longer goes quiet. The hints live in one module, and the refusal tests check the error code and where it landed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1vFteHTo61YyRRVVrJAn4 Signed-off-by: James Estes <james.estes@credibledata.com>
|
All six are in, and since each pass has found a site of the same shape, I went looking for more before pushing.
Looking for more of the same I probed about forty more expressions on the One more hole, same rule as item 1 by a different path: the choices of an Item 2 is one site, not a class. Of the fifteen places the translator returns an error value without Ruled out, each already handled somewhere:
One thing I noticed that is not about booleans: |
|
Thanks for the thorough pass, and for chasing down the One last request. After that I'll land it. There are more places a boolean can still slip through as a value, and we'll close those in a follow-up of our own instead of another round here. |
ExprMinus rebuilt an error-typed operand as a number, so whatever used the result complained again; it now returns the operand unchanged, and the error-type-inference table says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HXm9dCuXFseFX4FS2DK1My Signed-off-by: James Estes <james.estes@credibledata.com>
|
Done in 80ff7df: |
On a dialect with
booleanType: 'none', which is SQL Server, a boolean is a condition and never a value. Where a program needs a boolean value, the translator reports an error at that expression and names the value to write instead. This is the design inpackages/malloy/src/doc/boolean-none.md(#3166). The first commit is #3146, which this PR includes.What compiles
where:,having:, a joinon:,pick … when, a filtered aggregate, and the operands ofand,or,not.dimension: is_late is dep_delay > 15is a condition, andwhere: is_latewritesWHERE dep_delay > 15. The translator'sCASE … THEN 1 ELSE 0 ENDrewrite of a declared boolean (field-declaration.ts) is gone.trueandfalse, written(1=1)and(1=0).not,!=,!~Malloy's negative forms are true where SQL would say NULL. Two methods on the base
Dialect, keyed onbooleanTypeand not meant to be overridden, spell them:sqlNotandsqlNullIsTrue. Every other dialect keepsCOALESCE.'none'dialectnot c(CASE WHEN c THEN 1 ELSE 0 END = 0)a != b,a !~ b(CASE WHEN NOT (c) THEN 1 ELSE 0 END = 0), wherecis the positive comparisonTranslator errors
One check,
booleanValueErrorinlang/ast/expressions/boolean-value.ts, called where a boolean would become a value. It logs the error and hands back an error-typed value, which the caller returns, so the enclosing expressions stay quiet and one mistake is one error, at the expression which made it. The sites:select:,group_by:,aggregate:,calculate:andindex:, whether a declaration or a reference, and a boolean field named as the whole view (m -> is_late,m -> by_carrier + is_late);is nullandis not nullwith a boolean operand;is_active = 1,size > is_small), plain, partial, or in an alternation including a boolean among the choices of aninlist; one check before theequality()/compare()dispatch covers all but theinlist;??with a boolean operand;pickorcasewhose result is a boolean, and a booleancasesubject;The message is
'sqlserver' does not support a boolean as a result column; write the value, pick 'yes' when is_late else 'no', with the expression the user wrote in the hint where there is one.select: *andindex: *leave a boolean field out with a warning that names it by its full path (o.is_lateforo.*); a measure is never named, since*never included one.Tests
lang/test/boolean-none.spec.ts: each condition site translates, each value site is refused by its error code at the marked expression, a nested mistake logs one error,select: *warns, and the same model on a dialect with booleans translates. The lang harness gains a_mssql_connection, whose table has no boolean column since nothing read from a SQL Server schema is one, andmakeModelFuncshifts a${…}mark past itsprefixso the location check works with one.dialect.spec.tspins the two spellings on both kinds of dialect; the SQL Server spec runs a declared boolean inwhere:andhaving:,notand!=on NULL, a boolean function as a condition,sql_boolean, and the refusal of a declared boolean ingroup_by:.rand,pi,greatest,least,lag,starts_with,ends_with,is_inf,is_nan,sql_boolean, parenthesisation, and six date, timestamp and timezone comparisons). They now assert the same fact through apick, on every dialect; what each asserts is unchanged. Apickcannot tellfalsefrom NULL, sostarts_with,ends_with,is_infandis_nan, which areCOALESCEd tofalseon purpose, keep a null-input-is-false test wherebooleanType !== 'none'. Four harness tests that build a boolean column, two that group by one, andconcatof a boolean literal are gated the same way.What changes, and what does not
Generated SQL compared against
mainon duckdb, postgres, bigquery, snowflake, trino, presto, mysql and databricks over a 68-query corpus covering every site above: identical on all eight.On SQL Server 2022 and 2017, every query that failed with server error 4145, a boolean where a condition is expected, now passes, and no other test changes state. What remains is nesting, regular expressions, arrays and records,
all()/exclude(), and a few engine facts (aFLOATholds no NaN;GREATESTignores NULL; abitreads back as a number), each its own conversation.