diff --git a/src/ts_generator/sql_parser/quoted_strings.rs b/src/ts_generator/sql_parser/quoted_strings.rs index 4fa7f673..ae22de68 100644 --- a/src/ts_generator/sql_parser/quoted_strings.rs +++ b/src/ts_generator/sql_parser/quoted_strings.rs @@ -29,16 +29,20 @@ pub struct DisplayObjectName<'a>(pub &'a ObjectName); impl fmt::Display for DisplayObjectName<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // In sqlparser 0.59.0, ObjectName contains Vec instead of Vec - // We need to extract the Ident from the first ObjectNamePart - let first_part = &self.0 .0[0]; - if let Some(ident) = first_part.as_ident() { + // For qualified table names (e.g., database.schema.table), we want the last identifier (table name) + let last_part = self.0 .0.last().expect( + "ObjectName must contain at least one part (sqlparser invariant).\ + If you're seeing this, it's a bug in sqlparser or the SQL parsing logic.", + ); + + if let Some(ident) = last_part.as_ident() { let quote_style = &ident.quote_style; let name = &ident.value; let name = trim_table_name(name, quote_style); write!(f, "{name}") } else { // Fallback: if it's a function-based name, use the default Display implementation - write!(f, "{}", first_part) + write!(f, "{}", last_part) } } } @@ -53,3 +57,68 @@ impl fmt::Display for DisplayTableAlias<'_> { write!(f, "{name}") } } + +#[cfg(test)] +mod tests { + use super::*; + use sqlparser::ast::{SetExpr, Statement, TableFactor}; + use sqlparser::dialect::PostgreSqlDialect; + use sqlparser::parser::Parser; + + fn parse_table_name(sql: &str) -> ObjectName { + let dialect = PostgreSqlDialect {}; + let statements = Parser::parse_sql(&dialect, sql).expect("Failed to parse SQL"); + let statement = &statements[0]; + + if let Statement::Query(query) = statement { + if let SetExpr::Select(select) = query.body.as_ref() { + if let Some(table_with_joins) = select.from.first() { + if let TableFactor::Table { name, .. } = &table_with_joins.relation { + return name.clone(); + } + } + } + } + panic!("Could not extract table name from SQL"); + } + + #[test] + fn test_display_object_name_single_identifier() { + // Test simple table name: "users" + let object_name = parse_table_name("SELECT * FROM users"); + let display = DisplayObjectName(&object_name); + assert_eq!(display.to_string(), "users"); + } + + #[test] + fn test_display_object_name_qualified_schema_table() { + // Test qualified name: "public.users" -> should return "users" + let object_name = parse_table_name("SELECT * FROM public.users"); + let display = DisplayObjectName(&object_name); + assert_eq!(display.to_string(), "users"); + } + + #[test] + fn test_display_object_name_qualified_database_schema_table() { + // Test fully qualified name: "database.schema.table" -> should return "table" + let object_name = parse_table_name("SELECT * FROM mydb.public.users"); + let display = DisplayObjectName(&object_name); + assert_eq!(display.to_string(), "users"); + } + + #[test] + fn test_display_object_name_with_quotes() { + // Test quoted identifier: `"my_table"` -> should return "my_table" without quotes + let object_name = parse_table_name("SELECT * FROM \"my_table\""); + let display = DisplayObjectName(&object_name); + assert_eq!(display.to_string(), "my_table"); + } + + #[test] + fn test_display_object_name_qualified_with_quotes() { + // Test qualified with quotes: `"public"."my_table"` -> should return "my_table" + let object_name = parse_table_name("SELECT * FROM \"public\".\"my_table\""); + let display = DisplayObjectName(&object_name); + assert_eq!(display.to_string(), "my_table"); + } +} diff --git a/tests/demo/select/qualified_table_names.queries.ts b/tests/demo/select/qualified_table_names.queries.ts new file mode 100644 index 00000000..466a9cd4 --- /dev/null +++ b/tests/demo/select/qualified_table_names.queries.ts @@ -0,0 +1,39 @@ +export type QualifiedTableNameParams = []; + +export interface IQualifiedTableNameResult { + racesId: number; + racesName: 'human' | 'night elf' | 'dwarf' | 'gnome' | 'orc' | 'troll' | 'tauren' | 'undead'; +} + +export interface IQualifiedTableNameQuery { + params: QualifiedTableNameParams; + result: IQualifiedTableNameResult; +} + +export type QualifiedTableNameJoinParams = []; + +export interface IQualifiedTableNameJoinResult { + factionId: number; + factionName: 'alliance' | 'horde'; + raceId: number; + raceName: 'human' | 'night elf' | 'dwarf' | 'gnome' | 'orc' | 'troll' | 'tauren' | 'undead'; +} + +export interface IQualifiedTableNameJoinQuery { + params: QualifiedTableNameJoinParams; + result: IQualifiedTableNameJoinResult; +} + +export type MixedQualifiedNamesParams = []; + +export interface IMixedQualifiedNamesResult { + factionId: number; + factionName: 'alliance' | 'horde'; + raceId: number; + raceName: 'human' | 'night elf' | 'dwarf' | 'gnome' | 'orc' | 'troll' | 'tauren' | 'undead'; +} + +export interface IMixedQualifiedNamesQuery { + params: MixedQualifiedNamesParams; + result: IMixedQualifiedNamesResult; +} diff --git a/tests/demo/select/qualified_table_names.snapshot.ts b/tests/demo/select/qualified_table_names.snapshot.ts new file mode 100644 index 00000000..d78ac113 --- /dev/null +++ b/tests/demo/select/qualified_table_names.snapshot.ts @@ -0,0 +1,40 @@ +export type QualifiedTableNameParams = []; + +export interface IQualifiedTableNameResult { + racesId: number; + racesName: 'human' | 'night elf' | 'dwarf' | 'gnome' | 'orc' | 'troll' | 'tauren' | 'undead'; +} + +export interface IQualifiedTableNameQuery { + params: QualifiedTableNameParams; + result: IQualifiedTableNameResult; +} + +export type QualifiedTableNameJoinParams = []; + +export interface IQualifiedTableNameJoinResult { + factionId: number; + factionName: 'alliance' | 'horde'; + raceId: number; + raceName: 'human' | 'night elf' | 'dwarf' | 'gnome' | 'orc' | 'troll' | 'tauren' | 'undead'; +} + +export interface IQualifiedTableNameJoinQuery { + params: QualifiedTableNameJoinParams; + result: IQualifiedTableNameJoinResult; +} + +export type MixedQualifiedNamesParams = []; + +export interface IMixedQualifiedNamesResult { + factionId: number; + factionName: 'alliance' | 'horde'; + raceId: number; + raceName: 'human' | 'night elf' | 'dwarf' | 'gnome' | 'orc' | 'troll' | 'tauren' | 'undead'; +} + +export interface IMixedQualifiedNamesQuery { + params: MixedQualifiedNamesParams; + result: IMixedQualifiedNamesResult; +} + diff --git a/tests/demo/select/qualified_table_names.ts b/tests/demo/select/qualified_table_names.ts new file mode 100644 index 00000000..21a9d07c --- /dev/null +++ b/tests/demo/select/qualified_table_names.ts @@ -0,0 +1,37 @@ +import { sql } from 'sqlx-ts' + +// Test qualified table name (schema.table) +const qualifiedTableName = sql` +-- @name: qualified table name +SELECT + races.id, + races.name +FROM public.races +WHERE races.id = 1 +` + +// Test multiple qualified table names in a JOIN +const qualifiedTableNameJoin = sql` +-- @name: qualified table name join +SELECT + races.id AS race_id, + races.name AS race_name, + factions.id AS faction_id, + factions.name AS faction_name +FROM public.races +INNER JOIN public.factions ON races.faction_id = factions.id +WHERE races.id = 1 +` + +// Test mixed qualified and unqualified table names +const mixedQualifiedNames = sql` +-- @name: mixed qualified names +SELECT + r.id AS race_id, + r.name AS race_name, + f.id AS faction_id, + f.name AS faction_name +FROM public.races AS r +INNER JOIN factions AS f ON r.faction_id = f.id +WHERE r.id = 1 +`