1use prax_query::introspection::{
7 ColumnInfo, DatabaseSchema, ForeignKeyInfo, IndexColumn, IndexInfo, ReferentialAction,
8 SortOrder, TableInfo, generate_prax_schema, normalize_type, queries,
9};
10#[cfg(feature = "postgres")]
14use prax_query::introspection::{EnumInfo, ViewInfo};
15use prax_query::sql::DatabaseType;
16
17use crate::config::Config;
18use crate::error::{CliError, CliResult};
19
20#[derive(Debug, Clone)]
22pub struct IntrospectionOptions {
23 pub schema: Option<String>,
25 pub include_views: bool,
27 pub include_materialized_views: bool,
29 pub table_filter: Option<String>,
31 pub exclude_pattern: Option<String>,
33 pub include_comments: bool,
35 pub sample_size: usize,
37}
38
39impl Default for IntrospectionOptions {
40 fn default() -> Self {
41 Self {
42 schema: None,
43 include_views: false,
44 include_materialized_views: false,
45 table_filter: None,
46 exclude_pattern: None,
47 include_comments: true,
48 sample_size: 100,
49 }
50 }
51}
52
53#[allow(async_fn_in_trait)]
55pub trait Introspector {
56 async fn introspect(&self, options: &IntrospectionOptions) -> CliResult<DatabaseSchema>;
58}
59
60pub fn get_database_type(provider: &str) -> CliResult<DatabaseType> {
62 match provider.to_lowercase().as_str() {
63 "postgresql" | "postgres" | "pg" => Ok(DatabaseType::PostgreSQL),
64 "mysql" | "mariadb" => Ok(DatabaseType::MySQL),
65 "sqlite" | "sqlite3" => Ok(DatabaseType::SQLite),
66 "mssql" | "sqlserver" | "sql_server" => Ok(DatabaseType::MSSQL),
67 _ => Err(CliError::Config(format!(
68 "Unsupported database provider: {}",
69 provider
70 ))),
71 }
72}
73
74pub fn default_schema(db_type: DatabaseType) -> &'static str {
76 match db_type {
77 DatabaseType::PostgreSQL => "public",
78 DatabaseType::MySQL => "",
79 DatabaseType::SQLite => "",
80 DatabaseType::MSSQL => "dbo",
81 }
82}
83
84pub async fn introspect_database(
91 provider: &str,
92 database_url: &str,
93 options: &IntrospectionOptions,
94) -> CliResult<DatabaseSchema> {
95 let db_type = get_database_type(provider)?;
96 match db_type {
97 DatabaseType::PostgreSQL => {
98 #[cfg(feature = "postgres")]
99 {
100 postgres::PostgresIntrospector::new(database_url.to_string())
101 .introspect(options)
102 .await
103 }
104 #[cfg(not(feature = "postgres"))]
105 {
106 let _ = (database_url, options);
107 Err(CliError::FeatureUnavailable(
108 "PostgreSQL introspection requires the `postgres` feature: rebuild with \
109 --features postgres."
110 .to_string(),
111 ))
112 }
113 }
114 DatabaseType::MySQL => {
115 #[cfg(feature = "mysql")]
116 {
117 mysql::MysqlIntrospector::new(database_url.to_string())
118 .introspect(options)
119 .await
120 }
121 #[cfg(not(feature = "mysql"))]
122 {
123 let _ = (database_url, options);
124 Err(CliError::FeatureUnavailable(
125 "MySQL introspection requires the `mysql` feature: rebuild with \
126 --features mysql."
127 .to_string(),
128 ))
129 }
130 }
131 DatabaseType::SQLite => {
132 #[cfg(feature = "sqlite")]
133 {
134 sqlite::SqliteIntrospector::new(database_url.to_string())
135 .introspect(options)
136 .await
137 }
138 #[cfg(not(feature = "sqlite"))]
139 {
140 let _ = (database_url, options);
141 Err(CliError::FeatureUnavailable(
142 "SQLite introspection requires the `sqlite` feature: rebuild with \
143 --features sqlite."
144 .to_string(),
145 ))
146 }
147 }
148 DatabaseType::MSSQL => {
149 #[cfg(feature = "mssql")]
150 {
151 mssql::MssqlIntrospector::new(database_url.to_string())
152 .introspect(options)
153 .await
154 }
155 #[cfg(not(feature = "mssql"))]
156 {
157 let _ = (database_url, options);
158 Err(CliError::FeatureUnavailable(
159 "MSSQL introspection requires the `mssql` feature: rebuild with \
160 --features mssql."
161 .to_string(),
162 ))
163 }
164 }
165 }
166}
167
168#[cfg(feature = "postgres")]
173pub mod postgres {
174 use std::collections::HashMap;
175
176 use super::*;
177 use tokio_postgres::{Client, NoTls, Row};
178
179 pub struct PostgresIntrospector {
181 connection_string: String,
182 }
183
184 impl PostgresIntrospector {
185 pub fn new(connection_string: String) -> Self {
187 Self { connection_string }
188 }
189
190 async fn connect(&self) -> CliResult<Client> {
192 let config = self
198 .connection_string
199 .parse::<tokio_postgres::Config>()
200 .map_err(|e| CliError::Config(format!("Invalid connection string: {}", e)))?;
201
202 let tls_disabled = matches!(
203 config.get_ssl_mode(),
204 tokio_postgres::config::SslMode::Disable
205 );
206
207 if tls_disabled && !config.get_hosts().iter().all(is_local_host) {
208 crate::output::warn(
209 "sslmode=disable with a non-local host: credentials and data will be \
210 sent in plaintext.",
211 );
212 }
213
214 let connect_timeout =
221 std::time::Duration::from_secs(super::INTROSPECT_CONNECT_TIMEOUT_SECS);
222 let client = if tls_disabled {
223 let (client, connection) = tokio::time::timeout(
224 connect_timeout,
225 tokio_postgres::connect(&self.connection_string, NoTls),
226 )
227 .await
228 .map_err(|_| {
229 CliError::Unreachable("Failed to connect: connection timed out".to_string())
230 })?
231 .map_err(|e| CliError::Unreachable(format!("Failed to connect: {}", e)))?;
232 tokio::spawn(async move {
233 if let Err(e) = connection.await {
234 eprintln!("Connection error: {}", e);
235 }
236 });
237 client
238 } else {
239 let (client, connection) = tokio::time::timeout(
240 connect_timeout,
241 tokio_postgres::connect(
242 &self.connection_string,
243 prax_postgres::tls::make_tls_connector(),
244 ),
245 )
246 .await
247 .map_err(|_| {
248 CliError::Unreachable("Failed to connect: connection timed out".to_string())
249 })?
250 .map_err(|e| CliError::Unreachable(format!("Failed to connect: {}", e)))?;
251 tokio::spawn(async move {
252 if let Err(e) = connection.await {
253 eprintln!("Connection error: {}", e);
254 }
255 });
256 client
257 };
258
259 Ok(client)
260 }
261 }
262
263 impl Introspector for PostgresIntrospector {
264 async fn introspect(&self, options: &IntrospectionOptions) -> CliResult<DatabaseSchema> {
265 let client = self.connect().await?;
266 let schema_name = options.schema.as_deref().unwrap_or("public");
267
268 let mut db_schema = DatabaseSchema {
269 name: "database".to_string(),
270 schema: Some(schema_name.to_string()),
271 ..Default::default()
272 };
273
274 let tables_sql = queries::tables_query(DatabaseType::PostgreSQL, Some(schema_name));
276 let table_rows = client
277 .query(&tables_sql, &[])
278 .await
279 .map_err(|e| CliError::Database(format!("Failed to query tables: {}", e)))?;
280
281 for row in table_rows {
282 let table_name: String = row.get(0);
283
284 if let Some(ref pattern) = options.table_filter
286 && !matches_pattern(&table_name, pattern)
287 {
288 continue;
289 }
290 if let Some(ref exclude) = options.exclude_pattern
291 && matches_pattern(&table_name, exclude)
292 {
293 continue;
294 }
295
296 let comment: Option<String> = row.try_get(1).ok();
297
298 db_schema.tables.push(TableInfo {
299 name: table_name,
300 schema: Some(schema_name.to_string()),
301 comment: if options.include_comments {
302 comment
303 } else {
304 None
305 },
306 ..Default::default()
307 });
308 }
309
310 let cols_sql = "SELECT \
317 c.column_name, \
318 c.data_type, \
319 c.udt_name, \
320 c.is_nullable = 'YES' as nullable, \
321 c.column_default, \
322 c.character_maximum_length, \
323 c.numeric_precision, \
324 c.numeric_scale, \
325 col_description((quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass, c.ordinal_position) as comment, \
326 CASE WHEN c.column_default LIKE 'nextval%' THEN true ELSE false END as auto_increment, \
327 c.table_name \
328 FROM information_schema.columns c \
329 WHERE c.table_schema = $1 \
330 ORDER BY c.table_name, c.ordinal_position";
331 let col_rows = client
332 .query(cols_sql, &[&schema_name])
333 .await
334 .map_err(|e| CliError::Database(format!("Failed to query columns: {}", e)))?;
335
336 let mut columns_by_table: HashMap<String, Vec<Row>> = HashMap::new();
337 for col_row in col_rows {
338 let table_name: String = col_row.get(10);
339 columns_by_table
340 .entry(table_name)
341 .or_default()
342 .push(col_row);
343 }
344
345 let pk_sql = "SELECT a.attname as column_name, c.relname as table_name \
346 FROM pg_index i \
347 JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) \
348 JOIN pg_class c ON c.oid = i.indrelid \
349 JOIN pg_namespace n ON n.oid = c.relnamespace \
350 WHERE i.indisprimary AND n.nspname = $1 \
351 ORDER BY c.relname, array_position(i.indkey, a.attnum)";
352 let pk_rows = client
353 .query(pk_sql, &[&schema_name])
354 .await
355 .map_err(|e| CliError::Database(format!("Failed to query primary keys: {}", e)))?;
356
357 let mut pks_by_table: HashMap<String, Vec<Row>> = HashMap::new();
358 for pk_row in pk_rows {
359 let table_name: String = pk_row.get(1);
360 pks_by_table.entry(table_name).or_default().push(pk_row);
361 }
362
363 let fk_sql = "SELECT \
364 tc.constraint_name, \
365 kcu.column_name, \
366 ccu.table_name as referenced_table, \
367 ccu.table_schema as referenced_schema, \
368 ccu.column_name as referenced_column, \
369 rc.delete_rule, \
370 rc.update_rule, \
371 tc.table_name \
372 FROM information_schema.table_constraints tc \
373 JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name \
374 JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name \
375 JOIN information_schema.referential_constraints rc ON rc.constraint_name = tc.constraint_name \
376 WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1 \
377 ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position";
378 let fk_rows = client
379 .query(fk_sql, &[&schema_name])
380 .await
381 .map_err(|e| CliError::Database(format!("Failed to query foreign keys: {}", e)))?;
382
383 let mut fks_by_table: HashMap<String, Vec<Row>> = HashMap::new();
384 for fk_row in fk_rows {
385 let table_name: String = fk_row.get(7);
386 fks_by_table.entry(table_name).or_default().push(fk_row);
387 }
388
389 let idx_sql = "SELECT \
390 i.relname as index_name, \
391 a.attname as column_name, \
392 ix.indisunique as is_unique, \
393 ix.indisprimary as is_primary, \
394 am.amname as index_type, \
395 pg_get_expr(ix.indpred, ix.indrelid) as filter, \
396 t.relname as table_name \
397 FROM pg_index ix \
398 JOIN pg_class t ON t.oid = ix.indrelid \
399 JOIN pg_class i ON i.oid = ix.indexrelid \
400 JOIN pg_namespace n ON n.oid = t.relnamespace \
401 JOIN pg_am am ON i.relam = am.oid \
402 JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) \
403 WHERE n.nspname = $1 \
404 ORDER BY t.relname, i.relname, array_position(ix.indkey, a.attnum)";
405 let idx_rows = client
406 .query(idx_sql, &[&schema_name])
407 .await
408 .map_err(|e| CliError::Database(format!("Failed to query indexes: {}", e)))?;
409
410 let mut indexes_by_table: HashMap<String, Vec<Row>> = HashMap::new();
411 for idx_row in idx_rows {
412 let table_name: String = idx_row.get(6);
413 indexes_by_table
414 .entry(table_name)
415 .or_default()
416 .push(idx_row);
417 }
418
419 for table in &mut db_schema.tables {
421 for col_row in columns_by_table.remove(&table.name).unwrap_or_default() {
422 let col_name: String = col_row.get(0);
423 let data_type: String = col_row.get(1);
424 let udt_name: String = col_row.get(2);
425 let nullable: bool = col_row.get(3);
426 let default: Option<String> = col_row.try_get(4).ok();
427 let max_length: Option<i32> = col_row.try_get(5).ok();
428 let precision: Option<i32> = col_row.try_get(6).ok();
429 let scale: Option<i32> = col_row.try_get(7).ok();
430 let comment: Option<String> = col_row.try_get(8).ok();
431 let auto_increment: bool = col_row.try_get(9).unwrap_or(false);
432
433 let normalized = normalize_type(
434 DatabaseType::PostgreSQL,
435 &udt_name,
436 max_length,
437 precision,
438 scale,
439 );
440
441 table.columns.push(ColumnInfo {
442 name: col_name,
443 db_type: data_type,
444 normalized_type: normalized,
445 nullable,
446 default,
447 auto_increment,
448 max_length,
449 precision,
450 scale,
451 comment: if options.include_comments {
452 comment
453 } else {
454 None
455 },
456 ..Default::default()
457 });
458 }
459
460 for pk_row in pks_by_table.remove(&table.name).unwrap_or_default() {
461 let col_name: String = pk_row.get(0);
462 table.primary_key.push(col_name.clone());
463
464 if let Some(col) = table.columns.iter_mut().find(|c| c.name == col_name) {
466 col.is_primary_key = true;
467 }
468 }
469
470 let mut fk_map: HashMap<String, ForeignKeyInfo> = HashMap::new();
471 for fk_row in fks_by_table.remove(&table.name).unwrap_or_default() {
472 let constraint_name: String = fk_row.get(0);
473 let column_name: String = fk_row.get(1);
474 let ref_table: String = fk_row.get(2);
475 let ref_schema: Option<String> = fk_row.try_get(3).ok();
476 let ref_column: String = fk_row.get(4);
477 let delete_rule: String = fk_row.get(5);
478 let update_rule: String = fk_row.get(6);
479
480 let fk =
481 fk_map
482 .entry(constraint_name.clone())
483 .or_insert_with(|| ForeignKeyInfo {
484 name: constraint_name,
485 columns: Vec::new(),
486 referenced_table: ref_table,
487 referenced_schema: ref_schema,
488 referenced_columns: Vec::new(),
489 on_delete: ReferentialAction::from_str(&delete_rule),
490 on_update: ReferentialAction::from_str(&update_rule),
491 });
492
493 fk.columns.push(column_name);
494 fk.referenced_columns.push(ref_column);
495 }
496
497 table.foreign_keys = fk_map.into_values().collect();
498
499 let mut idx_map: HashMap<String, IndexInfo> = HashMap::new();
500 for idx_row in indexes_by_table.remove(&table.name).unwrap_or_default() {
501 let idx_name: String = idx_row.get(0);
502 let col_name: String = idx_row.get(1);
503 let is_unique: bool = idx_row.get(2);
504 let is_primary: bool = idx_row.get(3);
505 let idx_type: Option<String> = idx_row.try_get(4).ok();
506 let filter: Option<String> = idx_row.try_get(5).ok();
507
508 let idx = idx_map
509 .entry(idx_name.clone())
510 .or_insert_with(|| IndexInfo {
511 name: idx_name,
512 columns: Vec::new(),
513 is_unique,
514 is_primary,
515 index_type: idx_type,
516 filter,
517 });
518
519 idx.columns.push(IndexColumn {
520 name: col_name,
521 order: SortOrder::Asc,
522 ..Default::default()
523 });
524 }
525
526 table.indexes = idx_map.into_values().collect();
527 }
528
529 let enums_sql = queries::enums_query(Some(schema_name));
531 let enum_rows = client
532 .query(&enums_sql, &[])
533 .await
534 .map_err(|e| CliError::Database(format!("Failed to query enums: {}", e)))?;
535
536 let mut enum_map: HashMap<String, EnumInfo> = HashMap::new();
537 for enum_row in enum_rows {
538 let enum_name: String = enum_row.get(0);
539 let enum_value: String = enum_row.get(1);
540
541 let enum_info = enum_map
542 .entry(enum_name.clone())
543 .or_insert_with(|| EnumInfo {
544 name: enum_name,
545 schema: Some(schema_name.to_string()),
546 values: Vec::new(),
547 });
548
549 enum_info.values.push(enum_value);
550 }
551
552 db_schema.enums = enum_map.into_values().collect();
553
554 if options.include_views || options.include_materialized_views {
556 let views_sql = queries::views_query(DatabaseType::PostgreSQL, Some(schema_name));
557 let view_rows = client
558 .query(&views_sql, &[])
559 .await
560 .map_err(|e| CliError::Database(format!("Failed to query views: {}", e)))?;
561
562 for view_row in view_rows {
563 let view_name: String = view_row.get(0);
564 let definition: Option<String> = view_row.try_get(1).ok();
565 let is_materialized: bool = view_row.get(2);
566
567 if is_materialized && !options.include_materialized_views {
568 continue;
569 }
570 if !is_materialized && !options.include_views {
571 continue;
572 }
573
574 db_schema.views.push(ViewInfo {
575 name: view_name,
576 schema: Some(schema_name.to_string()),
577 definition,
578 is_materialized,
579 columns: Vec::new(),
580 });
581 }
582 }
583
584 Ok(db_schema)
585 }
586 }
587
588 fn is_local_host(host: &tokio_postgres::config::Host) -> bool {
590 match host {
591 tokio_postgres::config::Host::Tcp(name) => {
592 name == "localhost" || name == "127.0.0.1" || name == "::1"
593 }
594 tokio_postgres::config::Host::Unix(_) => true,
595 }
596 }
597}
598
599#[cfg(any(
608 feature = "postgres",
609 feature = "mysql",
610 feature = "sqlite",
611 feature = "mssql"
612))]
613pub(crate) const INTROSPECT_CONNECT_TIMEOUT_SECS: u64 = 5;
614
615#[cfg(any(feature = "mysql", feature = "sqlite"))]
620trait JsonRowSource {
621 async fn json_rows(&self, sql: &str) -> CliResult<Vec<serde_json::Value>>;
623}
624
625#[cfg(feature = "mysql")]
626impl JsonRowSource for prax_mysql::MysqlRawEngine {
627 async fn json_rows(&self, sql: &str) -> CliResult<Vec<serde_json::Value>> {
628 let rows = self
629 .raw_sql_query(sql, &[])
630 .await
631 .map_err(|e| CliError::Database(format!("Introspection query failed: {}", e)))?;
632 Ok(rows.into_iter().map(|r| r.into_json()).collect())
633 }
634}
635
636#[cfg(feature = "sqlite")]
637impl JsonRowSource for prax_sqlite::SqliteRawEngine {
638 async fn json_rows(&self, sql: &str) -> CliResult<Vec<serde_json::Value>> {
639 let rows = self
640 .raw_sql_query(sql, &[])
641 .await
642 .map_err(|e| CliError::Database(format!("Introspection query failed: {}", e)))?;
643 Ok(rows.into_iter().map(|r| r.into_json()).collect())
644 }
645}
646
647#[cfg(any(
655 feature = "postgres",
656 feature = "mysql",
657 feature = "sqlite",
658 feature = "mssql"
659))]
660fn matches_pattern(name: &str, pattern: &str) -> bool {
661 if pattern == "*" {
662 return true;
663 }
664
665 if pattern.starts_with('*') && pattern.ends_with('*') {
666 let middle = &pattern[1..pattern.len() - 1];
667 return name.contains(middle);
668 }
669
670 if let Some(suffix) = pattern.strip_prefix('*') {
671 return name.ends_with(suffix);
672 }
673
674 if let Some(prefix) = pattern.strip_suffix('*') {
675 return name.starts_with(prefix);
676 }
677
678 name == pattern
679}
680
681#[cfg(any(feature = "mysql", feature = "sqlite"))]
688fn json_get<'a>(row: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> {
689 if let Some(v) = row.get(key) {
690 return Some(v);
691 }
692 row.as_object()?
693 .iter()
694 .find(|(k, _)| k.eq_ignore_ascii_case(key))
695 .map(|(_, v)| v)
696}
697
698#[cfg(any(feature = "mysql", feature = "sqlite"))]
700fn json_str(row: &serde_json::Value, key: &str) -> Option<String> {
701 json_get(row, key)
702 .and_then(|v| v.as_str())
703 .map(str::to_string)
704}
705
706#[cfg(feature = "mysql")]
710fn json_bool(row: &serde_json::Value, key: &str) -> bool {
711 match json_get(row, key) {
712 Some(serde_json::Value::Bool(b)) => *b,
713 Some(serde_json::Value::Number(n)) => n.as_i64().is_some_and(|i| i != 0),
714 Some(serde_json::Value::String(s)) => {
715 matches!(s.as_str(), "1" | "YES" | "yes" | "true" | "TRUE")
716 }
717 _ => false,
718 }
719}
720
721#[cfg(any(feature = "mysql", feature = "sqlite"))]
723fn json_i32(row: &serde_json::Value, key: &str) -> Option<i32> {
724 match json_get(row, key) {
725 Some(serde_json::Value::Number(n)) => n.as_i64().and_then(|i| i32::try_from(i).ok()),
726 Some(serde_json::Value::String(s)) => s.parse::<i32>().ok(),
727 _ => None,
728 }
729}
730
731#[cfg(feature = "mysql")]
736pub mod mysql {
737 use super::*;
738 use prax_mysql::{MysqlPool, MysqlRawEngine};
739
740 pub struct MysqlIntrospector {
742 connection_string: String,
743 }
744
745 impl MysqlIntrospector {
746 pub fn new(connection_string: String) -> Self {
748 Self { connection_string }
749 }
750
751 async fn engine(&self) -> CliResult<MysqlRawEngine> {
752 let pool = MysqlPool::builder()
753 .url(self.connection_string.clone())
754 .connection_timeout(std::time::Duration::from_secs(
755 super::INTROSPECT_CONNECT_TIMEOUT_SECS,
756 ))
757 .build()
758 .await
759 .map_err(|e| CliError::Unreachable(format!("Failed to connect: {}", e)))?;
760 Ok(MysqlRawEngine::new(pool))
761 }
762
763 async fn rows(engine: &MysqlRawEngine, sql: &str) -> CliResult<Vec<serde_json::Value>> {
766 super::JsonRowSource::json_rows(engine, sql).await
767 }
768 }
769
770 impl Introspector for MysqlIntrospector {
771 async fn introspect(&self, options: &IntrospectionOptions) -> CliResult<DatabaseSchema> {
772 let engine = self.engine().await?;
773 let schema = options.schema.clone();
776 let schema_ref = schema.as_deref();
777
778 let mut db_schema = DatabaseSchema {
779 name: "database".to_string(),
780 schema: schema.clone(),
781 ..Default::default()
782 };
783
784 let table_rows = Self::rows(
785 &engine,
786 &queries::tables_query(DatabaseType::MySQL, schema_ref),
787 )
788 .await?;
789 for row in &table_rows {
790 let Some(table_name) = json_str(row, "table_name") else {
791 continue;
792 };
793 if let Some(ref pattern) = options.table_filter
794 && !matches_pattern(&table_name, pattern)
795 {
796 continue;
797 }
798 if let Some(ref exclude) = options.exclude_pattern
799 && matches_pattern(&table_name, exclude)
800 {
801 continue;
802 }
803
804 db_schema.tables.push(TableInfo {
805 name: table_name,
806 schema: schema.clone(),
807 comment: if options.include_comments {
808 json_str(row, "comment").filter(|c| !c.is_empty())
809 } else {
810 None
811 },
812 ..Default::default()
813 });
814 }
815
816 for table in &mut db_schema.tables {
817 populate_table(&engine, table, schema_ref, options).await?;
818 }
819
820 Ok(db_schema)
821 }
822 }
823
824 async fn populate_table(
826 engine: &MysqlRawEngine,
827 table: &mut TableInfo,
828 schema: Option<&str>,
829 options: &IntrospectionOptions,
830 ) -> CliResult<()> {
831 let col_rows = MysqlIntrospector::rows(
833 engine,
834 &queries::columns_query(DatabaseType::MySQL, &table.name, schema),
835 )
836 .await?;
837 for row in &col_rows {
838 let Some(name) = json_str(row, "column_name") else {
839 continue;
840 };
841 let data_type = json_str(row, "data_type").unwrap_or_default();
842 let max_length = json_i32(row, "character_maximum_length");
843 let precision = json_i32(row, "numeric_precision");
844 let scale = json_i32(row, "numeric_scale");
845 let normalized = normalize_type(
846 DatabaseType::MySQL,
847 &data_type,
848 max_length,
849 precision,
850 scale,
851 );
852
853 table.columns.push(ColumnInfo {
854 name,
855 db_type: data_type,
856 normalized_type: normalized,
857 nullable: json_bool(row, "nullable"),
858 default: json_str(row, "column_default"),
859 auto_increment: json_bool(row, "auto_increment"),
860 max_length,
861 precision,
862 scale,
863 comment: if options.include_comments {
864 json_str(row, "comment").filter(|c| !c.is_empty())
865 } else {
866 None
867 },
868 ..Default::default()
869 });
870 }
871
872 let pk_rows = MysqlIntrospector::rows(
874 engine,
875 &queries::primary_keys_query(DatabaseType::MySQL, &table.name, schema),
876 )
877 .await?;
878 for row in &pk_rows {
879 if let Some(col) = json_str(row, "column_name") {
880 table.primary_key.push(col.clone());
881 if let Some(c) = table.columns.iter_mut().find(|c| c.name == col) {
882 c.is_primary_key = true;
883 }
884 }
885 }
886
887 let fk_rows = MysqlIntrospector::rows(
889 engine,
890 &queries::foreign_keys_query(DatabaseType::MySQL, &table.name, schema),
891 )
892 .await?;
893 let mut fk_map: std::collections::HashMap<String, ForeignKeyInfo> =
894 std::collections::HashMap::new();
895 let mut fk_order: Vec<String> = Vec::new();
896 for row in &fk_rows {
897 let Some(cname) = json_str(row, "constraint_name") else {
898 continue;
899 };
900 let fk = fk_map.entry(cname.clone()).or_insert_with(|| {
901 fk_order.push(cname.clone());
902 ForeignKeyInfo {
903 name: cname.clone(),
904 columns: Vec::new(),
905 referenced_table: json_str(row, "referenced_table").unwrap_or_default(),
906 referenced_schema: json_str(row, "referenced_schema"),
907 referenced_columns: Vec::new(),
908 on_delete: ReferentialAction::from_str(
909 &json_str(row, "delete_rule").unwrap_or_default(),
910 ),
911 on_update: ReferentialAction::from_str(
912 &json_str(row, "update_rule").unwrap_or_default(),
913 ),
914 }
915 });
916 if let Some(col) = json_str(row, "column_name") {
917 fk.columns.push(col);
918 }
919 if let Some(rc) = json_str(row, "referenced_column") {
920 fk.referenced_columns.push(rc);
921 }
922 }
923 table.foreign_keys = fk_order
924 .into_iter()
925 .filter_map(|n| fk_map.remove(&n))
926 .collect();
927
928 let idx_rows = MysqlIntrospector::rows(
930 engine,
931 &queries::indexes_query(DatabaseType::MySQL, &table.name, schema),
932 )
933 .await?;
934 let mut idx_map: std::collections::HashMap<String, IndexInfo> =
935 std::collections::HashMap::new();
936 let mut idx_order: Vec<String> = Vec::new();
937 for row in &idx_rows {
938 let Some(iname) = json_str(row, "index_name") else {
939 continue;
940 };
941 let idx = idx_map.entry(iname.clone()).or_insert_with(|| {
942 idx_order.push(iname.clone());
943 IndexInfo {
944 name: iname.clone(),
945 columns: Vec::new(),
946 is_unique: json_bool(row, "is_unique"),
947 is_primary: json_bool(row, "is_primary"),
948 index_type: json_str(row, "index_type"),
949 filter: json_str(row, "filter"),
950 }
951 });
952 if let Some(col) = json_str(row, "column_name") {
953 idx.columns.push(IndexColumn {
954 name: col,
955 order: SortOrder::Asc,
956 ..Default::default()
957 });
958 }
959 }
960 table.indexes = idx_order
961 .into_iter()
962 .filter_map(|n| idx_map.remove(&n))
963 .collect();
964
965 Ok(())
966 }
967}
968
969#[cfg(feature = "sqlite")]
974pub mod sqlite {
975 use super::*;
976 use prax_sqlite::{SqlitePool, SqliteRawEngine};
977
978 pub struct SqliteIntrospector {
984 connection_string: String,
985 }
986
987 impl SqliteIntrospector {
988 pub fn new(connection_string: String) -> Self {
990 Self { connection_string }
991 }
992
993 async fn engine(&self) -> CliResult<SqliteRawEngine> {
994 let pool = SqlitePool::builder()
995 .url(self.connection_string.clone())
996 .connection_timeout(std::time::Duration::from_secs(
997 super::INTROSPECT_CONNECT_TIMEOUT_SECS,
998 ))
999 .build()
1000 .await
1001 .map_err(|e| CliError::Unreachable(format!("Failed to open database: {}", e)))?;
1002 Ok(SqliteRawEngine::new(pool))
1003 }
1004
1005 async fn rows(engine: &SqliteRawEngine, sql: &str) -> CliResult<Vec<serde_json::Value>> {
1006 super::JsonRowSource::json_rows(engine, sql).await
1007 }
1008 }
1009
1010 impl Introspector for SqliteIntrospector {
1011 async fn introspect(&self, options: &IntrospectionOptions) -> CliResult<DatabaseSchema> {
1012 let engine = self.engine().await?;
1013
1014 let mut db_schema = DatabaseSchema {
1015 name: "database".to_string(),
1016 schema: None,
1017 ..Default::default()
1018 };
1019
1020 let table_rows =
1022 Self::rows(&engine, &queries::tables_query(DatabaseType::SQLite, None)).await?;
1023 for row in &table_rows {
1024 let Some(table_name) = json_str(row, "table_name") else {
1025 continue;
1026 };
1027 if let Some(ref pattern) = options.table_filter
1028 && !matches_pattern(&table_name, pattern)
1029 {
1030 continue;
1031 }
1032 if let Some(ref exclude) = options.exclude_pattern
1033 && matches_pattern(&table_name, exclude)
1034 {
1035 continue;
1036 }
1037 db_schema.tables.push(TableInfo {
1038 name: table_name,
1039 ..Default::default()
1040 });
1041 }
1042
1043 for table in &mut db_schema.tables {
1044 populate_table(&engine, table).await?;
1045 }
1046
1047 Ok(db_schema)
1048 }
1049 }
1050
1051 async fn populate_table(engine: &SqliteRawEngine, table: &mut TableInfo) -> CliResult<()> {
1052 let col_rows = SqliteIntrospector::rows(
1055 engine,
1056 &queries::columns_query(DatabaseType::SQLite, &table.name, None),
1057 )
1058 .await?;
1059 let mut pk_positions: Vec<(i32, String)> = Vec::new();
1060 for row in &col_rows {
1061 let Some(name) = json_str(row, "name") else {
1062 continue;
1063 };
1064 let decl_type = json_str(row, "type").unwrap_or_default();
1065 let base_type: String = decl_type
1068 .split([' ', '('])
1069 .next()
1070 .unwrap_or("")
1071 .to_ascii_lowercase();
1072 let normalized = normalize_type(DatabaseType::SQLite, &base_type, None, None, None);
1073 let not_null = json_i32(row, "notnull").unwrap_or(0) != 0;
1074 let pk_pos = json_i32(row, "pk").unwrap_or(0);
1075 if pk_pos > 0 {
1076 pk_positions.push((pk_pos, name.clone()));
1077 }
1078
1079 table.columns.push(ColumnInfo {
1080 name,
1081 db_type: decl_type,
1082 normalized_type: normalized,
1083 nullable: !not_null,
1088 default: json_str(row, "dflt_value"),
1089 auto_increment: false,
1092 is_primary_key: pk_pos > 0,
1093 ..Default::default()
1094 });
1095 }
1096
1097 pk_positions.sort_by_key(|(pos, _)| *pos);
1099 table.primary_key = pk_positions.into_iter().map(|(_, name)| name).collect();
1100
1101 if table.primary_key.len() == 1
1103 && let Some(col) = table
1104 .columns
1105 .iter_mut()
1106 .find(|c| c.name == table.primary_key[0])
1107 && col.db_type.to_ascii_lowercase().contains("int")
1108 {
1109 col.auto_increment = true;
1110 }
1111
1112 let fk_rows = SqliteIntrospector::rows(
1115 engine,
1116 &queries::foreign_keys_query(DatabaseType::SQLite, &table.name, None),
1117 )
1118 .await?;
1119 let mut fk_map: std::collections::BTreeMap<i64, ForeignKeyInfo> =
1120 std::collections::BTreeMap::new();
1121 for row in &fk_rows {
1122 let id = row.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
1123 let fk = fk_map.entry(id).or_insert_with(|| ForeignKeyInfo {
1124 name: String::new(),
1128 columns: Vec::new(),
1129 referenced_table: json_str(row, "table").unwrap_or_default(),
1130 referenced_schema: None,
1131 referenced_columns: Vec::new(),
1132 on_delete: ReferentialAction::from_str(
1133 &json_str(row, "on_delete").unwrap_or_default(),
1134 ),
1135 on_update: ReferentialAction::from_str(
1136 &json_str(row, "on_update").unwrap_or_default(),
1137 ),
1138 });
1139 if let Some(from) = json_str(row, "from") {
1140 fk.columns.push(from);
1141 }
1142 if let Some(to) = json_str(row, "to") {
1143 fk.referenced_columns.push(to);
1144 }
1145 }
1146 table.foreign_keys = fk_map
1147 .into_values()
1148 .map(|mut fk| {
1149 if fk.name.is_empty() {
1150 fk.name = format!("fk_{}_{}", table.name, fk.columns.join("_"));
1151 }
1152 fk
1153 })
1154 .collect();
1155
1156 let idx_rows = SqliteIntrospector::rows(
1159 engine,
1160 &queries::indexes_query(DatabaseType::SQLite, &table.name, None),
1161 )
1162 .await?;
1163 for row in &idx_rows {
1164 let Some(idx_name) = json_str(row, "name") else {
1165 continue;
1166 };
1167 let origin = json_str(row, "origin").unwrap_or_default();
1168 let is_primary = origin == "pk";
1169 let is_unique = json_i32(row, "unique").unwrap_or(0) != 0;
1170
1171 let info_rows = SqliteIntrospector::rows(
1173 engine,
1174 &format!("PRAGMA index_info('{}')", idx_name.replace('\'', "''")),
1175 )
1176 .await?;
1177 let columns: Vec<IndexColumn> = info_rows
1178 .iter()
1179 .filter_map(|r| json_str(r, "name"))
1180 .map(|name| IndexColumn {
1181 name,
1182 order: SortOrder::Asc,
1183 ..Default::default()
1184 })
1185 .collect();
1186
1187 table.indexes.push(IndexInfo {
1188 name: idx_name,
1189 columns,
1190 is_unique,
1191 is_primary,
1192 index_type: None,
1193 filter: None,
1194 });
1195 }
1196
1197 Ok(())
1198 }
1199}
1200
1201#[cfg(feature = "mssql")]
1206pub mod mssql {
1207 use super::*;
1208 use prax_mssql::MssqlPool;
1209 use prax_mssql::Row;
1210
1211 pub struct MssqlIntrospector {
1215 connection_string: String,
1216 }
1217
1218 impl MssqlIntrospector {
1219 pub fn new(connection_string: String) -> Self {
1221 Self { connection_string }
1222 }
1223 }
1224
1225 fn row_str(row: &Row, col: &str) -> Option<String> {
1227 row.try_get::<&str, _>(col)
1228 .ok()
1229 .flatten()
1230 .map(str::to_string)
1231 }
1232
1233 fn row_bool(row: &Row, col: &str) -> bool {
1235 if let Ok(Some(b)) = row.try_get::<bool, _>(col) {
1236 return b;
1237 }
1238 row_i64(row, col).is_some_and(|i| i != 0)
1240 }
1241
1242 fn row_i64(row: &Row, col: &str) -> Option<i64> {
1244 if let Ok(Some(v)) = row.try_get::<i32, _>(col) {
1245 return Some(v as i64);
1246 }
1247 if let Ok(Some(v)) = row.try_get::<i64, _>(col) {
1248 return Some(v);
1249 }
1250 if let Ok(Some(v)) = row.try_get::<i16, _>(col) {
1251 return Some(v as i64);
1252 }
1253 if let Ok(Some(v)) = row.try_get::<u8, _>(col) {
1254 return Some(v as i64);
1255 }
1256 None
1257 }
1258
1259 fn row_i32(row: &Row, col: &str) -> Option<i32> {
1260 row_i64(row, col).and_then(|v| i32::try_from(v).ok())
1261 }
1262
1263 impl Introspector for MssqlIntrospector {
1264 async fn introspect(&self, options: &IntrospectionOptions) -> CliResult<DatabaseSchema> {
1265 let pool = MssqlPool::builder()
1266 .connection_string(self.connection_string.clone())
1267 .connection_timeout(std::time::Duration::from_secs(
1268 super::INTROSPECT_CONNECT_TIMEOUT_SECS,
1269 ))
1270 .build()
1271 .await
1272 .map_err(|e| CliError::Unreachable(format!("Failed to connect: {}", e)))?;
1273 let mut conn = pool.get().await.map_err(|e| {
1274 CliError::Unreachable(format!("Failed to acquire connection: {}", e))
1275 })?;
1276
1277 let schema_name = options.schema.clone().unwrap_or_else(|| "dbo".to_string());
1278 let schema_ref = Some(schema_name.as_str());
1279
1280 let mut db_schema = DatabaseSchema {
1281 name: "database".to_string(),
1282 schema: Some(schema_name.clone()),
1283 ..Default::default()
1284 };
1285
1286 let table_rows = conn
1287 .query(&queries::tables_query(DatabaseType::MSSQL, schema_ref), &[])
1288 .await
1289 .map_err(|e| CliError::Database(format!("Failed to query tables: {}", e)))?;
1290 for row in &table_rows {
1291 let Some(table_name) = row_str(row, "table_name") else {
1292 continue;
1293 };
1294 if let Some(ref pattern) = options.table_filter
1295 && !matches_pattern(&table_name, pattern)
1296 {
1297 continue;
1298 }
1299 if let Some(ref exclude) = options.exclude_pattern
1300 && matches_pattern(&table_name, exclude)
1301 {
1302 continue;
1303 }
1304 db_schema.tables.push(TableInfo {
1305 name: table_name,
1306 schema: Some(schema_name.clone()),
1307 comment: if options.include_comments {
1308 row_str(row, "comment")
1309 } else {
1310 None
1311 },
1312 ..Default::default()
1313 });
1314 }
1315
1316 for i in 0..db_schema.tables.len() {
1317 let table_name = db_schema.tables[i].name.clone();
1318
1319 let col_rows = conn
1321 .query(
1322 &queries::columns_query(DatabaseType::MSSQL, &table_name, schema_ref),
1323 &[],
1324 )
1325 .await
1326 .map_err(|e| CliError::Database(format!("Failed to query columns: {}", e)))?;
1327 for row in &col_rows {
1328 let Some(name) = row_str(row, "column_name") else {
1329 continue;
1330 };
1331 let data_type = row_str(row, "data_type").unwrap_or_default();
1332 let max_length = row_i32(row, "character_maximum_length");
1339 let precision = row_i32(row, "numeric_precision");
1340 let scale = row_i32(row, "numeric_scale");
1341 let normalized = normalize_type(
1342 DatabaseType::MSSQL,
1343 &data_type,
1344 max_length,
1345 precision,
1346 scale,
1347 );
1348 db_schema.tables[i].columns.push(ColumnInfo {
1349 name,
1350 db_type: data_type,
1351 normalized_type: normalized,
1352 nullable: row_bool(row, "nullable"),
1353 default: row_str(row, "column_default"),
1354 auto_increment: row_bool(row, "auto_increment"),
1355 max_length,
1356 precision,
1357 scale,
1358 comment: if options.include_comments {
1359 row_str(row, "comment")
1360 } else {
1361 None
1362 },
1363 ..Default::default()
1364 });
1365 }
1366
1367 let pk_rows = conn
1369 .query(
1370 &queries::primary_keys_query(DatabaseType::MSSQL, &table_name, schema_ref),
1371 &[],
1372 )
1373 .await
1374 .map_err(|e| {
1375 CliError::Database(format!("Failed to query primary keys: {}", e))
1376 })?;
1377 for row in &pk_rows {
1378 if let Some(col) = row_str(row, "column_name") {
1379 db_schema.tables[i].primary_key.push(col.clone());
1380 if let Some(c) = db_schema.tables[i]
1381 .columns
1382 .iter_mut()
1383 .find(|c| c.name == col)
1384 {
1385 c.is_primary_key = true;
1386 }
1387 }
1388 }
1389
1390 let fk_rows = conn
1392 .query(
1393 &queries::foreign_keys_query(DatabaseType::MSSQL, &table_name, schema_ref),
1394 &[],
1395 )
1396 .await
1397 .map_err(|e| {
1398 CliError::Database(format!("Failed to query foreign keys: {}", e))
1399 })?;
1400 let mut fk_map: std::collections::HashMap<String, ForeignKeyInfo> =
1401 std::collections::HashMap::new();
1402 let mut fk_order: Vec<String> = Vec::new();
1403 for row in &fk_rows {
1404 let Some(cname) = row_str(row, "constraint_name") else {
1405 continue;
1406 };
1407 let fk = fk_map.entry(cname.clone()).or_insert_with(|| {
1408 fk_order.push(cname.clone());
1409 ForeignKeyInfo {
1410 name: cname.clone(),
1411 columns: Vec::new(),
1412 referenced_table: row_str(row, "referenced_table").unwrap_or_default(),
1413 referenced_schema: row_str(row, "referenced_schema"),
1414 referenced_columns: Vec::new(),
1415 on_delete: ReferentialAction::from_str(
1416 &row_str(row, "delete_rule").unwrap_or_default(),
1417 ),
1418 on_update: ReferentialAction::from_str(
1419 &row_str(row, "update_rule").unwrap_or_default(),
1420 ),
1421 }
1422 });
1423 if let Some(col) = row_str(row, "column_name") {
1424 fk.columns.push(col);
1425 }
1426 if let Some(rc) = row_str(row, "referenced_column") {
1427 fk.referenced_columns.push(rc);
1428 }
1429 }
1430 db_schema.tables[i].foreign_keys = fk_order
1431 .into_iter()
1432 .filter_map(|n| fk_map.remove(&n))
1433 .collect();
1434
1435 let idx_rows = conn
1437 .query(
1438 &queries::indexes_query(DatabaseType::MSSQL, &table_name, schema_ref),
1439 &[],
1440 )
1441 .await
1442 .map_err(|e| CliError::Database(format!("Failed to query indexes: {}", e)))?;
1443 let mut idx_map: std::collections::HashMap<String, IndexInfo> =
1444 std::collections::HashMap::new();
1445 let mut idx_order: Vec<String> = Vec::new();
1446 for row in &idx_rows {
1447 let Some(iname) = row_str(row, "index_name") else {
1448 continue;
1449 };
1450 let idx = idx_map.entry(iname.clone()).or_insert_with(|| {
1451 idx_order.push(iname.clone());
1452 IndexInfo {
1453 name: iname.clone(),
1454 columns: Vec::new(),
1455 is_unique: row_bool(row, "is_unique"),
1456 is_primary: row_bool(row, "is_primary"),
1457 index_type: row_str(row, "index_type"),
1458 filter: row_str(row, "filter"),
1459 }
1460 });
1461 if let Some(col) = row_str(row, "column_name") {
1462 idx.columns.push(IndexColumn {
1463 name: col,
1464 order: SortOrder::Asc,
1465 ..Default::default()
1466 });
1467 }
1468 }
1469 db_schema.tables[i].indexes = idx_order
1470 .into_iter()
1471 .filter_map(|n| idx_map.remove(&n))
1472 .collect();
1473 }
1474
1475 Ok(db_schema)
1476 }
1477 }
1478}
1479
1480pub fn format_as_prax(schema: &DatabaseSchema, config: &Config) -> String {
1486 let mut output = String::new();
1487
1488 output.push_str("// Generated by `prax db pull`\n");
1489 output.push_str("// Edit this file to customize your schema\n\n");
1490
1491 output.push_str("datasource db {\n");
1492 output.push_str(&format!(
1493 " provider = \"{}\"\n",
1494 config.database.provider
1495 ));
1496 output.push_str(" url = env(\"DATABASE_URL\")\n");
1497 output.push_str("}\n\n");
1498
1499 output.push_str("generator client {\n");
1500 output.push_str(" provider = \"prax-client-rust\"\n");
1501 output.push_str(" output = \"./src/generated\"\n");
1502 output.push_str("}\n\n");
1503
1504 output.push_str(&generate_prax_schema(schema));
1506
1507 output
1508}
1509
1510pub fn format_as_json(schema: &DatabaseSchema) -> CliResult<String> {
1512 serde_json::to_string_pretty(schema)
1513 .map_err(|e| CliError::Config(format!("Failed to serialize schema: {}", e)))
1514}
1515
1516pub fn format_as_sql(schema: &DatabaseSchema, db_type: DatabaseType) -> String {
1518 let mut output = String::new();
1519
1520 output.push_str("-- Generated by `prax db pull`\n");
1521 output.push_str(&format!("-- Database: {}\n\n", db_type_name(db_type)));
1522
1523 if db_type == DatabaseType::PostgreSQL {
1525 for enum_info in &schema.enums {
1526 output.push_str(&format!("CREATE TYPE {} AS ENUM (\n", enum_info.name));
1527 let values: Vec<String> = enum_info
1528 .values
1529 .iter()
1530 .map(|v| format!(" '{}'", v))
1531 .collect();
1532 output.push_str(&values.join(",\n"));
1533 output.push_str("\n);\n\n");
1534 }
1535 }
1536
1537 for table in &schema.tables {
1539 output.push_str(&format!(
1540 "CREATE TABLE {} (\n",
1541 quote_identifier(&table.name, db_type)
1542 ));
1543
1544 let mut col_defs: Vec<String> = Vec::new();
1545
1546 for col in &table.columns {
1547 let mut def = format!(
1548 " {} {}",
1549 quote_identifier(&col.name, db_type),
1550 col.db_type
1551 );
1552
1553 if !col.nullable {
1554 def.push_str(" NOT NULL");
1555 }
1556
1557 if let Some(ref default) = col.default {
1558 def.push_str(&format!(" DEFAULT {}", default));
1559 }
1560
1561 col_defs.push(def);
1562 }
1563
1564 if !table.primary_key.is_empty() {
1566 let pk_cols: Vec<String> = table
1567 .primary_key
1568 .iter()
1569 .map(|c| quote_identifier(c, db_type))
1570 .collect();
1571 col_defs.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
1572 }
1573
1574 output.push_str(&col_defs.join(",\n"));
1575 output.push_str("\n);\n\n");
1576
1577 for idx in &table.indexes {
1579 if idx.is_primary {
1580 continue;
1581 }
1582
1583 let unique = if idx.is_unique { "UNIQUE " } else { "" };
1584 let cols: Vec<String> = idx
1585 .columns
1586 .iter()
1587 .map(|c| quote_identifier(&c.name, db_type))
1588 .collect();
1589
1590 output.push_str(&format!(
1591 "CREATE {}INDEX {} ON {} ({});\n",
1592 unique,
1593 quote_identifier(&idx.name, db_type),
1594 quote_identifier(&table.name, db_type),
1595 cols.join(", ")
1596 ));
1597 }
1598
1599 output.push('\n');
1600 }
1601
1602 output
1603}
1604
1605fn db_type_name(db_type: DatabaseType) -> &'static str {
1606 match db_type {
1607 DatabaseType::PostgreSQL => "PostgreSQL",
1608 DatabaseType::MySQL => "MySQL",
1609 DatabaseType::SQLite => "SQLite",
1610 DatabaseType::MSSQL => "SQL Server",
1611 }
1612}
1613
1614fn quote_identifier(name: &str, db_type: DatabaseType) -> String {
1615 match db_type {
1616 DatabaseType::PostgreSQL => format!("\"{}\"", name),
1617 DatabaseType::MySQL => format!("`{}`", name),
1618 DatabaseType::SQLite => format!("\"{}\"", name),
1619 DatabaseType::MSSQL => format!("[{}]", name),
1620 }
1621}