PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 90 · После параметров, транзакций и чтения результатов CUBRID
CUBRID: схема каталога, связи, представления, триггеры и права
Шесть рабочих задач: паспорт импорта, составные связи заказа, живой VIEW-каталог, триггер аудита, наследуемые поля и ограниченный пользователь; полный разбор cubrid_schema.
Сначала понять структуру, затем выполнять нужную операцию
В существующую базу нужно загрузить новый прайс. Но сначала важно понять: есть ли поле цены, как связаны заказ и позиции, почему после UPDATE появился журнал и что разрешено служебному читателю. Для этих вопросов нужен паспорт структуры базы — метаданные. Эта глава заканчивается работающим импортом Кабеля USB, точной выборкой заказа склада 1 №700, живым каталогом, журналом 3→7 и проверенным отказом записи для reader.
Таблица хранит строки: например, sku=USB-169, title=Кабель USB, price_cents=1290. Схема описывает, что title — колонка VARCHAR, sku — уникален, note допускает NULL. Метаданные — данные об этом описании. Их можно получить в массив PHP и принять решение: разрешено ли импортировать поле, какие колонки входят в ключ, какую часть существующей модели нужно учесть.
- Таблица товара: id, sku, title, price_cents, note; строка USB-169 / Кабель USB / 1290
- cubrid_schema + ATTRIBUTE → массив описаний; ATTR_NAME даёт имя колонки
- Сопоставляем поля поставщика, отдельно проверяем значения и разрешения
- Подготовленный INSERT → COMMIT → SELECT подтверждает сохранённое название, цену и NULL
| Что есть в базе | Что приходит в PHP | Как это используется |
|---|---|---|
| Колонка title VARCHAR(120) NOT NULL | ATTR_NAME=title, NON_NULL=«1», PRECISION=«120» | Показать поле в паспорте импорта; обязательность формы и разрешение редактирования задать отдельно. |
| Уникальный sku | CONSTRAINT с ATTR_NAME=sku, TYPE=«0» | Объяснить отказ дублирующего артикула; не путать с составным PK заказа. |
| PRIMARY KEY(warehouse,order_no) | Две записи PRIMARY_KEY с KEY_SEQ 1 и 2 | Построить понятное соответствие двух колонок JOIN заказа и позиций. |
| VIEW с WHERE qty>0 | QUERY_SPEC — определение запроса | Понять, почему после изменения остатков каталог показывает другой набор строк. |
Самый маленький запрос описания
// $db — успешное cubrid_connect; products — известная таблица вашей базы.
$fields = cubrid_schema($db, CUBRID_SCH_ATTRIBUTE, 'products');
if ($fields === false) { throw new RuntimeException('Схема недоступна'); }
foreach ($fields as $field) {
echo $field['ATTR_NAME'], "\n";
}Здесь нет выборки товаров: цикл выводит названия колонок. Константа CUBRID_SCH_ATTRIBUTE выбирает формат ответа. Квадратные скобки читают именованное значение ATTR_NAME из очередной строки описания. Строгое === false отделяет отказ от пустого массива. Создание соединения и первой таблицы разобрано в начале CUBRID, foreach — в циклах, подготовленные значения — в параметрах запроса.
Ниже сначала идут шесть полезных самостоятельных программ. Затем полная карточка cubrid_schema соединяет 19 типов из официальной страницы PHP и дополнение ATTR_WITH_SYNONYM проверенного драйвера. Разбор точных ключей разделён на группы, чтобы для одного NULL-ограничения не приходилось искать его среди триггеров и методов.
| Потребность | Видимый результат | Переход |
|---|---|---|
| Импорт поставщика | Кабель USB / 1290 коп.; discount отклонён | Паспорт импорта |
| Разобраться в связях | Заказ склада 1 №700: Кабель 3, Клемма 6 | Составные PK/FK и JOIN |
| Каталог доступных товаров | Клипса появляется после изменения qty с 0 на 2 | VIEW и QUERY_SPEC |
| Объяснить скрытую запись журнала | Одна запись 3→7, повторное 7 не журналируется | Триггер изменения |
| Разобрать наследуемую структуру | Кабель / A-2 / 7 и происхождение title/qty | Классы, атрибуты, методы |
| Дать отчёту чтение без записи | SELECT работает, UPDATE отказан, остаток 3 | Права и synonym |
Профиль воспроизведения исторический и отдельный: PHP 7.4.33 CLI, native cubrid 11.2.0.0002, CCI 11.2.0, CUBRID 11.3.5. Он не требует понижения PHP сайта. Общий lab.php проверяет изолированную учебную базу и точный реестр собственных объектов; README содержит подготовку окружения. Новые схемы создаются отдельно для каждого примера и удаляются в finally, включая VIEW, TRIGGER, USER и SYNONYM. Метаданные других баз не публикуются.
Паспорт импорта: какие поля существуют и что сохранится
Поставщик прислал поля товара. До загрузки нужно обнаружить неизвестный discount, сохранить название и цену, а не полагаться на порядок столбцов CSV. Паспорт схемы покажет пять полей; программа примет Кабель USB за 1290 копеек и отдельно покажет DEFAULT у черновика.
- Схема: id, sku, title, price_cents, note → список допустимых имён
- Вход USB-169 / Кабель USB / 1290 → явная проверка приложения
- INSERT именованных полей → COMMIT → две прочитанные строки
- Другой вход с discount → отказ до INSERT; missing169 → успешный пустой массив
Маленький вызов перед полной задачей
$fields = cubrid_schema($db, CUBRID_SCH_ATTRIBUTE, $tableName);
if ($fields === false) { throw new RuntimeException('Не прочитана схема'); }
foreach ($fields as $field) {
echo $field['ATTR_NAME'], "\n"; // id, sku, title, price_cents, note
}Первый аргумент выбирает соединение, константа — вид сведений, третий аргумент — имя таблицы без SQL-кавычек. Возвращается массив строк описания, а не товары. foreach читает каждое описание; ATTR_NAME — ключ с именем поля. В полном примере список используется для проверки входа, а INSERT по-прежнему перечисляет поля явно.
Что увидит читатель после запуска
| Наблюдение | Результат |
|---|---|
| Полей в таблице | 5 |
| Принят товар | Кабель USB |
| Цена, коп. | 1290 |
| Примечание | null |
| Цена черновика по DEFAULT | 0 |
| Неизвестное поле отклонено | да |
| Несуществующее поле: пустой результат | да |
Полная программа, файлы и ZIP для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c169Config();$db=c169Connect();$runtime=c169Runtime($db);
function schema169($db,int $kind,string $class,string $attribute=''): array {
$call=c169Capture(static function()use($db,$kind,$class,$attribute){
return $attribute===''?cubrid_schema($db,$kind,$class):cubrid_schema($db,$kind,$class,$attribute);
});
if (!is_array($call['value'])||$call['warning']) {c169Failure('Сведения о схеме не получены',$call);}
return $call['value'];
}
function schemaName169(string $value): string {
$parts=explode('.',strtolower($value));return trim((string)end($parts),'[]"');
}
function schemaExact169(array $rows,string $key,string $name): array {
return array_values(array_filter($rows,static function(array $row)use($key,$name):bool {
return isset($row[$key])&&schemaName169((string)$row[$key])===strtolower($name);
}));
}
$name=c169OwnName($db,$owned,'TABLE','import');$table=c169Qualified($name);
c169Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,sku VARCHAR(24) UNIQUE,title VARCHAR(120) NOT NULL,price_cents INT NOT NULL DEFAULT 0,note VARCHAR(160) NULL)');
c169Commit($db);
$classes=schemaExact169(schema169($db,CUBRID_SCH_CLASS,$name),'NAME',$name);
$attributes=schema169($db,CUBRID_SCH_ATTRIBUTE,$name);
$constraints=schema169($db,CUBRID_SCH_CONSTRAINT,$name);
$absent=schema169($db,CUBRID_SCH_ATTRIBUTE,$name,'missing169');
$fields=array_column($attributes,'ATTR_NAME');sort($fields);
$allowed=['id','sku','title','price_cents','note'];
$input=['id'=>1,'sku'=>'USB-169','title'=>'Кабель USB','price_cents'=>1290,'note'=>null];
$unknown=array_diff(array_keys($input),$fields);
if ($unknown||array_diff(array_keys($input),$allowed)||$input['title']===''||$input['price_cents']<0) {
throw new RuntimeException('Данные не соответствуют контракту импорта.');
}
c169Command($db,'INSERT INTO '.$table.' (id,sku,title,price_cents,note) VALUES (?,?,?,?,?)',array_values($input));
c169Command($db,'INSERT INTO '.$table.' (id,sku,title) VALUES (?,?,?)',[2,'DRAFT-169','Черновик']);
c169Commit($db);
$saved=c169Read($db,'SELECT id,title,price_cents,note FROM '.$table.' WHERE id IN (1,2) ORDER BY id LIMIT 2');
c169Commit($db);
$bad=['id'=>3,'sku'=>'X-169','title'=>'Ошибка','discount'=>10];
$rejected=array_values(array_diff(array_keys($bad),$fields));
$rows=[['Полей в таблице',count($fields)],['Принят товар',$saved[0]['title']],['Цена, коп.',(int)$saved[0]['price_cents']],
['Примечание',$saved[0]['note']],['Цена черновика по DEFAULT',(int)$saved[1]['price_cents']],
['Неизвестное поле отклонено',$rejected===['discount']],['Несуществующее поле: пустой результат',$absent===[]]];
$checks=['own_class'=>count($classes)===1,'five_columns'=>$fields===['id','note','price_cents','sku','title'],
'constraints_present'=>count($constraints)>=1,'two_rows'=>count($saved)===2,'title_saved'=>$saved[0]['title']==='Кабель USB',
'price_saved'=>$saved[0]['price_cents']==='1290','nullable_note'=>$saved[0]['note']===null,'default_applied'=>$saved[1]['price_cents']==='0',
'unknown_field_rejected'=>$rejected===['discount'],'empty_is_not_error'=>$absent===[]];
$observations=['class'=>$classes,'attributes'=>$attributes,'constraints'=>$constraints,'saved_rows'=>$saved,'rejected_fields'=>$rejected,'absent_attribute'=>$absent];
} finally {
if (function_exists('c169LockChildStopped') && !c169LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c169Cleanup($db,$owned);} finally {c169Close($db);}}
}
c169Report('cub169-import-contract','Паспорт импорта: какие поля существуют и что сохранится',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Полей в таблице — 5
Принят товар — Кабель USB
Цена, коп. — 1290
Примечание — null
Цена черновика по DEFAULT — 0
Неизвестное поле отклонено — да
Несуществующее поле: пустой результат — да
Распакуйте все файлы и выполните example.php по README.txt. lab.php создаёт и регистрирует только собственные объекты этого урока, затем удаляет их в обратном порядке. result.html показывает результат; result.json сохраняет строки и ответы схемы; metrics.json подтверждает условия и очистку. Полный файл содержит прямые вызовы cubrid_schema и SQL, поэтому можно связать действие с конкретным результатом.
Открыть фактический результат · Открыть данные JSON.
Ошибки и границы
Существующее поле ещё не разрешено редактировать любому пользователю. Схема не знает бизнес-условия «цена неотрицательна» и не заменяет проверку значений, авторизацию или подготовленные параметры. В CUBRID 11.3 CHECK в DDL разбирается, но игнорируется; пример поэтому проверяет цену явно. DEFAULT применяется при пропуске поля, а не автоматически вместо присланного NULL. Схема может измениться после её чтения: отказ INSERT всё равно обрабатывается.
Автор обсуждения хотел сопоставлять CSV с существующей SQL-таблицей по полям и типам перед импортом. Исходный вопрос. Здесь имена сравниваются с реальной схемой, затем отдельный allowlist и бизнес-проверки допускают запись. Перестановка колонок не меняет смысл именованных параметров; неизвестный discount получает явный отказ. Код сторонних библиотек из ответов не переносится.
Измените условие
Поставщик переставил title и sku местами и добавил discount. Нужно ли разрешить импорт, если число колонок совпало?
Разбор решения
Нет. Сопоставляйте по именам, явно разрешайте поля, проверяйте каждое значение. discount требует отдельного согласованного правила или явного отказа. Количество и порядок полей не доказывают смысловую совместимость.
Карта связи заказа и позиций: почему важен порядок колонок ключа
В двух филиалах есть заказ №700. Если связать позиции только по номеру, экран смешает данные. Составной ключ warehouse + order_no делает связь явной; метаданные позволяют восстановить её при сопровождении чужой базы, а ограниченный JOIN возвращает только выбранный заказ.
- Заказ: (warehouse=1, order_no=700) → покупатель Мастерская
- Позиции: (1,700,1) Кабель, 3 шт.; (1,700,2) Клемма, 6 шт.
- KEY_SEQ=1 связывает warehouse, KEY_SEQ=2 связывает order_no
- SQL JOIN по обеим колонкам + WHERE выбранного заказа → две позиции
Маленький вызов перед полной задачей
$keys = cubrid_schema($db, CUBRID_SCH_PRIMARY_KEY, $ordersName);
if ($keys === false) { throw new RuntimeException('Ключ неизвестен'); }
// KEY_SEQ начинается с 1. Сортируем описания ключа, не весь массив заказов.
usort($keys, static function ($a, $b) { return (int)$a['KEY_SEQ'] <=> (int)$b['KEY_SEQ']; });
echo implode(' + ', array_column($keys, 'ATTR_NAME')); // warehouse + order_noКлюч — одна или несколько колонок, совместно отличающих строку. FOREIGN KEY связывает значения с ключом другой таблицы. Строки ответа описывают пары колонок; одна связь из двух колонок даёт две строки, а не две независимые связи. Сортировка небольшого описания по KEY_SEQ нужна для порядка колонок; данные заказа фильтруются в SQL.
Что увидит читатель после запуска
| Наблюдение | Результат |
|---|---|
| Первичный ключ | warehouse + order_no |
| Связь | warehouse → warehouse; order_no → order_no |
| Заказ | Склад 1 / 700 |
| Покупатель | Мастерская |
| Позиция 1 | Кабель — 3 шт. |
| Позиция 2 | Клемма — 6 шт. |
| Чужой филиал не смешан | да |
Полная программа, файлы и ZIP для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c169Config();$db=c169Connect();$runtime=c169Runtime($db);
function schema169($db,int $kind,string $class,string $attribute=''): array {
$call=c169Capture(static function()use($db,$kind,$class,$attribute){
return $attribute===''?cubrid_schema($db,$kind,$class):cubrid_schema($db,$kind,$class,$attribute);
});
if (!is_array($call['value'])||$call['warning']) {c169Failure('Сведения о схеме не получены',$call);}
return $call['value'];
}
function schemaName169(string $value): string {
$parts=explode('.',strtolower($value));return trim((string)end($parts),'[]"');
}
function schemaExact169(array $rows,string $key,string $name): array {
return array_values(array_filter($rows,static function(array $row)use($key,$name):bool {
return isset($row[$key])&&schemaName169((string)$row[$key])===strtolower($name);
}));
}
$orderName=c169OwnName($db,$owned,'TABLE','orders');$orders=c169Qualified($orderName);
c169Ddl($db,'CREATE TABLE '.$orders.' (warehouse INT,order_no INT,customer VARCHAR(40),PRIMARY KEY(warehouse,order_no))');c169Commit($db);
$lineName=c169OwnName($db,$owned,'TABLE','lines');$lines=c169Qualified($lineName);
c169Ddl($db,'CREATE TABLE '.$lines.' (warehouse INT,order_no INT,line_no INT,title VARCHAR(40),qty INT,PRIMARY KEY(warehouse,order_no,line_no),FOREIGN KEY(warehouse,order_no) REFERENCES '.$orders.' (warehouse,order_no) ON DELETE RESTRICT)');c169Commit($db);
c169Command($db,'INSERT INTO '.$orders.' (warehouse,order_no,customer) VALUES (?,?,?)',[1,700,'Мастерская']);
c169Command($db,'INSERT INTO '.$orders.' (warehouse,order_no,customer) VALUES (?,?,?)',[2,700,'Другой филиал']);
c169Command($db,'INSERT INTO '.$lines.' (warehouse,order_no,line_no,title,qty) VALUES (?,?,?,?,?)',[1,700,1,'Кабель',3]);
c169Command($db,'INSERT INTO '.$lines.' (warehouse,order_no,line_no,title,qty) VALUES (?,?,?,?,?)',[1,700,2,'Клемма',6]);c169Commit($db);
$pk=schema169($db,CUBRID_SCH_PRIMARY_KEY,$orderName);
usort($pk,static function(array $a,array $b):int{return (int)$a['KEY_SEQ']<=>(int)$b['KEY_SEQ'];});
$imported=schema169($db,CUBRID_SCH_IMPORTED_KEYS,$lineName);
$exported=schema169($db,CUBRID_SCH_EXPORTED_KEYS,$orderName);
$cross=schema169($db,CUBRID_SCH_CROSS_REFERENCE,$orderName,$lineName);
usort($cross,static function(array $a,array $b):int{return (int)$a['KEY_SEQ']<=>(int)$b['KEY_SEQ'];});
$items=c169Read($db,'SELECT o.customer,l.title,l.qty FROM '.$orders.' o JOIN '.$lines.' l ON l.warehouse=o.warehouse AND l.order_no=o.order_no WHERE o.warehouse=? AND o.order_no=? ORDER BY l.line_no LIMIT 10',[1,700]);c169Commit($db);
$pairs=[];foreach($cross as $key){$pairs[]=$key['PKCOLUMN_NAME'].' → '.$key['FKCOLUMN_NAME'];}
$rows=[['Первичный ключ',implode(' + ',array_column($pk,'ATTR_NAME'))],['Связь',implode('; ',$pairs)],['Заказ','Склад 1 / 700'],
['Покупатель',$items[0]['customer']],['Позиция 1',$items[0]['title'].' — '.$items[0]['qty'].' шт.'],['Позиция 2',$items[1]['title'].' — '.$items[1]['qty'].' шт.'],['Чужой филиал не смешан',array_unique(array_column($items,'customer'))===['Мастерская']]];
$checks=['pk_order'=>array_column($pk,'ATTR_NAME')===['warehouse','order_no'],'two_imported'=>count($imported)===2,
'two_exported'=>count($exported)===2,'two_cross'=>count($cross)===2,'paired_keys'=>$pairs===['warehouse → warehouse','order_no → order_no'],
'two_items'=>count($items)===2,'correct_titles'=>array_column($items,'title')===['Кабель','Клемма'],
'correct_quantities'=>array_column($items,'qty')===['3','6'],'customer_bound'=>array_unique(array_column($items,'customer'))===['Мастерская']];
$observations=['primary_key'=>$pk,'imported'=>$imported,'exported'=>$exported,'cross_reference'=>$cross,'order_items'=>$items,'sql_limit'=>10];
} finally {
if (function_exists('c169LockChildStopped') && !c169LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c169Cleanup($db,$owned);} finally {c169Close($db);}}
}
c169Report('cub169-key-map','Карта связи заказа и позиций: почему важен порядок колонок ключа',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первичный ключ — warehouse + order_no
Связь — warehouse → warehouse; order_no → order_no
Заказ — Склад 1 / 700
Покупатель — Мастерская
Позиция 1 — Кабель — 3 шт.
Позиция 2 — Клемма — 6 шт.
Чужой филиал не смешан — да
Распакуйте все файлы и выполните example.php по README.txt. lab.php создаёт и регистрирует только собственные объекты этого урока, затем удаляет их в обратном порядке. result.html показывает результат; result.json сохраняет строки и ответы схемы; metrics.json подтверждает условия и очистку. Полный файл содержит прямые вызовы cubrid_schema и SQL, поэтому можно связать действие с конкретным результатом.
Открыть фактический результат · Открыть данные JSON.
Ошибки и границы
Метаданные показывают объявленные ограничения, а не все смысловые отношения приложения. При отсутствии FK пустой массив не доказывает, что таблицы никак не связаны. Нельзя строить произвольный SQL из непроверенного имени таблицы. Для CROSS_REFERENCE четвёртый аргумент — имя дочерней таблицы, несмотря общее имя attr_name в PHP-сигнатуре. Действия DELETE/UPDATE читают отдельно; наличие FK не означает CASCADE.
Вопрос о большой существующей базе: как найти объявленные внешние ключи, чтобы разобраться в её связях. Исходный вопрос. Наш пример использует три направления CUBRID schema и составную пару warehouse/order_no, затем выполняет понятный ограниченный JOIN. Каталоги MySQL и SQL из чужой СУБД не выдаются за API CUBRID.
Измените условие
Зачем сохранять warehouse в условиях JOIN, если сегодня номер заказа уникален?
Разбор решения
Договорённость сегодня не заменяет объявленный составной ключ. Второй филиал уже может иметь такой номер. Используйте все пары из связи, ограничивайте нужный заказ до передачи результата в PHP.
Витрина как представление: что скрыто за именем готового каталога
Несколько экранов используют одинаковое условие «есть остаток». Представление хранит SQL-описание такой витрины. Программа покажет Кабель, затем после пополнения — ещё и Клипсу; QUERY_SPEC помогает понять, почему строки появились или исчезли.
- TABLE: Кабель — 3 шт., Клипса — 0 → VIEW с WHERE qty>0 → только Кабель
- UPDATE исходной Клипсы: 0 → 2 → COMMIT
- Новый SELECT VIEW → Кабель — 3 шт. и Клипса — 2 шт.
- QUERY_SPEC показывает сохранённое правило; не копирует товарные данные в PHP
Маленький вызов перед полной задачей
$definition = cubrid_schema($db, CUBRID_SCH_QUERY_SPEC, $viewName);
if ($definition === false) { throw new RuntimeException('Нет определения'); }
echo $definition[0]['QUERY_SPEC']; // SQL, из которого составлено представление.Представление (view, исторически vclass) похоже на именованный рецепт SELECT. QUERY_SPEC возвращает текст рецепта, а SELECT FROM view — данные. Поэтому это разные вызовы с разными результатами. Предикат витрины выбирает строки; внешний SELECT всё равно задаёт необходимые колонки, порядок и LIMIT.
Что увидит читатель после запуска
| Наблюдение | Результат |
|---|---|
| До пополнения | Кабель — 3 шт. |
| Товаров после пополнения | 2 |
| Добавленная позиция | Клипса — 2 шт. |
| Определение представления получено | да |
| Служебная заметка не выбрана | да |
Полная программа, файлы и ZIP для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c169Config();$db=c169Connect();$runtime=c169Runtime($db);
function schema169($db,int $kind,string $class,string $attribute=''): array {
$call=c169Capture(static function()use($db,$kind,$class,$attribute){
return $attribute===''?cubrid_schema($db,$kind,$class):cubrid_schema($db,$kind,$class,$attribute);
});
if (!is_array($call['value'])||$call['warning']) {c169Failure('Сведения о схеме не получены',$call);}
return $call['value'];
}
function schemaName169(string $value): string {
$parts=explode('.',strtolower($value));return trim((string)end($parts),'[]"');
}
function schemaExact169(array $rows,string $key,string $name): array {
return array_values(array_filter($rows,static function(array $row)use($key,$name):bool {
return isset($row[$key])&&schemaName169((string)$row[$key])===strtolower($name);
}));
}
$name=c169CreateTable($db,$owned,'view_items',[
['id'=>1,'sku'=>'CAB-169','title'=>'Кабель','category'=>'parts','price_cents'=>1200,'qty'=>3,'note'=>'Закупочная пометка'],
['id'=>2,'sku'=>'CLIP-169','title'=>'Клипса','category'=>'parts','price_cents'=>250,'qty'=>0,'note'=>null]]);
$viewName=c169OwnName($db,$owned,'VIEW','available');$view=c169Qualified($viewName);
c169Ddl($db,'CREATE VIEW '.$view.' AS SELECT sku,title,price_cents,qty FROM '.c169Qualified($name).' WHERE qty>0');c169Commit($db);
$views=schemaExact169(schema169($db,CUBRID_SCH_VCLASS,$viewName),'NAME',$viewName);
$spec=schema169($db,CUBRID_SCH_QUERY_SPEC,$viewName);
$before=c169Read($db,'SELECT sku,title,qty FROM '.$view.' ORDER BY sku LIMIT 5');c169Commit($db);
c169Command($db,'UPDATE '.c169Qualified($name).' SET qty=2 WHERE id=2');c169Commit($db);
$after=c169Read($db,'SELECT sku,title,qty FROM '.$view.' ORDER BY sku LIMIT 5');c169Commit($db);
$rows=[['До пополнения',$before[0]['title'].' — '.$before[0]['qty'].' шт.'],['Товаров после пополнения',count($after)],
['Добавленная позиция',$after[1]['title'].' — '.$after[1]['qty'].' шт.'],['Определение представления получено',count($spec)>0],['Служебная заметка не выбрана',!array_key_exists('note',$after[0])]];
$checks=['one_view'=>count($views)===1,'view_query_present'=>count($spec)>0&&is_string($spec[0]['QUERY_SPEC']),
'before_bounded'=>count($before)===1&&$before[0]['qty']==='3','after_two'=>count($after)===2,
'new_stock'=>$after[1]['title']==='Клипса'&&$after[1]['qty']==='2','private_note_not_projected'=>!array_key_exists('note',$after[0]),
'explicit_projection'=>array_keys($after[0])===['sku','title','qty']];
$observations=['view_metadata'=>$views,'query_specification'=>$spec,'before'=>$before,'after'=>$after];
} finally {
if (function_exists('c169LockChildStopped') && !c169LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c169Cleanup($db,$owned);} finally {c169Close($db);}}
}
c169Report('cub169-view-catalog','Витрина как представление: что скрыто за именем готового каталога',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До пополнения — Кабель — 3 шт.
Товаров после пополнения — 2
Добавленная позиция — Клипса — 2 шт.
Определение представления получено — да
Служебная заметка не выбрана — да
Распакуйте все файлы и выполните example.php по README.txt. lab.php создаёт и регистрирует только собственные объекты этого урока, затем удаляет их в обратном порядке. result.html показывает результат; result.json сохраняет строки и ответы схемы; metrics.json подтверждает условия и очистку. Полный файл содержит прямые вызовы cubrid_schema и SQL, поэтому можно связать действие с конкретным результатом.
Открыть фактический результат · Открыть данные JSON.
Ошибки и границы
Сервер может нормализовать текст определения: сравнивать его побайтно с исходной строкой обычно неправильно. Представление не является автоматически кэшем или отдельной копией данных. Отсутствие note в данном SELECT само по себе не лишает пользователя права читать исходную таблицу — права разбираются отдельно. Возможность UPDATE через view зависит от его определения и свойств таблиц.
Измените условие
Нужно показать только два дешёвых товара из витрины. Достаточно взять все строки view и отсортировать их в PHP?
Разбор решения
Нет. Используйте SELECT нужных полей FROM view ORDER BY price_cents,sku LIMIT 2. Сохранённое условие представления не заменяет требования конкретного экрана.
Почему появился журнал: триггер реального изменения остатка
После UPDATE в журнале появилась строка, хотя PHP не отправлял туда INSERT. Это может быть действие триггера. Создадим понятный журнал остатка: изменение 3→7 сохранится один раз, повторное присваивание 7 не создаст лишней записи. Метаданные объяснят источник поведения.
- PHP UPDATE qty=7 → у Кабеля было 3 шт., стало 7
- AFTER UPDATE: old.qty=3, obj.qty=7 → условие истинно → audit: 3 → 7
- COMMIT сохраняет и товар, и запись журнала
- Повторный UPDATE: 7 → 7 → условие ложно → новых строк audit нет
Маленький вызов перед полной задачей
$triggers = cubrid_schema($db, CUBRID_SCH_TRIGGER, 'dba.' . $tableName);
if ($triggers === false) { throw new RuntimeException('Не прочитаны триггеры'); }
foreach ($triggers as $trigger) { echo $trigger['NAME'], ': ', $trigger['ACTION'], "\n"; }Триггер — действие сервера при определённом событии. SCH_TRIGGER читает его описание, но не запускает событие. В AFTER UPDATE прежняя строка называется old, текущая — obj. Это именно синтаксис CUBRID; привычные NEW/OLD из другой СУБД нельзя переносить механически.
Что увидит читатель после запуска
| Наблюдение | Результат |
|---|---|
| Товар | Кабель |
| Текущий остаток | 7 |
| Журнал | 3 → 7 |
| Записей после двух UPDATE | 1 |
| Триггер найден | да |
Полная программа, файлы и ZIP для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c169Config();$db=c169Connect();$runtime=c169Runtime($db);
function schema169($db,int $kind,string $class,string $attribute=''): array {
$call=c169Capture(static function()use($db,$kind,$class,$attribute){
return $attribute===''?cubrid_schema($db,$kind,$class):cubrid_schema($db,$kind,$class,$attribute);
});
if (!is_array($call['value'])||$call['warning']) {c169Failure('Сведения о схеме не получены',$call);}
return $call['value'];
}
function schemaName169(string $value): string {
$parts=explode('.',strtolower($value));return trim((string)end($parts),'[]"');
}
function schemaExact169(array $rows,string $key,string $name): array {
return array_values(array_filter($rows,static function(array $row)use($key,$name):bool {
return isset($row[$key])&&schemaName169((string)$row[$key])===strtolower($name);
}));
}
$name=c169CreateTable($db,$owned,'stock',[
['id'=>1,'sku'=>'CAB-169','title'=>'Кабель','category'=>'parts','price_cents'=>1200,'qty'=>3,'note'=>null]]);
$auditName=c169OwnName($db,$owned,'TABLE','audit');$audit=c169Qualified($auditName);
c169Ddl($db,'CREATE TABLE '.$audit.' (item_id INT,old_qty INT,new_qty INT)');c169Commit($db);
$triggerName=c169OwnName($db,$owned,'TRIGGER','qty_log');$trigger=c169Qualified($triggerName);
c169Ddl($db,'CREATE TRIGGER '.$trigger.' AFTER UPDATE ON '.c169Qualified($name).'(qty) IF old.qty<>obj.qty EXECUTE INSERT INTO '.$audit.' (item_id,old_qty,new_qty) VALUES (obj.id,old.qty,obj.qty)');c169Commit($db);
$metadata=schemaExact169(schema169($db,CUBRID_SCH_TRIGGER,'dba.'.$name),'NAME',$triggerName);
c169Command($db,'UPDATE '.c169Qualified($name).' SET qty=7 WHERE id=1');c169Commit($db);
c169Command($db,'UPDATE '.c169Qualified($name).' SET qty=7 WHERE id=1');c169Commit($db);
$logs=c169Read($db,'SELECT item_id,old_qty,new_qty FROM '.$audit.' ORDER BY item_id LIMIT 10');
$item=c169Read($db,'SELECT title,qty FROM '.c169Qualified($name).' WHERE id=1');c169Commit($db);
$rows=[['Товар',$item[0]['title']],['Текущий остаток',(int)$item[0]['qty']],['Журнал',$logs[0]['old_qty'].' → '.$logs[0]['new_qty']],
['Записей после двух UPDATE',count($logs)],['Триггер найден',count($metadata)===1]];
$checks=['one_trigger'=>count($metadata)===1,'one_log'=>count($logs)===1,'same_item'=>$logs[0]['item_id']==='1',
'old_value'=>$logs[0]['old_qty']==='3','new_value'=>$logs[0]['new_qty']==='7','stock_saved'=>$item[0]['qty']==='7',
'same_value_not_logged'=>count($logs)===1,'name_saved'=>$item[0]['title']==='Кабель'];
$observations=['trigger_metadata'=>$metadata,'audit_rows'=>$logs,'item'=>$item,'explicit_UPDATE_calls'=>2];
} finally {
if (function_exists('c169LockChildStopped') && !c169LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c169Cleanup($db,$owned);} finally {c169Close($db);}}
}
c169Report('cub169-trigger-audit','Почему появился журнал: триггер реального изменения остатка',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар — Кабель
Текущий остаток — 7
Журнал — 3 → 7
Записей после двух UPDATE — 1
Триггер найден — да
Распакуйте все файлы и выполните example.php по README.txt. lab.php создаёт и регистрирует только собственные объекты этого урока, затем удаляет их в обратном порядке. result.html показывает результат; result.json сохраняет строки и ответы схемы; metrics.json подтверждает условия и очистку. Полный файл содержит прямые вызовы cubrid_schema и SQL, поэтому можно связать действие с конкретным результатом.
Открыть фактический результат · Открыть данные JSON.
Ошибки и границы
В примере qty NOT NULL, поэтому простое сравнение <> достаточно. Для nullable-полей нужно отдельно различать NULL и значение. Журнал и основная запись участвуют в транзакции; не следует обещать, что audit сохранится после rollback. Общий аудит всех полей, пользователя, времени и версий — отдельное требование. Пустой список триггеров не исключает внешние процессы или действия приложения.
Автор хотел писать аудит только при действительном изменении значений, а не при каждом UPDATE. Исходный вопрос. В собственной схеме одна колонка qty NOT NULL меняется 3→7 и повторно получает 7: сохраняется только одна запись журнала. Синтаксис old/obj сверяется с CUBRID; специфические конструкции PostgreSQL из обсуждения не копируются.
Измените условие
Почему два UPDATE дали одну запись журнала? Это потеря события?
Разбор решения
Журнал фиксирует изменение значения, а не каждую попытку команды. Условие old.qty<>obj.qty исключило 7 → 7. Если нужен журнал попыток, меняются требование, структура и условие триггера.
Наследуемая схема: откуда у складской позиции появились поля
При сопровождении старой CUBRID-схемы поле может быть унаследовано. Нужно понять, почему у позиции есть title, qty и zone_label, хотя в последнем CREATE объявлена только zone_label. Программа сохраняет Кабель на полке A-2 и показывает происхождение его полей.
- Базовый класс item: id, title + общий policy
- stock UNDER item добавляет qty
- shelf UNDER stock добавляет zone_label; экземпляр Кабель/A-2/7
- SOURCE_CLASS объясняет происхождение поля; сведения о методах честно сообщают пустые списки
Маленький вызов перед полной задачей
$parents = cubrid_schema($db, CUBRID_SCH_DIRECT_SUPER_CLASS, $shelfName);
if ($parents === false) { throw new RuntimeException('Родитель неизвестен'); }
foreach ($parents as $parent) { echo $parent['SUPER_CLASS_NAME'], "\n"; }Термин class здесь принадлежит объектно-реляционной базе: это объявленная серверная структура, а instance — её строка/объект. Он не объявляет PHP-класс через class и не вызывает PHP-конструктор. UNDER добавляет наследуемую структуру. Метаданные CLASS_ATTRIBUTE относятся к общему состоянию класса; обычные ATTRIBUTE — к полям экземпляров.
Что увидит читатель после запуска
| Наблюдение | Результат |
|---|---|
| Складская позиция | Кабель / A-2 / 7 шт. |
| Поля экземпляра | id, qty, title, zone_label |
| Общий атрибут класса | policy |
| Непосредственный родитель найден | да |
| Внешних методов объявлено | 0 |
| Файлов методов | 0 |
Полная программа, файлы и ZIP для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c169Config();$db=c169Connect();$runtime=c169Runtime($db);
function schema169($db,int $kind,string $class,string $attribute=''): array {
$call=c169Capture(static function()use($db,$kind,$class,$attribute){
return $attribute===''?cubrid_schema($db,$kind,$class):cubrid_schema($db,$kind,$class,$attribute);
});
if (!is_array($call['value'])||$call['warning']) {c169Failure('Сведения о схеме не получены',$call);}
return $call['value'];
}
function schemaName169(string $value): string {
$parts=explode('.',strtolower($value));return trim((string)end($parts),'[]"');
}
function schemaExact169(array $rows,string $key,string $name): array {
return array_values(array_filter($rows,static function(array $row)use($key,$name):bool {
return isset($row[$key])&&schemaName169((string)$row[$key])===strtolower($name);
}));
}
$baseName=c169OwnName($db,$owned,'TABLE','item_base');$base=c169Qualified($baseName);
c169Ddl($db,'CREATE TABLE '.$base.' CLASS ATTRIBUTE (policy VARCHAR(20) DEFAULT \'review\') (id INT,title VARCHAR(40)) DONT_REUSE_OID');c169Commit($db);
$midName=c169OwnName($db,$owned,'TABLE','stock_base');$mid=c169Qualified($midName);
c169Ddl($db,'CREATE TABLE '.$mid.' UNDER '.$base.' (qty INT) DONT_REUSE_OID');c169Commit($db);
$leafName=c169OwnName($db,$owned,'TABLE','shelf');$leaf=c169Qualified($leafName);
c169Ddl($db,'CREATE TABLE '.$leaf.' UNDER '.$mid.' (zone_label VARCHAR(20)) DONT_REUSE_OID');c169Commit($db);
c169Command($db,'INSERT INTO '.$leaf.' (id,title,qty,zone_label) VALUES (?,?,?,?)',[1,'Кабель',7,'A-2']);c169Commit($db);
$attrs=schema169($db,CUBRID_SCH_ATTRIBUTE,$leafName);
$classAttrs=schema169($db,CUBRID_SCH_CLASS_ATTRIBUTE,$baseName);
$super=schema169($db,CUBRID_SCH_SUPERCLASS,$leafName);
$direct=schema169($db,CUBRID_SCH_DIRECT_SUPER_CLASS,$leafName);
$sub=schema169($db,CUBRID_SCH_SUBCLASS,$baseName);
$methods=schema169($db,CUBRID_SCH_METHOD,$baseName);
$classMethods=schema169($db,CUBRID_SCH_CLASS_METHOD,$baseName);
$methodFiles=schema169($db,CUBRID_SCH_METHOD_FILE,$baseName);
$item=c169Read($db,'SELECT title,qty,zone_label FROM '.$leaf.' WHERE id=1');c169Commit($db);
$names=array_column($attrs,'ATTR_NAME');sort($names);
$origin=[];foreach($attrs as $attr){$origin[$attr['ATTR_NAME']]=schemaName169((string)$attr['SOURCE_CLASS']);}
$rows=[['Складская позиция',$item[0]['title'].' / '.$item[0]['zone_label'].' / '.$item[0]['qty'].' шт.'],
['Поля экземпляра',implode(', ',$names)],['Общий атрибут класса',implode(', ',array_column($classAttrs,'ATTR_NAME'))],
['Непосредственный родитель найден',count(schemaExact169($direct,'SUPER_CLASS_NAME',$midName))===1],
['Внешних методов объявлено',count($methods)+count($classMethods)],['Файлов методов',count($methodFiles)]];
$checks=['visible_item'=>$item[0]===['title'=>'Кабель','qty'=>'7','zone_label'=>'A-2'],'four_fields'=>$names===['id','qty','title','zone_label'],
'class_policy'=>in_array('policy',array_column($classAttrs,'ATTR_NAME'),true),'direct_parent'=>count(schemaExact169($direct,'SUPER_CLASS_NAME',$midName))===1,
'some_super'=>count($super)>=1,'subclass_visible'=>count($sub)>=1,'origin_title'=>($origin['title']??null)===$baseName,
'origin_qty'=>($origin['qty']??null)===$midName,'origin_zone'=>($origin['zone_label']??null)===$leafName,
'no_instance_methods'=>$methods===[],'no_class_methods'=>$classMethods===[],'no_method_files'=>$methodFiles===[]];
$observations=['attributes'=>$attrs,'class_attributes'=>$classAttrs,'superclasses'=>$super,'direct_superclasses'=>$direct,
'subclasses'=>$sub,'methods'=>$methods,'class_methods'=>$classMethods,'method_files'=>$methodFiles,'item'=>$item];
} finally {
if (function_exists('c169LockChildStopped') && !c169LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c169Cleanup($db,$owned);} finally {c169Close($db);}}
}
c169Report('cub169-class-lineage','Наследуемая схема: откуда у складской позиции появились поля',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Складская позиция — Кабель / A-2 / 7 шт.
Поля экземпляра — id, qty, title, zone_label
Общий атрибут класса — policy
Непосредственный родитель найден — да
Внешних методов объявлено — 0
Файлов методов — 0
Распакуйте все файлы и выполните example.php по README.txt. lab.php создаёт и регистрирует только собственные объекты этого урока, затем удаляет их в обратном порядке. result.html показывает результат; result.json сохраняет строки и ответы схемы; metrics.json подтверждает условия и очистку. Полный файл содержит прямые вызовы cubrid_schema и SQL, поэтому можно связать действие с конкретным результатом.
Открыть фактический результат · Открыть данные JSON.
Ошибки и границы
Наследование таблиц не заменяет привычные FK-связи заказа и позиций. Здесь оно нужно, чтобы понять уже существующую объектную модель CUBRID. Методы базы могут быть связаны с внешними функциями; пустые METHOD/CLASS_METHOD/METHOD_FILE — нормальный результат нашей схемы без таких объявлений. Это не доказательство, что API не поддерживается. Пример ничего не компилирует и не загружает из неизвестных файлов.
Измените условие
SOURCE_CLASS у title указывает на базовый класс. Следует ли продублировать title в leaf?
Разбор решения
Нет. Сначала выясните наследование и правила разрешения имён. Дублирование меняет модель и может создать конфликт. Отличайте место объявления поля, класс экземпляра и PHP-объект, которым приложение оформляет результат.
Доступ только на чтение и второе имя каталога: метаданные против реального действия
Отчёт должен читать каталог, но не менять остатки. Создадим временного пользователя с SELECT, дадим таблице второе имя и проверим действие: Кабель читается, UPDATE отклонён, остаток остаётся 3. Сведения о правах помогают объяснить отказ, а не заменяют саму проверку операции.
- DBA создаёт собственную таблицу и временного reader → GRANT SELECT
- Synonym catalog_alias указывает на ту же таблицу; он не хранит вторую копию
- Новое соединение reader → SELECT возвращает Кабель, 3 шт.
- UPDATE до 9 → отказ → rollback → DBA перечитывает прежний остаток 3
Маленький вызов перед полной задачей
$rights = cubrid_schema($db, CUBRID_SCH_CLASS_PRIVILEGE, $tableName);
if ($rights === false) { throw new RuntimeException('Права не прочитаны'); }
foreach ($rights as $right) {
echo $right['PRIVILEGE'], ' / grantable=', $right['GRANTABLE'], "\n";
}Пользователь базы — учётная запись подключения, не строка веб-таблицы users. GRANT выдаёт разрешение, synonym создаёт второе имя объекта. SCH_ATTR_WITH_SYNONYM читает атрибуты через это имя; обычная SQL-выборка получает данные. Ключ GRANTABLE относится к возможности передать разрешение другому пользователю, а не к разрешению UPDATE.
Что увидит читатель после запуска
| Наблюдение | Результат |
|---|---|
| Доступный каталог | Кабель — 3 шт. |
| Метаданные прав получены | да |
| UPDATE отклонён | да |
| Остаток не изменён | 3 |
| Атрибуты через synonym найдены | да |
| Второе соединение закрыто | да |
Полная программа, файлы и ZIP для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c169Config();$db=c169Connect();$runtime=c169Runtime($db);
function schema169($db,int $kind,string $class,string $attribute=''): array {
$call=c169Capture(static function()use($db,$kind,$class,$attribute){
return $attribute===''?cubrid_schema($db,$kind,$class):cubrid_schema($db,$kind,$class,$attribute);
});
if (!is_array($call['value'])||$call['warning']) {c169Failure('Сведения о схеме не получены',$call);}
return $call['value'];
}
function schemaName169(string $value): string {
$parts=explode('.',strtolower($value));return trim((string)end($parts),'[]"');
}
function schemaExact169(array $rows,string $key,string $name): array {
return array_values(array_filter($rows,static function(array $row)use($key,$name):bool {
return isset($row[$key])&&schemaName169((string)$row[$key])===strtolower($name);
}));
}
$name=c169CreateTable($db,$owned,'permissions',[
['id'=>1,'sku'=>'CAB-169','title'=>'Кабель','category'=>'parts','price_cents'=>1200,'qty'=>3,'note'=>'Внутренняя пометка']]);
$userName=c169OwnName($db,$owned,'USER','reader');$password=bin2hex(random_bytes(12));
// Закрытый файл нужен только проверке отсутствия секрета в публичных результатах.
file_put_contents(__DIR__.'/secret_values.json',json_encode(['schema'=>'aen-cubrid169-private-secrets-v1','values'=>[$password]],JSON_THROW_ON_ERROR));
chmod(__DIR__.'/secret_values.json',0600);
c169Ddl($db,'CREATE USER '.c169Qualified($userName).' PASSWORD \''.$password.'\'');c169Commit($db);
c169Ddl($db,'GRANT SELECT ON '.c169Qualified($name).' TO '.c169Qualified($userName));c169Commit($db);
$aliasName=c169OwnName($db,$owned,'SYNONYM','catalog_alias');$alias=c169Qualified($aliasName);
c169Ddl($db,'CREATE SYNONYM '.$alias.' FOR dba.'.c169Qualified($name));c169Commit($db);
$aliasAttrs=schema169($db,CUBRID_SCH_ATTR_WITH_SYNONYM,$aliasName);
$reader=null;
try {
$reader=c169ConnectUser($userName,$password,$owned);
$classPrivileges=schema169($reader,CUBRID_SCH_CLASS_PRIVILEGE,'dba.'.$name);
$attributePrivileges=schema169($reader,CUBRID_SCH_ATTR_PRIVILEGE,'dba.'.$name,'title');
$item=c169Read($reader,'SELECT title,qty FROM dba.'.$alias.' WHERE id=1');c169Commit($reader);
$attempt=c169Capture(static function()use($reader,$name){return cubrid_execute($reader,'UPDATE dba.'.c169Qualified($name).' SET qty=9 WHERE id=1');});
if (is_resource($attempt['value'])) { $unexpected=$attempt['value'];c169Release($unexpected);throw new RuntimeException('Учебному читателю неожиданно разрешена запись.'); }
c169Rollback($reader);
} finally {c169Close($reader);unset($password);}
$saved=c169Read($db,'SELECT title,qty FROM '.c169Qualified($name).' WHERE id=1');c169Commit($db);
$rows=[['Доступный каталог',$item[0]['title'].' — '.$item[0]['qty'].' шт.'],['Метаданные прав получены',count($classPrivileges)>0],
['UPDATE отклонён',$attempt['value']===false&&$attempt['warning']],['Остаток не изменён',(int)$saved[0]['qty']],
['Атрибуты через synonym найдены',in_array('title',array_column($aliasAttrs,'ATTR_NAME'),true)],['Второе соединение закрыто',$reader===null]];
$checks=['reader_got_item'=>$item[0]===['title'=>'Кабель','qty'=>'3'],'privileges_nonempty'=>count($classPrivileges)>0,
'attribute_response_array'=>is_array($attributePrivileges),'write_denied'=>$attempt['value']===false&&$attempt['warning']&&$attempt['code']===-494&&$attempt['facility']===1,
'denial_has_message'=>$attempt['message_present'],'stock_unchanged'=>$saved[0]['qty']==='3','alias_has_title'=>in_array('title',array_column($aliasAttrs,'ATTR_NAME'),true),
'reader_closed'=>$reader===null,'secret_not_in_output'=>!isset($password)];
$observations=['class_privileges'=>$classPrivileges,'attribute_privileges'=>$attributePrivileges,'synonym_attributes'=>$aliasAttrs,
'read_row'=>$item,'refused_update'=>$attempt,'saved_row'=>$saved,'password_published'=>false];
} finally {
if (function_exists('c169LockChildStopped') && !c169LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c169Cleanup($db,$owned);} finally {c169Close($db);}}
}
c169Report('cub169-permission-synonym','Доступ только на чтение и второе имя каталога: метаданные против реального действия',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Доступный каталог — Кабель — 3 шт.
Метаданные прав получены — да
UPDATE отклонён — да
Остаток не изменён — 3
Атрибуты через synonym найдены — да
Второе соединение закрыто — да
Распакуйте все файлы и выполните example.php по README.txt. lab.php создаёт и регистрирует только собственные объекты этого урока, затем удаляет их в обратном порядке. result.html показывает результат; result.json сохраняет строки и ответы схемы; metrics.json подтверждает условия и очистку. Полный файл содержит прямые вызовы cubrid_schema и SQL, поэтому можно связать действие с конкретным результатом. Локальный secret_values.json содержит временный случайный пароль для проверки отсутствия утечки; он имеет права 0600, не является публичным результатом и не должен выкладываться на сайт. После разбора своего запуска его можно удалить.
Открыть фактический результат · Открыть данные JSON.
Ошибки и границы
В CUBRID 11.3 минимальная единица GRANT — таблица. Наличие SCH_ATTR_PRIVILEGE не означает, что можно переносить сюда column-level GRANT другой СУБД. Метаданные не заменяют серверную авторизацию и могут измениться после чтения. Synonym не обходит права. В программе создаётся только свой временный USER внутри отдельной базы, пароль не выводится; все подключения закрываются до DROP USER.
Измените условие
Если SELECT по synonym работает, можно ли считать, что UPDATE по исходному имени тоже разрешён?
Разбор решения
Нет. Имя и действие независимы. SELECT и UPDATE имеют разные разрешения, а synonym лишь разрешается в целевой объект. Проверяйте конкретную операцию и сохраняйте её диагностику до следующего вызова драйвера.
Классы и VIEW: NAME, TYPE, QUERY_SPEC
Класс в терминологии CUBRID описывает серверную структуру. Обычная таблица содержит экземпляры/строки; виртуальный класс, или VIEW, хранит запрос. PHP-класс и объект stdClass не создаются от одного появления слова CLASS в метаданных.
| Тип | Поле ответа | Смысл |
|---|---|---|
| CLASS / VCLASS | NAME | Имя, в современном профиле с владельцем dba. TYPE=0 обозначает системный класс, 1 — представление, 2 — обычный класс; в нашем созданном каталоге это 1 или 2. |
| CLASS / VCLASS | REMARKS | Дополнительный комментарий проверенного сервера, может быть null. Его нет в старой двухколоночной таблице PHP Manual. |
| QUERY_SPEC | QUERY_SPEC | SQL определения VIEW. Это строка запроса, не результат выполнения. Сервер может нормализовать кавычки, владельцев и пробелы. |
CLASS может перечислить и таблицы, и VIEW; VCLASS ограничивает выбор представлениями. В живом каталоге доступных товаров сначала показывается один Кабель, а после изменения qty у Клипсы та же VIEW возвращает две строки. Не сравнивайте определение с исходной строкой CREATE посимвольно: проверяйте смысл и наблюдаемый результат.
$views = cubrid_schema($db, CUBRID_SCH_VCLASS, $viewName);
$definition = cubrid_schema($db, CUBRID_SCH_QUERY_SPEC, $viewName);
// У каждого вызова отдельно проверяется === false до чтения элементов.Атрибуты: тип, NULL, DEFAULT и происхождение
ATTRIBUTE описывает поля каждой строки. CLASS_ATTRIBUTE описывает общие атрибуты самого класса — в уроке наследования это policy. ATTR_WITH_SYNONYM разрешает второе имя объекта и возвращает описание целевого класса. Во всех трёх случаях это свойства схемы, а не текущие значения товара.
| Поля | Что означают |
|---|---|
| ATTR_NAME | Имя атрибута; используйте его для именованного сопоставления импорта. |
| DOMAIN | Числовой код домена CCI: например, 8 для INT, 2 для VARCHAR в этом профиле. Это не PHP gettype. Полный тип дополнительно зависит от SCALE/PRECISION и структуры домена. |
| SCALE / PRECISION | Масштаб и точность/объявленная вместимость домена; не фактическая длина текущего title в байтах. Для фактических байтов есть cubrid_fetch_lengths. |
| INDEXED / UNIQUE / IS_KEY | Флаги наличия индексной/уникальной/ключевой характеристики. По одному флагу нельзя восстановить составной ключ и порядок его колонок: используйте PRIMARY_KEY и KEY_SEQ. |
| NON_NULL | Фактический ключ массива для ограничения NULL. В таблице старого PHP Manual назван NOT NULL; в её же примере и нашем профиле — NON_NULL. Значение «1» означает запрет SQL NULL. |
| SHARED | Признак общего атрибута; не путать с PHP shared memory и с GRANT пользователям. |
| DEFAULT | Представление объявленного значения/выражения по умолчанию. У qty DEFAULT 0 получена строка «0», у policy — строка с SQL-кавычками «'review'». Отсутствующий default в этом профиле представлен текстом «NULL», тогда как старый пример PHP показывает null. |
| ATTR_ORDER | Позиция объявления от 1, не индекс внешнего массива от 0. Сортировка по ней нужна, если цель — показать порядок схемы. |
| CLASS_NAME / SOURCE_CLASS | Описываемый класс и класс, в котором поле исходно объявлено. Они расходятся при наследовании. |
| REMARKS | Дополнительный комментарий проверенной версии. Пустая строка не заменяет правила формы или перевод названия поля. |
Для паспорта импорта нужны ATTR_NAME, NULL-ограничение и явные бизнес-правила. Для унаследованного каталога SOURCE_CLASS объясняет, откуда взялись title и qty. Отображайте DEFAULT как описание, не выполняйте его через eval и не подставляйте SQL-текст напрямую во входные значения.
$title = cubrid_schema($db, CUBRID_SCH_ATTRIBUTE, $tableName, 'title');
if ($title === false) { throw new RuntimeException('Описание title недоступно'); }
$requiredByStorage = $title !== [] && (int)$title[0]['NON_NULL'] === 1;
// Обязательность поля пользовательской формы задаётся отдельно.Для дополнения ATTR_WITH_SYNONYM и прав проверяйте defined("CUBRID_SCH_ATTR_WITH_SYNONYM") на другом установленном драйвере. Оно зарегистрировано в 11.2.0.0002, но отсутствует в перечне 19 типов старой страницы PHP. Обычная таблица и её synonym не превращаются в разные копии данных.
Ключи, индексы и направления связей
Первичный ключ идентифицирует строку, внешний ключ связывает её с другой таблицей. Индекс помогает доступу и может обеспечивать уникальность. Эти понятия связаны, но возвращаются разными типами схемы. Наличие одного индекса не объясняет все правила таблицы.
| Тип и поля | Смысл |
|---|---|
| CONSTRAINT: TYPE | 0 — UNIQUE, 1 — INDEX, 2 — REVERSE UNIQUE, 3 — REVERSE INDEX по базовому контракту. Проверенный сервер не включает сюда PK/FK; таблица только с ними дала [], а sku UNIQUE — одну запись. |
| CONSTRAINT: NAME, ATTR_NAME, KEY_ORDER | Имя ограничения/индекса, его атрибут, позиция от 1. Несколько строк могут относиться к одному составному объекту. |
| CONSTRAINT: NUM_PAGES, NUM_KEYS | Статистика индексной структуры. Полученные «0» не означают, что таблица пуста или запрос обязательно использует индекс. Для плана запроса нужен отдельный анализ. |
| CONSTRAINT: PRIMARY_KEY, ASC_DESC | PRIMARY_KEY присутствует в официальном формате; не заменяет тип PRIMARY_KEY. ASC_DESC — дополнительный ответ проверенного сервера, «A» у нашего UNIQUE означает восходящее направление. |
| PRIMARY_KEY: CLASS_NAME, ATTR_NAME, KEY_SEQ, KEY_NAME | Класс, колонка, позиция в ключе от 1, имя ключа. Массив может прийти в ином порядке — наш составной PK сначала возвращал order_no, затем warehouse. |
| FK: PKTABLE_NAME / PKCOLUMN_NAME | Родительская таблица и её колонка. Имя может быть с владельцем или без него в зависимости от вида запроса. |
| FK: FKTABLE_NAME / FKCOLUMN_NAME | Дочерняя таблица и её колонка. FKTABLE_NAME — имя, не число, несмотря ошибочную пометку base:1 в старой таблице PHP. |
| FK: KEY_SEQ / FK_NAME / PK_NAME | Номер пары колонок от 1 и имена внешнего и родительского ключа. Группируйте по ключу, затем сортируйте пары по KEY_SEQ. |
| FK: UPDATE_RULE / DELETE_RULE | Действия при изменении/удалении родителя: 0 CASCADE, 1 RESTRICT, 2 NO ACTION, 3 SET NULL. Таблица PHP называет поля UPDATE_ACTION/DELETE_ACTION; её пример и фактический ответ используют *_RULE. |
IMPORTED_KEYS(lines) отвечает «на кого ссылаются позиции». EXPORTED_KEYS(orders) — «кто ссылается на заказ». CROSS_REFERENCE(orders, lines) ограничивает пару объектов; четвёртый аргумент здесь имя таблицы. Пустой ответ говорит об отсутствии выбранных объявленных FK, а не об отсутствии любой смысловой связи в приложении.
$pairs = cubrid_schema($db, CUBRID_SCH_CROSS_REFERENCE, $ordersName, $linesName);
if ($pairs === false) { throw new RuntimeException('Связь не прочитана'); }
usort($pairs, static function(array $a, array $b): int {
return (int)$a['KEY_SEQ'] <=> (int)$b['KEY_SEQ'];
});
// warehouse → warehouse; order_no → order_no для схемы заказа.В полном JOIN заказа обе пары участвуют в ON, WHERE выбирает склад 1 и заказ 700, ORDER BY задаёт порядок позиций, LIMIT ограничивает результат. Данные не загружаются целиком для последующего поиска заказа в PHP. Чтение метаданных не отменяет границы транзакций и серверную проверку FK.
Наследование и непосредственные родители
В существующей объектно-реляционной схеме CUBRID таблица может наследовать поля через UNDER. Это не PHP extends и не FK между двумя независимыми строками. В нашем примере item → stock → shelf: title приходит из item, qty из stock, zone_label объявлено в shelf.
| Тип | Фактический ответ и применение |
|---|---|
| SUPERCLASS | CLASS_NAME и TYPE. Для shelf в профиле возвращён stock, а не автоматически все предки вместе с item. Не обещайте транзитивный обход одним вызовом. |
| SUBCLASS | CLASS_NAME и TYPE. Для item возвращён stock. Чтобы строить дерево глубже, нужны отдельные ограниченные шаги и защита от повторного обхода, особенно при нескольких родителях. |
| DIRECT_SUPER_CLASS | В проверенной версии пара CLASS_NAME (shelf) и SUPER_CLASS_NAME (stock). Старая таблица PHP группирует этот тип с CLASS_NAME/TYPE; фактические ключи отличаются. |
| SOURCE_CLASS из ATTRIBUTE | Указывает место объявления конкретного поля. Это удобнее, чем угадывать происхождение по текущему SELECT. |
В полной цепочке наследования сохраняется строка Кабель / A-2 / 7, поэтому связь со схемой видна на данных. Имя zone оказалось зарезервированным в сервере 11.3; пример использует понятное zone_label. DONT_REUSE_OID объявлен явно для этих классов; он не меняет соседние таблицы. Работа с OID экземпляра подробно вынесена в следующую главу.
Методы и файлы реализации: пустой ответ тоже результат
Историческая объектная модель CUBRID допускает методы экземпляра и класса, связанные с внешней реализацией. Это отдельный серверный механизм. PHP-функция, SQL-триггер и метод PHP-объекта не становятся таким методом автоматически.
| Тип | Поля и граница |
|---|---|
| METHOD | NAME — имя метода экземпляра, RET_DOMAIN — домен результата, ARG_DOMAIN — описание доменов аргументов. Не считать ARG_DOMAIN готовым PHP-массивом параметров без разбора контракта конкретной версии. |
| CLASS_METHOD | Те же поля для метода класса; он относится к классу, а не к выбранной строке товара. |
| METHOD_FILE | METHOD_FILE — объявленный файл реализации. Это метаданные имени, не команда скачать, загрузить или исполнить файл. |
$methods = cubrid_schema($db, CUBRID_SCH_METHOD, $className);
if ($methods === false) { throw new RuntimeException('Описание методов недоступно'); }
echo count($methods); // 0 у классов этого урока, где методы не объявлены.В инвентаризации существующего класса все три запроса реально выполняются и возвращают []. Это честный полезный ответ «внешних методов здесь нет», а не заглушка или отказ API. Для схемы, где методы есть, эти поля помогают обнаружить внешние зависимости перед переносом. Их установка и выполнение требуют отдельной проверки файла, ABI и серверной конфигурации; данный урок ничего внешнего не загружает.
Триггеры: целевой класс, событие и видимый журнал
Триггер — действие сервера при событии над таблицей. Журнал изменения qty может появиться даже тогда, когда PHP выполнял только UPDATE и не вызывал INSERT журнала. Метаданные помогают найти причину, а повторное чтение журнала подтверждает поведение.
| Поля | Смысл |
|---|---|
| NAME / STATUS | Имя триггера и состояние; в нашем профиле ACTIVE. Пустой список по имени триггера не доказывает его отсутствия: параметром фильтруется целевой класс. |
| EVENT / TARGET_CLASS / TARGET_ATTR | Событие UPDATE, целевая таблица с владельцем, колонка qty. Для триггеров уровня таблицы атрибут может отличаться/отсутствовать; не предполагайте всегда qty. |
| ACTION_TIME / ACTION | Момент действия (например AFTER) и текст действия. Просмотр текста не выполняет его заново. |
| PRIORITY | Приоритет как строковое число, в примере «0.000000». Не сортируйте как произвольный текст, если нужен числовой порядок. |
| CONDITION_TIME / CONDITION | Момент проверки и текст условия. У проверенного при подготовке триггера без условия поля были пустыми. Для IF-триггера читайте фактические значения и сохраняйте исходный DDL; не переносите пустоту из другого триггера. |
| REMARKS | Дополнительный комментарий сервера, в примере пустая строка. |
$items = cubrid_schema($db, CUBRID_SCH_TRIGGER, 'dba.' . $stockName);
if ($items === false) { throw new RuntimeException('Триггеры не прочитаны'); }
foreach ($items as $item) { echo $item['NAME'], ' → ', $item['EVENT'], "\n"; }Полный журнал изменения 3 → 7 сохраняет ровно одну запись: повторное присваивание 7 не проходит IF old.qty<>obj.qty. Для AFTER UPDATE old обозначает прежний объект, obj — обновлённый. Здесь qty NOT NULL, поэтому сравнение простое; перенос на nullable-поля требует явной обработки SQL NULL. Триггер выполняется в той же транзакции изменения, а не отдельным подключением из PHP.
Права текущего пользователя и synonym
Подключение к базе, имя объекта и разрешение действия — три разных вопроса. CLASS_PRIVILEGE сообщает права текущего пользователя соединения на таблицу; ATTR_PRIVILEGE показывает сведения для её атрибута. Synonym предоставляет другое имя той же цели, а не новые права.
| Тип / поле | Смысл |
|---|---|
| CLASS_PRIVILEGE: CLASS_NAME | Таблица, для которой получены права. У DBA и ограниченного reader ответы различаются. |
| ATTR_PRIVILEGE: ATTR_NAME | Выбранный атрибут. Наличие этого типа API не означает поддержку GRANT на отдельную колонку: в CUBRID 11.3 минимальная единица GRANT — таблица. |
| PRIVILEGE | Вид разрешения, например SELECT, INSERT, UPDATE, DELETE, ALTER, INDEX, EXECUTE. Наличие SELECT не подразумевает UPDATE. |
| GRANTABLE | Возможность передать это разрешение другому пользователю. В проверке DBA дал YES, reader после простого GRANT SELECT — NO. |
| ATTR_WITH_SYNONYM | Описание целевых атрибутов через synonym; CLASS_NAME/SOURCE_CLASS относятся к реальной цели. Проверяйте доступность константы в установленном драйвере. |
В ограниченном отчёте reader читает Кабель — 3 шт., а попытка UPDATE получает отказ −494/facility 1 и исходный остаток остаётся 3. Это код проверенного пути, не общий код всех ошибок доступа. Программа сохраняет диагностику сразу, закрывает reader, затем удаляет собственные synonym, USER и таблицу. Пароль генерируется при запуске и не выводится.
Список разрешений может устареть до следующего действия, поэтому серверный отказ всё равно обрабатывается. Не отдавайте административные метаданные любому посетителю сайта. Допуск loopback в учебном broker ACL разрешает маршрут подключения внутри изолированного namespace; он не заменяет SQL GRANT. Авторизация конкретного пользователя и строк приложения остаётся отдельной задачей.
От описания схемы к конкретному объекту
Теперь схема связана с наблюдаемыми действиями: именованный импорт, JOIN по обеим колонкам ключа, изменение каталога через VIEW, серверный аудит и отдельное разрешение SELECT. Массив метаданных помогает объяснить структуру; он не заменяет параметры SQL, бизнес-проверки или серверную авторизацию.
Следующий шаг — адресовать, изменить и удалить конкретный объект CUBRID по OID. OID относится к экземпляру базы, PK задаётся схемой и правилами идентификации, а PHP-объект лишь представляет данные в программе. Для обычных экранов по-прежнему выбирайте нужные поля, WHERE, ORDER BY и LIMIT; описание схемы не является поводом загружать весь каталог.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
cubrid_schemaПолучить описание классов, колонок, PK/FK, представлений, триггеров, методов и прав; превратить его в понятное решение импорта или сопровождения.
PECL CUBRID ≥8.3.0; с 8.3.1 ошибка false вместо −1. Профиль главы: PHP 7.4.33, native 11.2.0.0002, сервер 11.3.5. · cubrid
cubrid_schema(resource $conn_identifier, int $schema_type, string $class_name = ?, string $attr_name = ?): array|falseКогда импорт сообщает «неизвестное поле», а заказ соединяется не с тем филиалом, полезно получить описание самой базы: какие поля объявлены, как устроен ключ, откуда пришёл атрибут и что разрешено данному соединению. cubrid_schema возвращает эти сведения в PHP-массиве. Она не читает товары вместо SELECT и не создаёт ограничения вместо DDL.
$columns = cubrid_schema($db, CUBRID_SCH_ATTRIBUTE, $tableName);
if ($columns === false) { throw new RuntimeException('Ошибка чтения схемы'); }
foreach ($columns as $column) {
echo $column['ATTR_NAME'], "\n";
}Для таблицы первого урока получится id, sku, title, price_cents, note. $db — уже открытое соединение; константа выбирает форму ответа; $tableName — известное имя таблицы. Один элемент внешнего массива описывает одну колонку, ATTR_NAME читает её имя. Это первый шаг проверки импорта. Для карты заказа та же функция вызывается с другими константами и возвращает сведения о ключах.
Сигнатура и аргументы
cubrid_schema(resource $conn_identifier, int $schema_type, string $class_name = ?, string $attr_name = ?): array|falseЗапись array|false здесь описывает возможные результаты; PHP 7.4 не поддерживает объявление union-типа в пользовательской функции. Знак ? в старой документации обозначает необязательный аргумент без опубликованного значения по умолчанию, а не строку, которую надо передать.
| Аргумент | Что передаётся | Граница |
|---|---|---|
| conn_identifier | Открытый ресурс native cubrid. | Не PDO, request выборки или имя базы строкой. |
| schema_type | Одна константа CUBRID_SCH_* из таблицы ниже. | Это выбор одного типа ответа, не битовая маска для объединения типов. |
| class_name | Класс/представление, цель триггера или родительская сторона связи — зависит от типа. | Передавайте проверенное имя без SQL-кавычек. В CUBRID 11.3 имя с владельцем dba.orders устраняет неоднозначность. |
| attr_name | Атрибут/его шаблон либо дочерний класс для CROSS_REFERENCE. | В проверенном strict_types профиле явный null вызывает TypeError. Если фильтр не нужен, опустите аргумент. Пустая строка фильтра не равна пропуску. |
Результат, ошибка и нумерация
| Результат | Как читать |
|---|---|
| Непустой array | Внешние числовые позиции начинаются с 0; внутри — ассоциативные строки со своими именами полей. Не передавайте этот массив в cubrid_fetch или cubrid_close_request. |
| [] | Успешный запрос не нашёл подходящих описаний: например, нет внешних методов или нет атрибута missing169. Это не false. |
| false | Не удалось получить описание. Немедленно сохраните cubrid_error_code, cubrid_error_code_facility и cubrid_error_msg; последующий вызов может изменить диагностику. |
| Числа в описании | В проверенном профиле TYPE, KEY_SEQ, DOMAIN и флаги приходят строками. Приводите конкретное поле к int для сравнения/сортировки; не превращайте строку DEFAULT в значение строки товара. |
| ATTR_ORDER / KEY_ORDER / KEY_SEQ | Порядковые номера начинаются с 1. Это значения внутри записи, а не индекс внешнего PHP-массива и не id товара. |
Функция доступна с PECL CUBRID 8.3.0. С 8.3.1 отказ возвращает false вместо прежнего −1. Профиль главы: PHP 7.4.33, native cubrid 11.2.0.0002, CCI 11.2.0, сервер 11.3.5. Сигнатура PHP Manual показывает array, но раздел возвратов явно включает false. Неверный тип 9999 в этом профиле дал false с кодом −10015/facility 2; это пример диагностики версии, не универсальная константа всех будущих выпусков.
Все типы ответа: переход к точным полям
Первые 19 строк — базовые типы официальной страницы PHP. Последняя строка — дополнительная константа, зарегистрированная в проверенном native-драйвере; она не выдаётся за двадцатую строку старого PHP Manual. Для совместимости старого приложения проверяйте defined перед использованием дополнения.
| Константа | Результат и переход | Параметры |
|---|---|---|
| CUBRID_SCH_CLASS | Классы и представления: NAME, TYPE, в проверенной версии также REMARKS | Шаблон имени класса; ограничьте выбор собственной схемой. |
| CUBRID_SCH_VCLASS | Только представления: NAME, TYPE, REMARKS | Имя или шаблон представления. |
| CUBRID_SCH_QUERY_SPEC | Определение представления: QUERY_SPEC | Точное имя представления. |
| CUBRID_SCH_ATTRIBUTE | Поля экземпляра: ATTR_NAME, DOMAIN и другие свойства | Точный класс; необязательный шаблон атрибута. |
| CUBRID_SCH_CLASS_ATTRIBUTE | Общие атрибуты класса, та же структура описания | Точный класс; необязательный шаблон атрибута. |
| CUBRID_SCH_METHOD | Методы экземпляра: NAME, RET_DOMAIN, ARG_DOMAIN | Точное имя класса. |
| CUBRID_SCH_CLASS_METHOD | Методы класса: NAME, RET_DOMAIN, ARG_DOMAIN | Точное имя класса. |
| CUBRID_SCH_METHOD_FILE | Файлы реализации методов: METHOD_FILE | Точное имя класса. |
| CUBRID_SCH_SUPERCLASS | Родительские классы: CLASS_NAME, TYPE | Точное имя класса; не считать ответ готовым транзитивным деревом. |
| CUBRID_SCH_SUBCLASS | Дочерние классы: CLASS_NAME, TYPE | Точное имя класса; глубину проверяйте явно. |
| CUBRID_SCH_CONSTRAINT | UNIQUE и индексы: TYPE, NAME, ATTR_NAME и статистика | Точный класс; отдельные типы ниже нужны для PK/FK. |
| CUBRID_SCH_TRIGGER | Серверные триггеры: NAME, событие, цель, действие | Целевой класс, например dba.orders, а не имя триггера. |
| CUBRID_SCH_CLASS_PRIVILEGE | Права на класс: CLASS_NAME, PRIVILEGE, GRANTABLE | Точный класс; права текущего пользователя соединения. |
| CUBRID_SCH_ATTR_PRIVILEGE | Сведения о правах атрибута: ATTR_NAME, PRIVILEGE, GRANTABLE | Класс и имя атрибута в четвёртом аргументе. |
| CUBRID_SCH_DIRECT_SUPER_CLASS | Явная связь: CLASS_NAME → SUPER_CLASS_NAME в проверенной версии | Точный класс; старый PHP Manual группирует этот ответ с CLASS_NAME/TYPE. |
| CUBRID_SCH_PRIMARY_KEY | Первичный ключ: CLASS_NAME, ATTR_NAME, KEY_SEQ, KEY_NAME | Точный класс; сортируйте колонки по KEY_SEQ. |
| CUBRID_SCH_IMPORTED_KEYS | Внешние ключи, которыми данный класс ссылается на родителей | class_name — дочерний класс. |
| CUBRID_SCH_EXPORTED_KEYS | Внешние ключи других классов, ссылающихся на данный | class_name — родительский класс. |
| CUBRID_SCH_CROSS_REFERENCE | Связи между заданными родительским и дочерним классами | class_name — родитель; attr_name — дочерний класс, не колонка. |
| CUBRID_SCH_ATTR_WITH_SYNONYM | Атрибуты с разрешением synonym, структура ATTRIBUTE | Дополнение native 11.2.0.0002; имя класса или synonym, необязательный шаблон атрибута. |
Применение 1: не отправить неизвестное поле импорта
$metadata = cubrid_schema($db, CUBRID_SCH_ATTRIBUTE, $tableName);
if ($metadata === false) { throw new RuntimeException('Схема недоступна'); }
$knownNames = array_column($metadata, 'ATTR_NAME');
$unknownNames = array_diff(array_keys($incoming), $knownNames);
if ($unknownNames) { throw new InvalidArgumentException('Есть неизвестные поля'); }
// Отдельно: allowlist редактируемых полей и проверка значений перед INSERT.В самостоятельном паспорте импорта discount отклоняется, а Кабель USB сохраняется за 1290 копеек. Совпадение имени не разрешает менять служебный id или передавать отрицательную цену: это отдельные правила приложения.
Применение 2: восстановить порядок составного ключа
$key = cubrid_schema($db, CUBRID_SCH_PRIMARY_KEY, $ordersName);
if ($key === false) { throw new RuntimeException('Ключ не прочитан'); }
usort($key, static function(array $a, array $b): int {
return (int)$a['KEY_SEQ'] <=> (int)$b['KEY_SEQ'];
});
echo implode(' + ', array_column($key, 'ATTR_NAME')); // warehouse + order_noВ заказе склада 1 №700 это защищает понимание JOIN: одного order_no недостаточно, потому что у склада 2 тоже есть заказ 700. Не стройте SQL из любого присланного имени: метаданные помогают проверить структуру, а список допустимых таблиц и операций остаётся вашим.
Шаблоны и расхождения старой документации
В native 11.2.0.0002 CLASS/VCLASS/TRIGGER включают режим шаблона имени класса; ATTRIBUTE/CLASS_ATTRIBUTE/ATTR_WITH_SYNONYM — шаблон имени атрибута. % и _ имеют смысл шаблона, поэтому имя c169_* с подчёркиваниями не считается автоматически строгим равенством. Полные примеры дополнительно отбирают точное имя и владельца в известной отдельной базе. Для TRIGGER проверяется имя целевого класса, затем NAME нужного триггера в полученных строках.
Старая фраза PHP Manual ограничивает attr_name типом ATTR_PRIVILEGE, но собственный пример этой страницы фильтрует ATTRIBUTE, а исходник драйвера явно включает описанные режимы. В подготовке пропущенный четвёртый аргумент вернул атрибуты; пустая строка и отсутствующее имя дали []; null при strict_types вызвал TypeError. CROSS_REFERENCE использует четвёртую строку для имени дочернего класса. Не переносите одинаковое назначение параметра на все типы.
Описание не является снимком бизнес-данных, разрешением на действие или универсальным мигратором. После чтения схемы объект может измениться, SELECT/INSERT всё равно требуют обработки отказа. Большая схема может дать большой массив: сужайте класс/атрибут, не перечитывайте все объекты при каждом показе товара. Данные результатов этой главы относятся к собственным объектам. Следующий шаг — конкретный объект базы по OID; описание полей текущего SELECT рассматривается отдельно в метаданных результата запроса.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


