Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,13 @@ trait Parser
"column",
"from",
"join",
"inner",
"left",
"right",
"full",
"cross",
"outer",
"on",
"where",
"group",
"having",
Expand Down
15 changes: 4 additions & 11 deletions sql/src/main/scala/app/softnetwork/elastic/sql/query/From.scala
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,9 @@ sealed trait Join extends Updateable {
override def validate(): Either[String, Unit] =
for {
_ <- source.validate()
_ <- alias match {
case Some(a) if a.alias.nonEmpty => Right(())
case _ => Left(s"JOIN $this requires an alias")
}
_ <- this match {
case j if joinType.isDefined && on.isEmpty && joinType.get != CrossJoin =>
Left(s"JOIN $j requires an ON clause")
case j if alias.isEmpty =>
Left(s"JOIN $j requires an alias")
case _ => Right(())
}
} yield ()
Expand Down Expand Up @@ -246,11 +240,10 @@ case class StandardJoin(
alias: Option[Alias] = None
) extends Join {
override def update(request: SingleSearch): StandardJoin = {
val updated = this.copy(
source = source.update(request),
on = on.map(_.update(request))
)
updated
// The source of a JOIN is a TABLE name, not a column expression — `Identifier.update`
// would treat the bare table name as `alias.column` and strip the (nonexistent) column
// part, leaving `name=""`. Only the ON clause needs to be updated for column resolution.
this.copy(on = on.map(_.update(request)))
}

override def validate(): Either[String, Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3106,6 +3106,33 @@ class ParserSpec extends AnyFlatSpec with Matchers {
firstStandardJoinType(long) shouldBe Some(FullJoin)
}

// ── Alias-less JOIN (Issue #95) ─────────────────────────────────────────────

it should "parse a JOIN without an alias on the joined table" in {
val sql =
"SELECT * FROM orders LEFT JOIN customers ON orders.customer_id = customers.id"
val result = Parser(sql)
result.isRight shouldBe true
val ss = result.toOption.get.asInstanceOf[SingleSearch]
val join = ss.from.mainTable.joins.head.asInstanceOf[StandardJoin]
join.alias shouldBe None
join.source.name shouldBe "customers"
}

it should "expose an alias-less joined table by its bare name in From.tableAliases" in {
val sql =
"SELECT * FROM orders LEFT JOIN customers ON orders.customer_id = customers.id"
val ss = Parser(sql).toOption.get.asInstanceOf[SingleSearch]
ss.from.tableAliases should contain("customers" -> "customers")
ss.from.joinAliases.keySet should contain("customers")
}

it should "still reject a JOIN with no ON clause when joinType is set" in {
// INNER/LEFT/RIGHT/FULL JOIN still require ON — only the alias rule is relaxed.
val sql = "SELECT * FROM orders LEFT JOIN customers"
Parser(sql).isLeft shouldBe true
}

behavior of "Parser Cluster"

it should "parse SHOW CLUSTER NAME" in {
Expand Down
Loading