diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ac74a5b51..5cf36d651 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -178,7 +178,8 @@ A user can be restricted to a subset of clients through `user_client_permissions
**A list — `clientScopeSql()`.** Any query returning more than one row appends the fragment for that resource's own client column:
```php
-$sql = mysqli_query($mysqli, "SELECT * FROM expenses
+$sql = mysqli_query($mysqli, "SELECT expense_id, expense_date, expense_amount, expense_description
+ FROM expenses
WHERE expense_archived_at IS NULL
" . clientScopeSql('expense_client_id') . "
ORDER BY expense_date DESC");
@@ -224,7 +225,26 @@ Per [SECURITY.md](SECURITY.md) — never in a public issue.
## Conventions
-**Database naming.** Every column is prefixed with the singular name of the entity it belongs to: `tickets.ticket_id`, `tickets.ticket_subject`, `clients.client_name`. This makes JOIN results unambiguous and is why queries can `SELECT *` across joins safely. New tables must follow it.
+**Database naming.** Every column is prefixed with the singular name of the entity it belongs to: `tickets.ticket_id`, `tickets.ticket_subject`, `clients.client_name`. This makes JOIN results unambiguous, so a `SELECT *` across joins is never *wrong*. New tables must follow it.
+
+**Select the columns you use, not `*`.** Unambiguous is not the same as cheap. `SELECT *` across three joined tables fetches every column of all three, including the `*_notes` and `*_details` TEXT columns, and throws away whatever the page never renders. A search result list that shows five fields was pulling sixty. List the columns instead:
+
+```php
+$sql = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket_number, ticket_subject, client_name
+ FROM tickets
+ LEFT JOIN clients ON ticket_client_id = client_id
+ WHERE ticket_archived_at IS NULL
+ " . clientScopeSql('ticket_client_id') . "");
+```
+
+Two things follow from that:
+
+- A query whose result only feeds `mysqli_num_rows()` needs no columns at all — write `SELECT 1`. Do not select a primary key "just in case": if the query joins two tables that both carry that column name, an unqualified `SELECT ticket_template_id` is an ambiguous-column error.
+- Keep the join even when no column of the joined table survives into the `SELECT`, if the join is doing work — supplying a `WHERE` term, an `ORDER BY`, or the client column you scope on. Dropping a join is a separate decision from trimming the column list.
+
+The trade is real and worth stating: `SELECT *` picks up new columns for free, an explicit list does not. Add a column to a table and every query that needs it must be updated by hand, and the failure mode is a blank field or a PHP 8 undefined-key warning rather than an error. That is the price of not fetching data nobody reads, and the project has decided to pay it on anything that loops or touches a TEXT column.
+
+The exception is `api/v1/*/read.php`. Those endpoints hand the whole row to `read_output.php`, which serialises it straight into the JSON response — there the row *is* the output contract, so `SELECT *` is correct and trimming it would silently drop fields from every consumer.
The prefix is the entity name, which is usually but not always the singular of the table name. Where a table is named for its container rather than its row, the prefix follows the row: `calendar_events` → `event_*`, `asset_interfaces` → `interface_*`, `invoice_items` / `quote_items` → `item_*`, `rack_units` → `unit_*`, `user_roles` → `role_*`, `product_stock` → `stock_*`. Pick the prefix your columns will read best as and use it for every column in the table.
diff --git a/admin/ai_providers.php b/admin/ai_providers.php
index 3bb6b5729..b9b728314 100644
--- a/admin/ai_providers.php
+++ b/admin/ai_providers.php
@@ -6,7 +6,7 @@ $order = "ASC";
require_once "includes/inc_all_admin.php";
-$sql = mysqli_query($mysqli, "SELECT * FROM ai_providers ORDER BY $sort $order");
+$sql = mysqli_query($mysqli, "SELECT ai_provider_api_key, ai_provider_api_url, ai_provider_id, ai_provider_name FROM ai_providers ORDER BY $sort $order");
$num_rows = mysqli_num_rows($sql);
diff --git a/admin/modals/user/user_archive.php b/admin/modals/user/user_archive.php
index 02637e8d9..f93205894 100644
--- a/admin/modals/user/user_archive.php
+++ b/admin/modals/user/user_archive.php
@@ -12,13 +12,13 @@ $user_email = escapeHtml($row['user_email']);
$user_avatar = escapeHtml($row['user_avatar']);
$user_initials = escapeHtml(initials($user_name));
-$sql_related_tickets = mysqli_query($mysqli, "SELECT * FROM tickets
+$sql_related_tickets = mysqli_query($mysqli, "SELECT 1 FROM tickets
WHERE ticket_assigned_to = $user_id AND ticket_resolved_at IS NULL AND ticket_closed_at IS NULL");
$ticket_count = mysqli_num_rows($sql_related_tickets);
// Related Recurring Tickets Query
-$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT * FROM recurring_tickets WHERE recurring_ticket_assigned_to = $user_id");
+$sql_related_recurring_tickets = mysqli_query($mysqli, "SELECT 1 FROM recurring_tickets WHERE recurring_ticket_assigned_to = $user_id");
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
diff --git a/admin/payment_providers.php b/admin/payment_providers.php
index e80a62ea3..0f7545f03 100644
--- a/admin/payment_providers.php
+++ b/admin/payment_providers.php
@@ -6,7 +6,8 @@ $order = "ASC";
require_once "includes/inc_all_admin.php";
-$sql = mysqli_query($mysqli, "SELECT * FROM payment_providers
+$sql = mysqli_query($mysqli, "SELECT account_name, category_name, payment_provider_description, payment_provider_id,
+ payment_provider_name, payment_provider_threshold, vendor_name FROM payment_providers
LEFT JOIN accounts ON payment_provider_account = account_id
LEFT JOIN vendors ON payment_provider_expense_vendor = vendor_id
LEFT JOIN categories ON payment_provider_expense_category = category_id
diff --git a/admin/users.php b/admin/users.php
index 14e49f3d5..954ae331e 100644
--- a/admin/users.php
+++ b/admin/users.php
@@ -8,7 +8,8 @@ require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
- "SELECT SQL_CALC_FOUND_ROWS * FROM users
+ "SELECT SQL_CALC_FOUND_ROWS role_name, user_archived_at, user_avatar, user_config_force_mfa, user_email,
+ user_settings.user_id, user_name, user_role_id, user_status, user_token FROM users
LEFT JOIN user_roles ON user_role_id = role_id
LEFT JOIN user_settings ON users.user_id = user_settings.user_id
WHERE (user_name LIKE '%$q%' OR user_email LIKE '%$q%')
diff --git a/agent/accounts.php b/agent/accounts.php
index ef8301d71..bae78d76b 100644
--- a/agent/accounts.php
+++ b/agent/accounts.php
@@ -11,7 +11,7 @@ enforceUserPermission('module_financial');
$sql = mysqli_query(
$mysqli,
- "SELECT SQL_CALC_FOUND_ROWS * FROM accounts
+ "SELECT SQL_CALC_FOUND_ROWS account_currency_code, account_id, account_name, account_notes, opening_balance FROM accounts
WHERE (account_name LIKE '%$q%')
AND account_archived_at IS NULL
ORDER BY $sort $order LIMIT $record_from, $record_to"
diff --git a/agent/asset.php b/agent/asset.php
index 2e8e20976..d7e0e3bdc 100644
--- a/agent/asset.php
+++ b/agent/asset.php
@@ -122,7 +122,7 @@ if (isset($_GET['asset_id'])) {
$recurring_ticket_count = mysqli_num_rows($sql_related_recurring_tickets);
// Related Documents
- $sql_related_documents = mysqli_query($mysqli, "SELECT * FROM asset_documents
+ $sql_related_documents = mysqli_query($mysqli, "SELECT 1 FROM asset_documents
LEFT JOIN documents ON asset_documents.document_id = documents.document_id
WHERE asset_documents.asset_id = $asset_id
AND document_archived_at IS NULL
@@ -252,7 +252,9 @@ if (isset($_GET['asset_id'])) {
// Related Software Query
$sql_related_software = mysqli_query(
$mysqli,
- "SELECT * FROM software_assets
+ "SELECT software_expire, software_assets.software_id, software_key, software_license_type,
+ software_name, software_notes, software_purchase, software_seats, software_type,
+ software_version FROM software_assets
LEFT JOIN software ON software_assets.software_id = software.software_id
WHERE software_assets.asset_id = $asset_id
AND software_archived_at IS NULL
diff --git a/agent/assets.php b/agent/assets.php
index c80b2a093..1705d0ade 100644
--- a/agent/assets.php
+++ b/agent/assets.php
@@ -641,7 +641,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$location_name_display = $location_name;
}
- $sql_credentials = mysqli_query($mysqli, "SELECT * FROM credentials WHERE credential_asset_id = $asset_id");
+ $sql_credentials = mysqli_query($mysqli, "SELECT 1 FROM credentials WHERE credential_asset_id = $asset_id");
$credential_count = mysqli_num_rows($sql_credentials);
// Tags
diff --git a/agent/contact.php b/agent/contact.php
index 2db7bee17..fc7aca669 100644
--- a/agent/contact.php
+++ b/agent/contact.php
@@ -83,7 +83,9 @@ if (isset($_GET['contact_id'])) {
$asset_count = mysqli_num_rows($sql_related_assets);
// Linked Software Licenses
- $sql_linked_software = mysqli_query($mysqli, "SELECT * FROM software_contacts, software
+ $sql_linked_software = mysqli_query($mysqli, "SELECT software_expire, software_contacts.software_id, software_key, software_license_type,
+ software_name, software_notes, software_purchase, software_seats, software_type,
+ software_version FROM software_contacts, software
WHERE software_contacts.contact_id = $contact_id
AND software_contacts.software_id = software.software_id
AND software_archived_at IS NULL
diff --git a/agent/dashboard.php b/agent/dashboard.php
index 237a497b1..61f336696 100644
--- a/agent/dashboard.php
+++ b/agent/dashboard.php
@@ -117,7 +117,7 @@ if ($user_config_dashboard_financial_enable == 1) {
$profit = $total_income - $total_expenses;
- $sql_accounts = mysqli_query($mysqli, "SELECT * FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
+ $sql_accounts = mysqli_query($mysqli, "SELECT account_id, account_name, opening_balance FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
$sql_latest_invoice_payments = mysqli_query($mysqli, "
SELECT client_name, invoice_number, invoice_prefix, payment_amount, payment_date FROM payments
diff --git a/agent/invoice.php b/agent/invoice.php
index c4129a555..660151858 100644
--- a/agent/invoice.php
+++ b/agent/invoice.php
@@ -130,8 +130,7 @@ if (isset($_GET['invoice_id'])) {
//Get billable, and unbilled tickets to add to invoice
$sql_tickets_billable = mysqli_query(
$mysqli, "
- SELECT
- *
+ SELECT 1
FROM
tickets
WHERE
@@ -199,7 +198,7 @@ if (isset($_GET['invoice_id'])) {
// Saved Payment Methods
$sql_saved_payment_methods = mysqli_query($mysqli, "
- SELECT * FROM client_saved_payment_methods
+ SELECT 1 FROM client_saved_payment_methods
LEFT JOIN payment_providers
ON client_saved_payment_methods.saved_payment_provider_id = payment_providers.payment_provider_id
WHERE saved_payment_client_id = $client_id
diff --git a/agent/modals/asset/asset.php b/agent/modals/asset/asset.php
index 7ad997ff0..0922e67fe 100644
--- a/agent/modals/asset/asset.php
+++ b/agent/modals/asset/asset.php
@@ -219,7 +219,9 @@ $file_count = mysqli_num_rows($sql_related_files);
// Related Software Query
$sql_related_software = mysqli_query(
$mysqli,
- "SELECT * FROM software_assets
+ "SELECT software_expire, software_assets.software_id, software_key, software_license_type,
+ software_name, software_notes, software_purchase, software_seats, software_type,
+ software_version FROM software_assets
LEFT JOIN software ON software_assets.software_id = software.software_id
WHERE software_assets.asset_id = $asset_id
AND software_archived_at IS NULL
diff --git a/agent/modals/contact/contact.php b/agent/modals/contact/contact.php
index e495ac928..c0027ac40 100644
--- a/agent/modals/contact/contact.php
+++ b/agent/modals/contact/contact.php
@@ -158,7 +158,7 @@ while ($row = mysqli_fetch_assoc($sql_note_type_icons)) {
}
// Linked Services
-$sql_linked_services = mysqli_query($mysqli, "SELECT * FROM service_contacts, services
+$sql_linked_services = mysqli_query($mysqli, "SELECT 1 FROM service_contacts, services
WHERE service_contacts.contact_id = $contact_id
AND service_contacts.service_id = services.service_id
ORDER BY service_name ASC"
diff --git a/agent/modals/payment/payment_add.php b/agent/modals/payment/payment_add.php
index 9845d9456..42862cf43 100644
--- a/agent/modals/payment/payment_add.php
+++ b/agent/modals/payment/payment_add.php
@@ -93,7 +93,7 @@ ob_start();
- Select an Account -
- Select an Account -
- Account -
- Select Account -
'$revenue_created_at' OR account_archived_at IS NULL) ORDER BY account_archived_at ASC, account_name ASC");
+ $sql_accounts = mysqli_query($mysqli, "SELECT account_archived_at, account_currency_code, account_id, account_name, opening_balance FROM accounts WHERE (account_archived_at > '$revenue_created_at' OR account_archived_at IS NULL) ORDER BY account_archived_at ASC, account_name ASC");
while ($row = mysqli_fetch_assoc($sql_accounts)) {
$account_id_select = intval($row['account_id']);
$account_name_select = escapeHtml($row['account_name']);
diff --git a/agent/modals/transfer/transfer_add.php b/agent/modals/transfer/transfer_add.php
index 1b0a84ff8..10217e7dc 100644
--- a/agent/modals/transfer/transfer_add.php
+++ b/agent/modals/transfer/transfer_add.php
@@ -50,7 +50,7 @@ ob_start();
- Account To -
'$transfer_created_at' OR account_archived_at IS NULL) ORDER BY account_archived_at ASC, account_name ASC");
+ $sql_accounts = mysqli_query($mysqli, "SELECT account_archived_at, account_id, account_name, opening_balance FROM accounts WHERE (account_archived_at > '$transfer_created_at' OR account_archived_at IS NULL) ORDER BY account_archived_at ASC, account_name ASC");
while ($row = mysqli_fetch_assoc($sql_accounts)) {
$account_id_select = intval($row['account_id']);
$account_name_select = escapeHtml($row['account_name']);
@@ -119,7 +119,7 @@ ob_start();