= select()
:description: Executes a parameterized SQL statement via ado.ExecuteSQL. Returns a list of dicts. Supports %s placeholders for safe parameter binding.
:keywords: select, ado.ExecuteSQL, SQL, placeholders, parameter binding, dict, SQL injection
:page-tags: api, db

Sends a SQL statement to the server via `ado.ExecuteSQL` and returns the parsed
ADO Recordset XML as an `ECMDbResult`. Positional parameters are safely bound
into the query before it is sent — never use string formatting for user input.

NOTE: Only SELECT queries are supported without additional server configuration.
Data manipulation statements (INSERT, UPDATE, DELETE) must be explicitly enabled
via a registry setting on the application server.

== Signature

[tabs]
====
Sync::
+
[source,python]
----
ecm.db.select(sql: str, *params: int | str | float, flags: int = 0) -> ECMDbResult
----

Async::
+
[source,python]
----
await ecm.db.select(sql: str, *params: int | str | float, flags: int = 0) -> ECMDbResult
----
====

== Parameters

[cols="1,1,1,3"]
|===
|Parameter |Type |Default |Description

|`sql`
|`str`
|—
|SQL template string with optional placeholders.

|`*params`
|`int \| str \| float`
|—
|Values substituted into the placeholders in order. The number of values must match the number of non-`%%` placeholders.

|`flags`
|`int`
|`0`
|Bitmask passed as `Flags` to `ado.ExecuteSQL`. `0` = automatic `N`-prefix for string literals on Unicode servers (default). `2` = disable automatic `N`-prefixing.
|===

== Placeholders

[cols="1,1,3"]
|===
|Placeholder |Type |Behaviour

|`%s`
|String
|Single quotes are escaped (`'` → `''`); value is enclosed in `'...'`.

|`%w`
|Word string
|Like `%s`, but additionally rejects whitespace and SQL comment markers (`--`, `/*`, `*/`).

|`%d`
|Integer
|Value is cast to `int`; non-numeric values raise `ValueError`.

|`%f`
|Float
|Value is cast to `float`; non-numeric values raise `ValueError`.

|`%u`
|Unquoted identifier
|Only letters, digits, `_`, `@`, `$`, `#` are allowed. Value is inserted without quotes. Use for table or column names.

|`%%`
|Literal `%`
|No parameter is consumed.
|===

== Return value

An `ECMDbResult` instance containing columns and rows.

=== ECMDbResult

[cols="1,1,3"]
|===
|Attribute / Method |Type |Description

|`.columns`
|`list[ECMDbColumn]`
|Ordered list of column definitions, sorted by 1-based column index.

|`.rows`
|`list[ECMDbRow]`
|List of result rows, one per `<z:row>` element.

|`len(result)`
|`int`
|Number of rows.

|`for row in result`
|—
|Iterates over `.rows` directly.
|===

=== ECMDbColumn fields

[cols="1,1,3"]
|===
|Field |Type |Description

|`name`
|`str`
|Column name as returned by the server.

|`number`
|`int`
|1-based column index.

|`data_type`
|`str`
|ADO data type string, e.g. `"string"`, `"int"`, `"dateTime"`.

|`max_length`
|`int \| None`
|Maximum byte length declared in the schema, or `None` if absent.

|`nullable`
|`bool`
|Whether the column allows `NULL` values.

|`base_table`
|`str \| None`
|Source table name, or `None` if not provided by the schema.

|`base_column`
|`str \| None`
|Source column name, or `None` if not provided by the schema.
|===

=== ECMDbRow access

[cols="1,1,3"]
|===
|Method / Syntax |Return type |Description

|`row["column_name"]`
|`str \| None`
|Raw string value, or `None` for `NULL`.

|`row.get("column_name")`
|`str \| None`
|Same as `__getitem__` — raw string or `None` for missing / `NULL`.

|`row.typed("column_name", str)`
|`str \| None`
|Value coerced to the given type. Supported: `str`, `int`, `float`, `bool`, `datetime`.
|===

`NULL` values are always returned as `None`, regardless of access method.

== Examples

=== Simple query

[tabs]
====
Sync::
+
[source,python]
----
result = ecm.db.select("SELECT benutzer, osemail FROM benutzer")
print(f"{len(result)} rows")
for row in result:
    print(row["benutzer"], row["osemail"])
----

Async::
+
[source,python]
----
result = await ecm.db.select("SELECT benutzer, osemail FROM benutzer")
print(f"{len(result)} rows")
for row in result:
    print(row["benutzer"], row["osemail"])
----
====

=== Query with parameters

[tabs]
====
Sync::
+
[source,python]
----
result = ecm.db.select(
    "SELECT id, benutzer FROM benutzer WHERE benutzer = %s AND aktiv = %d",
    "john",
    1,
)
for row in result:
    print(row["id"], row["benutzer"])
----

Async::
+
[source,python]
----
result = await ecm.db.select(
    "SELECT id, benutzer FROM benutzer WHERE benutzer = %s AND aktiv = %d",
    "john",
    1,
)
for row in result:
    print(row["id"], row["benutzer"])
----
====

=== Typed value access

[tabs]
====
Sync::
+
[source,python]
----
result = ecm.db.select("SELECT benutzer, aktiv, logincount FROM benutzer")
for row in result:
    username: str | None = row.typed("benutzer", str)
    active: bool | None = row.typed("aktiv", bool)
    login_count: int | None = row.typed("logincount", int)
    print(username, active, login_count)
----

Async::
+
[source,python]
----
result = await ecm.db.select("SELECT benutzer, aktiv, logincount FROM benutzer")
for row in result:
    username: str | None = row.typed("benutzer", str)
    active: bool | None = row.typed("aktiv", bool)
    login_count: int | None = row.typed("logincount", int)
    print(username, active, login_count)
----
====

=== Dynamic table name

[tabs]
====
Sync::
+
[source,python]
----
# %u inserts an unquoted identifier — only safe characters allowed
table = "benutzer"
result = ecm.db.select("SELECT * FROM %u WHERE id = %d", table, 42)
----

Async::
+
[source,python]
----
table = "benutzer"
result = await ecm.db.select("SELECT * FROM %u WHERE id = %d", table, 42)
----
====

=== Inspect column metadata

[tabs]
====
Sync::
+
[source,python]
----
result = ecm.db.select("SELECT * FROM benutzer WHERE 1 = 0")
for col in result.columns:
    print(col.name, col.data_type, col.max_length, col.nullable)
----

Async::
+
[source,python]
----
result = await ecm.db.select("SELECT * FROM benutzer WHERE 1 = 0")
for col in result.columns:
    print(col.name, col.data_type, col.max_length, col.nullable)
----
====

== See also

* xref:api/db/index.adoc[ecm.db] — Namespace overview with SQL injection protection notes
