fix(sql): keep column comments aligned for mixed star selects
Closes #8015
This commit is contained in:
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" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in new issue
Block a user