fix(sql): keep column comments aligned for mixed star selects

Closes #8015
This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-04 14:45:53 +08:00
1 parent ca003c5e93
commit 09f8d095d2
3 files changed
+124 -1

No files matched your search

@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { analyzeEditableQueryEditability, resolveSourceColumnsByOrdinal } from "@/lib/sql/sqlAnalysis";
/**
* An unqualified `*` is unambiguous when the query has exactly one source,
* so it must bind to that source the same way a qualified `alias.*` does.
* Regression coverage for #8015: `SELECT *, col FROM t` (bare star mixed
* with an explicit column, single source, no alias) was dropping the star
* expansion entirely because the bare-star projection carried no
* `sourceKey`, misaligning every result column from that point on.
*/
describe("single-source star projection column mapping", () => {
it("expands a bare unqualified star mixed with an explicit column", () => {
const result = analyzeEditableQueryEditability("SELECT *, amount FROM orders");
expect(result.editable).toBe(true);
if (!result.editable) return;
const source = { key: "orders:0", tableName: "orders" };
const resolved = resolveSourceColumnsByOrdinal("mysql", result.analysis, [{ source, columns: [{ name: "id" }, { name: "user_id" }, { name: "amount" }] }], 4);
expect(resolved).toEqual([
{ sourceKey: "orders:0", sourceColumn: "id" },
{ sourceKey: "orders:0", sourceColumn: "user_id" },
{ sourceKey: "orders:0", sourceColumn: "amount" },
{ sourceKey: "orders:0", sourceColumn: "amount" },
]);
});
it("expands a bare unqualified star mixed with an aliased source and leading explicit columns", () => {
// Matches the issue's own repro shape: qualified explicit columns before
// a trailing bare `*`, single aliased source, no primary key.
const result = analyzeEditableQueryEditability("SELECT fl.sqlicenseno, fl.sqtypetext, * FROM flow_licenseapprove AS fl");
expect(result.editable).toBe(true);
if (!result.editable) return;
const source = { key: "fl:0", tableName: "flow_licenseapprove" };
const resolved = resolveSourceColumnsByOrdinal("kingbase", result.analysis, [{ source, columns: [{ name: "sqlicenseno" }, { name: "sqtypetext" }, { name: "uniformid" }, { name: "sqtypeid" }] }], 5);
expect(resolved).toEqual([
{ sourceKey: "fl:0", sourceColumn: "sqlicenseno" },
{ sourceKey: "fl:0", sourceColumn: "sqtypetext" },
{ sourceKey: "fl:0", sourceColumn: "sqlicenseno" },
{ sourceKey: "fl:0", sourceColumn: "sqtypetext" },
{ sourceKey: "fl:0", sourceColumn: "uniformid" },
]);
});
it("still expands a plain unqualified star with no other columns (baseline, unchanged)", () => {
const result = analyzeEditableQueryEditability("SELECT * FROM orders");
expect(result.editable).toBe(true);
if (!result.editable) return;
expect(result.analysis.selectStar).toBe(true);
const source = { key: "orders:0", tableName: "orders" };
const resolved = resolveSourceColumnsByOrdinal("mysql", result.analysis, [{ source, columns: [{ name: "id" }, { name: "amount" }] }], 2);
expect(resolved).toEqual([
{ sourceKey: "orders:0", sourceColumn: "id" },
{ sourceKey: "orders:0", sourceColumn: "amount" },
]);
});
});
+9
View File
@@ -452,8 +452,17 @@ function parseSelectColumn(col: string, sources?: EditableQuerySource[]): Editab
function parseStarSelectColumn(col: string, sources?: EditableQuerySource[]): EditableQueryColumn | null { function parseStarSelectColumn(col: string, sources?: EditableQuerySource[]): EditableQueryColumn | null {
if (col === "*") { if (col === "*") {
// An unqualified `*` is unambiguous when the query has exactly one
// source, so it can bind to that source like a qualified `alias.*`
// would. Without this, expandProjectionColumnsForSources cannot find a
// matching table source (no sourceKey) and drops the star expansion
// entirely, misaligning every result column that follows it — e.g.
// `SELECT *, amount FROM orders` loses all result-column comments even
// though the table has no ambiguity to resolve.
const sourceKey = sources?.length === 1 ? sources[0]!.key : undefined;
return { return {
star: true, star: true,
...(sourceKey ? { sourceKey } : {}),
resultName: "*", resultName: "*",
expression: col, expression: col,
}; };
+56 -1
View File
@@ -310,11 +310,23 @@ fn parse_select_column(column: &str, sources: &[FromSource]) -> Option<EditableQ
fn parse_star_select_column(column: &str, sources: &[FromSource]) -> Option<EditableQueryColumn> { fn parse_star_select_column(column: &str, sources: &[FromSource]) -> Option<EditableQueryColumn> {
let trimmed = column.trim(); let trimmed = column.trim();
if trimmed == "*" { if trimmed == "*" {
// An unqualified `*` is unambiguous when the query has exactly one
// source, so it can bind to that source like a qualified `alias.*`
// would. Without this, the star projection carries no source_key and
// the ordinal column-comment resolver (apps/desktop TS side) cannot
// expand it against the table's columns, misaligning every result
// column from that point on — e.g. `SELECT *, amount FROM orders`
// loses all result-column comments even though the table has no
// ambiguity to resolve.
let source_key = match sources {
[only] => Some(only.key.clone()),
_ => None,
};
return Some(EditableQueryColumn { return Some(EditableQueryColumn {
source_name: None, source_name: None,
source_name_quoted: false, source_name_quoted: false,
source_qualifier: None, source_qualifier: None,
source_key: None, source_key,
star: true, star: true,
result_name: "*".to_string(), result_name: "*".to_string(),
expression: trimmed.to_string(), expression: trimmed.to_string(),
@@ -1226,6 +1238,49 @@ mod tests {
); );
} }
#[test]
fn maps_single_table_bare_star_mixed_with_explicit_column() {
// Regression for #8015: a bare, unqualified `*` mixed with another
// projected column is unambiguous when the query has exactly one
// source, so it must bind to that source (source_key) the same way
// a qualified `t.*` does. Previously it carried no source_key,
// which made the ordinal column-comment resolver drop the star
// expansion entirely and lose comments for the whole result set.
let result = analyze_editable_query_editability("SELECT *, amount FROM orders");
assert!(result.editable);
let analysis = result.analysis.unwrap();
assert_eq!(analysis.table_name, "orders");
assert_eq!(
analysis.columns,
vec![
star_column(None, Some("orders:0"), "*"),
column(Some("amount"), false, None, None, "amount", "amount"),
]
);
}
#[test]
fn maps_single_aliased_table_bare_trailing_star_after_qualified_columns() {
// Matches the issue's own repro shape: qualified explicit columns
// followed by a trailing bare `*`, single aliased source.
let result = analyze_editable_query_editability(
"SELECT fl.sqlicenseno, fl.sqtypetext, * FROM flow_licenseapprove AS fl",
);
assert!(result.editable);
let analysis = result.analysis.unwrap();
assert_eq!(analysis.table_alias.as_deref(), Some("fl"));
assert_eq!(
analysis.columns,
vec![
column(Some("sqlicenseno"), false, Some("fl"), Some("fl:0"), "sqlicenseno", "fl.sqlicenseno"),
column(Some("sqtypetext"), false, Some("fl"), Some("fl:0"), "sqtypetext", "fl.sqtypetext"),
star_column(None, Some("fl:0"), "*"),
]
);
}
#[test] #[test]
fn recognizes_distinct_single_table_projection_as_update_only() { fn recognizes_distinct_single_table_projection_as_update_only() {
let result = analyze_editable_query_editability("select distinct id, name from users"); let result = analyze_editable_query_editability("select distinct id, name from users");