Skip to content

Commit 525834e

Browse files
committed
Schema Introspection
1 parent 89ecd09 commit 525834e

File tree

6 files changed

+161
-0
lines changed

6 files changed

+161
-0
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ $posts = $driver->queryAll('SELECT * FROM posts WHERE author_id = ?', [12345]);
8585
- Optional persistent connections (with connection pooling)
8686
- **Automatic retry** with exponential backoff for transient failures
8787
- Custom `SqlxException` class with error codes for precise error handling
88+
- **Schema introspection** via `describeTable()` for table column metadata
8889

8990
---
9091

@@ -726,6 +727,30 @@ $driver = Sqlx\DriverFactory::make([
726727
for
727728
debugging.
728729

730+
#### Schema Introspection
731+
732+
- `describeTable(string $table, ?string $schema = null): array` – returns column metadata for the specified table.
733+
734+
```php
735+
$columns = $driver->describeTable('users');
736+
// Returns:
737+
// [
738+
// ['name' => 'id', 'type' => 'integer', 'nullable' => false, 'default' => null, 'ordinal' => 1],
739+
// ['name' => 'email', 'type' => 'varchar(255)', 'nullable' => false, 'default' => null, 'ordinal' => 2],
740+
// ['name' => 'created_at', 'type' => 'timestamp', 'nullable' => true, 'default' => 'now()', 'ordinal' => 3],
741+
// ]
742+
743+
// With explicit schema
744+
$columns = $driver->describeTable('users', 'public');
745+
```
746+
747+
Each column entry contains:
748+
- `name` – Column name (string)
749+
- `type` – Database-specific type (string), e.g., `varchar(255)`, `integer`, `timestamp`
750+
- `nullable` – Whether `NULL` is allowed (bool)
751+
- `default` – Default value expression (string|null)
752+
- `ordinal` – 1-based column position (int)
753+
729754
---
730755

731756
### Sqlx\PreparedQuery

src/dbms/mssql/inner.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
#![allow(clippy::needless_pass_by_value)]
22
use crate::php_sqlx_impl_driver_inner;
33

4+
/// SQL query to describe table columns using `information_schema`.
5+
pub const DESCRIBE_TABLE_QUERY: &str = r"
6+
SELECT
7+
column_name AS name,
8+
data_type + COALESCE('(' + CAST(character_maximum_length AS VARCHAR) + ')', '') AS type,
9+
CASE WHEN is_nullable = 'YES' THEN 1 ELSE 0 END AS nullable,
10+
column_default AS [default],
11+
ordinal_position AS ordinal
12+
FROM information_schema.columns
13+
WHERE table_name = $table
14+
AND table_schema = COALESCE($schema, SCHEMA_NAME())
15+
ORDER BY ordinal_position
16+
";
17+
418
pub const SETTINGS: Settings = Settings {
519
collapsible_in_enabled: true,
620
escaping_double_single_quotes: true,

src/dbms/mysql/inner.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
use crate::php_sqlx_impl_driver_inner;
44

5+
/// SQL query to describe table columns using `information_schema`.
6+
pub const DESCRIBE_TABLE_QUERY: &str = r"
7+
SELECT
8+
column_name AS name,
9+
column_type AS type,
10+
is_nullable = 'YES' AS nullable,
11+
column_default AS `default`,
12+
ordinal_position AS ordinal
13+
FROM information_schema.columns
14+
WHERE table_name = $table
15+
AND table_schema = COALESCE($schema, DATABASE())
16+
ORDER BY ordinal_position
17+
";
18+
519
pub const SETTINGS: Settings = Settings {
620
collapsible_in_enabled: true,
721
escaping_double_single_quotes: true,

src/dbms/postgres/inner.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
#![allow(clippy::needless_pass_by_value)]
22
use crate::php_sqlx_impl_driver_inner;
3+
4+
/// SQL query to describe table columns using `information_schema`.
5+
pub const DESCRIBE_TABLE_QUERY: &str = r"
6+
SELECT
7+
column_name AS name,
8+
data_type || COALESCE('(' || character_maximum_length::text || ')', '') AS type,
9+
is_nullable = 'YES' AS nullable,
10+
column_default AS default,
11+
ordinal_position AS ordinal
12+
FROM information_schema.columns
13+
WHERE table_name = $table
14+
AND table_schema = COALESCE($schema, current_schema())
15+
ORDER BY ordinal_position
16+
";
17+
318
pub const SETTINGS: Settings = Settings {
419
collapsible_in_enabled: true,
520
escaping_double_single_quotes: true,

src/driver.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,48 @@ macro_rules! php_sqlx_impl_driver {
921921
.query_grouped_column_dictionary(query, parameters, None)
922922
}
923923

924+
/// Describes table columns with their types and metadata.
925+
///
926+
/// Returns information about each column in the specified table, including
927+
/// name, type, nullability, default value, and ordinal position.
928+
///
929+
/// # Parameters
930+
/// - `table`: Name of the table to describe.
931+
/// - `schema`: Optional schema name. If `None`, uses the database default schema.
932+
///
933+
/// # Returns
934+
/// An array of associative arrays, each containing:
935+
/// - `name`: Column name (string)
936+
/// - `type`: Database-specific column type (string, e.g., "varchar(255)", "int")
937+
/// - `nullable`: Whether the column allows NULL values (bool)
938+
/// - `default`: Default value for the column (string|null)
939+
/// - `ordinal`: Column position, 1-based (int)
940+
///
941+
/// # Example
942+
/// ```php
943+
/// $columns = $driver->describeTable('users');
944+
/// // [
945+
/// // ['name' => 'id', 'type' => 'integer', 'nullable' => false, 'default' => null, 'ordinal' => 1],
946+
/// // ['name' => 'email', 'type' => 'varchar(255)', 'nullable' => false, 'default' => null, 'ordinal' => 2],
947+
/// // ]
948+
///
949+
/// // With schema
950+
/// $columns = $driver->describeTable('users', 'public');
951+
/// ```
952+
///
953+
/// # Exceptions
954+
/// Throws an exception if:
955+
/// - the table or schema name is invalid (contains invalid characters);
956+
/// - the query fails to execute;
957+
/// - the table does not exist.
958+
pub fn describe_table(
959+
&self,
960+
table: &str,
961+
schema: Option<&str>,
962+
) -> $crate::error::Result<Vec<Zval>> {
963+
self.driver_inner.describe_table(table, schema)
964+
}
965+
924966
/// Executes an INSERT/UPDATE/DELETE query and returns affected row count.
925967
///
926968
/// # Arguments

src/inner_driver.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,57 @@ macro_rules! php_sqlx_impl_driver_inner {
904904
})
905905
}
906906

907+
/// Describes table columns with their types and metadata.
908+
///
909+
/// Returns information about each column in the specified table, including
910+
/// name, type, nullability, default value, and ordinal position.
911+
///
912+
/// # Parameters
913+
/// - `table_name`: Name of the table to describe.
914+
/// - `schema`: Optional schema name. If `None`, uses the database default schema.
915+
///
916+
/// # Returns
917+
/// An array of associative arrays, each containing:
918+
/// - `name`: Column name
919+
/// - `type`: Database-specific column type (e.g., "varchar(255)", "int")
920+
/// - `nullable`: Whether the column allows NULL values
921+
/// - `default`: Default value for the column, or NULL if none
922+
/// - `ordinal`: Column position (1-based)
923+
///
924+
/// # Errors
925+
/// Returns an error if:
926+
/// - the table or schema name is invalid (contains invalid characters);
927+
/// - the query fails to execute;
928+
/// - the table does not exist.
929+
pub fn describe_table(
930+
&self,
931+
table_name: &str,
932+
schema: Option<&str>,
933+
) -> $crate::error::Result<Vec<Zval>> {
934+
// Validate identifiers to prevent SQL injection
935+
if !is_valid_ident(table_name) {
936+
return Err(SqlxError::InvalidIdentifier {
937+
value: table_name.to_string(),
938+
});
939+
}
940+
if let Some(s) = schema {
941+
if !is_valid_ident(s) {
942+
return Err(SqlxError::InvalidIdentifier {
943+
value: s.to_string(),
944+
});
945+
}
946+
}
947+
948+
let mut params = BTreeMap::new();
949+
params.insert("table".to_string(), ParameterValue::String(table_name.to_string()));
950+
params.insert(
951+
"schema".to_string(),
952+
schema.map_or(ParameterValue::Null, |s| ParameterValue::String(s.to_string())),
953+
);
954+
955+
self.query_all(DESCRIBE_TABLE_QUERY, Some(params), Some(true))
956+
}
957+
907958
/// Begins a new SQL transaction and places it into the transaction stack.
908959
///
909960
/// This method must be called before executing transactional operations

0 commit comments

Comments
 (0)