@@ -91,12 +91,17 @@ configuration entry.
9191 comma-separated source list and every other join modifier — ` RIGHT ` , ` FULL ` , a
9292 bare ` OUTER ` , ` CROSS ` , ` NATURAL ` , ` SEMI ` , ` APPLY ` , ` STRAIGHT ` , ` GLOBAL ` , a
9393 join hint, and ` USING (...) ` — are rejected.
94- - Each join requires exactly one ` ON ` equality between a qualified column of the
95- joined source and a qualified column of a source introduced earlier. A left
96- join uses the same rule as an inner join.
94+ - Each join requires exactly one ` ON ` condition, which may be any boolean
95+ expression, including ` ON TRUE ` . Its column references, qualified or not,
96+ resolve against the sources introduced up to and including the joined source,
97+ so a reference to a source joined later is rejected, and its placeholders
98+ follow the same rules as a ` WHERE ` predicate. A left join uses the same rule
99+ as an inner join.
97100- Every column read from a left-joined source is nullable, even when the schema
98- declares it ` NOT NULL ` , because an unmatched row reads it as ` NULL ` . The base
99- source and every inner-joined source keep their schema nullability.
101+ declares it ` NOT NULL ` , because an unmatched row reads it as ` NULL ` . The join
102+ kind alone decides this, so a left join whose ` ON ` condition filters its
103+ joined rows reads them as nullable too. The base source and every
104+ inner-joined source keep their schema nullability.
100105- An alias replaces the table name as the exposed source name, so a qualified
101106 reference to an aliased table must use the alias and not the table name.
102107- Two sources may not expose the same name.
@@ -514,11 +519,11 @@ string literal, a quoted identifier, or a comment is not a parameter.
514519 assigned to, or inserted into.
515520- A placeholder written as the direct operand of ` ::type ` or
516521 ` CAST(... AS type) ` is typed by that cast instead, wherever it appears in an
517- accepted statement: a projection, ` WHERE ` , ` GROUP BY ` , ` HAVING ` , ` ORDER BY` ,
518- ` LIMIT ` , ` OFFSET ` , ` FETCH ` , an ` INSERT ` value, an ` UPDATE ` or
519- ` DO UPDATE ` assignment, a ` WITH ` body, and a subquery at any depth, such as
520- ` EXISTS ` , ` IN (SELECT ...) ` , or a scalar subquery. The cast type
521- may be any type a column may declare and sqlcj maps, including a declared
522+ accepted statement: a projection, a join ` ON ` condition , ` WHERE ` , ` GROUP BY` ,
523+ ` HAVING ` , ` ORDER BY ` , ` LIMIT ` , ` OFFSET ` , ` FETCH ` , an ` INSERT ` value, an
524+ ` UPDATE ` or ` DO UPDATE ` assignment, a ` WITH ` body, and a subquery at any
525+ depth, such as ` EXISTS ` , ` IN (SELECT ...) ` , or a scalar subquery. The cast
526+ type may be any type a column may declare and sqlcj maps, including a declared
522527 enum name, written unquoted and matched case-insensitively, and a
523528 one-dimensional array, which binds a ` List ` . A cast type sqlcj does not map,
524529 such as ` INTERVAL ` or a multi-dimensional ` INT[][] ` , is rejected naming the
@@ -814,8 +819,8 @@ name, and its header line.
814819 ` lower(name) AS n ` , and the ` FILTER ` and ` OVER ` forms keep the
815820 unsupported-expression rejection.
816821- A ` FROM ` item that is not a table, a comma-separated source list, a join that
817- is not a plain inner or left join, a join predicate that is not one qualified
818- equality , and a set operation such as ` UNION ` .
822+ is not a plain inner or left join, a join that does not carry exactly one
823+ ` ON ` condition , and a set operation such as ` UNION ` .
819824- An uncast placeholder in a location sqlcj does not analyze, including
820825 ` ORDER BY $1 ` , a projection such as ` SELECT $1 AS x ` , a grouping list, a
821826 ` HAVING ` predicate, a subquery, a
@@ -828,9 +833,8 @@ name, and its header line.
828833 ` LIMIT 5, $1 ` , a placeholder inside a write value such as
829834 ` COALESCE($2, bio) ` , and a ` FETCH FIRST $1 ROWS ONLY ` clause, so dynamic ` IN `
830835 expansion is unavailable. A cast states the type of such a placeholder in
831- every one of those locations except a join condition, where any placeholder
832- stays rejected, and it does not accept a placeholder that is not the direct
833- operand of its cast, such as ` (:x)::int ` .
836+ every one of those locations, and it does not accept a placeholder that is not
837+ the direct operand of its cast, such as ` (:x)::int ` .
834838- A cast type sqlcj does not map, such as ` $1::interval ` or a
835839 multi-dimensional ` $1::int[][] ` , which is rejected naming the placeholder and
836840 the declared type, or the result column and the declared type when the cast
@@ -1084,14 +1088,30 @@ Reads:
10841088 ` QueryAnalyzerTest.shouldAnalyzeLeftJoinAfterInnerJoin ` ,
10851089 ` QueryAnalyzerTest.shouldAnalyzeInnerJoinAfterLeftJoin ` ,
10861090 ` QueryAnalyzerTest.shouldResolveLeftJoinedParametersInTextualBindingOrder ` ,
1087- ` QueryAnalyzerTest.shouldRejectUnsupportedJoinModifier ` , and
1088- ` QueryAnalyzerTest.shouldRejectLeftJoinWithoutSingleQualifiedEquality ` cover
1089- both left-join spellings, the nullable columns of a left-joined source, the
1090- chained join orders, the unchanged parameters, and the join kinds and ` ON `
1091- shapes that stay rejected, and
1091+ and ` QueryAnalyzerTest.shouldRejectUnsupportedJoinModifier ` cover both
1092+ left-join spellings, the nullable columns of a left-joined source, the
1093+ chained join orders, the unchanged parameters, and the join kinds that stay
1094+ rejected, and
10921095 ` PostgresIntegrationTest.shouldExecuteGeneratedLeftJoinAgainstPostgres `
10931096 executes a left join against PostgreSQL 16 whose unmatched row reads the
10941097 joined columns as ` null ` .
1098+ - ` QueryAnalyzerTest.shouldAnalyzeJoinConditionOfAnyBooleanExpression ` ,
1099+ ` QueryAnalyzerTest.shouldReadFilteredLeftJoinedColumnsAsNullable ` ,
1100+ ` QueryAnalyzerTest.shouldResolveJoinConditionParameterInTextualBindingOrder ` ,
1101+ ` QueryAnalyzerTest.shouldResolveNamedJoinConditionParameter ` ,
1102+ ` QueryAnalyzerTest.shouldTypeCastJoinConditionPlaceholderFromItsCast ` ,
1103+ ` QueryAnalyzerTest.shouldRejectJoinConditionReferenceToLaterSource ` ,
1104+ ` QueryAnalyzerTest.shouldRejectUnknownJoinConditionColumn ` , and
1105+ ` QueryAnalyzerTest.shouldRejectJoinWithoutOneOnCondition ` cover a composite
1106+ key, a comparison other than equality, a condition over one source alone, an
1107+ unqualified reference, ` ON TRUE ` , the nullability of a filtered left join,
1108+ the uncast, named, and cast placeholder forms and their binding order, the
1109+ join scope, and the joins that carry no single ` ON ` condition;
1110+ ` SqlcjCompilerIntegrationTest.shouldGenerateCompilableJavaForJoinConditions `
1111+ compiles a repository over them, and
1112+ ` PostgresIntegrationTest.shouldExecuteGeneratedFilteredLeftJoinAgainstPostgres `
1113+ executes a filtered left join against PostgreSQL 16 whose filtered-out and
1114+ unmatched rows read the joined components as ` null ` .
10951115- ` QueryAnalyzerTest.shouldResolveRowTableForFullRowSelect ` ,
10961116 ` QueryAnalyzerTest.shouldNotResolveRowTableForQuerySpecificResult ` ,
10971117 ` QueryAnalyzerTest.shouldNotResolveRowTableForJoinedWildcard ` , and
0 commit comments