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

Дерево руководства
Глава 290 · Практика
PDO_CUBRID: понятное превью, связи импорта и паспорт схемы
Ограниченное превью, составные связи импорта и происхождение полей: реальные PDO_CUBRID-проекты, метаданные и паспорт схемы.
Что показать менеджеру и что проверить перед импортом
Перед загрузкой заказов нужно понять, какие поля образуют ключ и в каком порядке вставлять связанные записи. Перед показом каталога — выбрать нужные строки, дать колонкам понятные подписи и отличить нулевой остаток от неизвестного значения. Перед переносом старой схемы — увидеть, какие поля унаследованы и какой триггер записывает изменения. Эти задачи связаны описанием данных, но требуют разных источников информации.
| Нужный результат | Что читаем | Чего этот ответ не означает |
|---|---|---|
| Превью трёх товаров и CSV | Ограниченный SELECT и getColumnMeta его колонок | Это не полная схема всех таблиц и не загрузка каталога в PHP. |
| Порядок импорта заказов и строк | Первичные/внешние ключи через cubrid_schema | Связь не соединяет строки автоматически: в JOIN нужно написать обе части ключа. |
| Паспорт складской позиции и её журнала | Наследование, атрибуты, триггер, права и реальные записи | Описание триггера не доказывает, что он сработал: это проверяется отдельным SQL-чтением. |
PDO — настроенное соединение с базой. PDOStatement — отдельный подготовленный запрос с результатом и положением курсора. Строка, полученная fetch(PDO::FETCH_ASSOC), — уже обычный PHP-массив: его ключи берутся из выбранных имён колонок. getColumnMeta описывает колонку результата; cubrid_schema описывает объект схемы базы. Поэтому поле item_title в SELECT title AS item_title и атрибут title в таблице относятся к одному чтению, но имеют разные задачи.
- Таблица goods: id, title, price, qty, note
- SELECT id AS item_id, title AS item_title, price, qty, note ORDER BY id LIMIT 3
- getColumnMeta(1): подпись item_title и сведения выбранной колонки
- cubrid_schema(ATTRIBUTE, goods): описание атрибута title таблицы
- fetch: Кабель O’Reilly / 125.50 / 3 / NULL → понятная строка превью
Сначала нужны основы PDO и подключение и границы операции PDO_CUBRID. Здесь каждый новый вызов разобран до полного проекта. Класс таблицы CUBRID и наследование её атрибутов далее относятся к схеме базы, а не к классам и объектам PHP. Поддержка этого метода специфична для PDO_CUBRID: один интерфейс PDO не делает его доступным в другом драйвере.
Приложению нужны определённые строки: товары на экране, остаток после заявки или описание столбцов для импорта. PDO даёт объект соединения и объекты подготовленных запросов. PDO_CUBRID связывает этот интерфейс с CUBRID; особенности SQL, типов и транзакций остаются важны.
Проверяемая среда совместимости: PHP 7.4.33, PDO core, PDO_CUBRID v11.2.0.0002 и CUBRID 11.3.5. Это отдельный стенд для исторического драйвера, а не рекомендация менять PHP существующего сайта. Начало PDO объясняет общий интерфейс, а начало CUBRID — таблицу и SQL.
Метаданные выбранных колонок: все 14 полей PDO_CUBRID
Колонка price в таблице имеет DECIMAL(10,2), а в превью называется price. Поле title через AS становится item_title. Для подписи выбранных столбцов нужна информация результата; для разрешённых полей редактирования — собственная карта приложения и схема таблицы. Не восстанавливайте право на UPDATE из одного имени, полученного getColumnMeta.
$stmt = $db->prepare('SELECT title AS item_title, price FROM goods ORDER BY id LIMIT 3');
$stmt->execute();
$meta = $stmt->getColumnMeta(0);
if ($meta === false) { throw new RuntimeException('Колонка не описана'); }
echo $meta['name']; // item_title
$stmt->closeCursor();Номер 0 означает первую колонку результата, а не строку и не ATTR_ORDER схемы. getColumnMeta вызывается после выполнения запроса. Для всего описания перебирают от 0 до columnCount()-1. Передача несуществующего номера или отсутствие результата даёт false по общему PDO-контракту; точную диагностику предоставляет драйвер. Четырнадцать полей ниже — именно форма PDO_CUBRID, не список гарантированных ключей любого PDO-драйвера.
| Поле getColumnMeta | Что можно из него понять |
|---|---|
| type | Имя SQL-типа этой выбранной колонки; не общий pdo_type из другого драйвера. |
| name | Имя результата, в том числе AS item_title; оно удобно для подписи CSV/экрана. |
| table | Имя исходного класса таблицы, если сервер может связать выражение с ним. Не универсальный парсер происхождения JOIN/выражения. |
| def | Описание SQL default. Это текст схемы, его нельзя выполнять через eval или считать текущим значением ячейки. |
| precision | Точность SQL-типа; для DECIMAL(10,2) десять цифр всего, а не десять знаков после точки. |
| scale | Масштаб точного числа: для DECIMAL(10,2) два десятичных разряда. |
| not_null | Признак запрета SQL NULL; пустая строка и ноль этим запретом не исключаются. |
| auto_increment | Автоматическое получение значения соответствующего атрибута. |
| unique_key | Признак уникального ключа в метаданных результата; полную структуру читают отдельным schema-запросом. |
| multiple_key | В закреплённом PHP7-драйвере вычислен как отрицание unique_key. Его нельзя выдавать за надёжный список неуникальных индексов. |
| primary_key | Признак участия в первичном ключе, когда он отражён в метаданных колонки. |
| foreign_key | Признак внешнего ключа; не содержит полного соответствия составных колонок. |
| reverse_index | Признак обратного индекса в описании колонки. |
| reverse_unique | Признак обратного уникального индекса. |
В реальном PHP 7.4.33 к этим 14 полям драйвера PDO core добавил len и pdo_type: всего 16 ключей. Для INTEGER получены len=11 и pdo_type=PDO::PARAM_STR; для NUMERIC(10,2) — len=12, precision=10, scale=2. Эти значения не означают, что число надо печатать с одиннадцатью знаками. У alias item_id первичный ключ не был помечен primary_key, хотя таблица имеет PK. Поэтому паспорт схемы и разрешения нельзя строить только на flags результата.
NULL, пустая строка и число 0 имеют разные значения. NULL означает отсутствие значения, пустая строка — присутствующий текст длины ноль, qty=0 — известный нулевой остаток. Превью выводит их раздельно, а цену не превращает в приблизительный float без необходимости. Полученный тип PHP проверяется на используемом драйвере: имя SQL INTEGER само по себе не гарантирует PHP int.
Положение в результате: forward-only и scroll
Превью иногда просит показать вторую строку уже выбранного короткого списка, затем первую и последнюю. Курсор хранит позицию внутри результата конкретного PDOStatement. Он не является номером записи в таблице: порядок задаёт ORDER BY, а состав списка — WHERE и LIMIT. Случайный доступ к трём строкам допустим; получение сотен тысяч строк ради перехода к одной странице обычно требует другого SQL-плана.
$stmt = $db->prepare(
'SELECT id,title FROM goods ORDER BY id LIMIT 3',
[PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]
);
$stmt->execute();
$first = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_FIRST);
$last = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_LAST);
$stmt->closeCursor();По умолчанию запрашивается CURSOR_FWDONLY: обычный fetch идёт вперёд. CURSOR_SCROLL выбирается при prepare, до execute. Первый аргумент fetch задаёт форму строки; второй — направление; третий используется ABS и REL. FIRST и LAST выбирают крайние строки, NEXT и PRIOR перемещают на одну, ABS задаёт абсолютное смещение, REL — относительное. Нумерацию ABS нельзя переносить из другого PDO-драйвера: закреплённый PDO_CUBRID передаёт смещение непосредственно CCI, без добавления единицы. Полный проект фиксирует наблюдённые позиции данного сочетания версий.
В проверенном scroll-режиме ABS 1 выбрал первую строку, ABS 0 вернул false с SQLSTATE 00000, а REL 1 после первой выбрал вторую. Переход назад в forward-only дал PDOException с HY000. Это не SQL OFFSET: запрос с LIMIT уже определил ограниченный результат, затем курсор перемещается внутри него. Само имя FETCH_ORI_ABS не гарантирует одинаковое начало нумерации у всех драйверов.
false может означать конец результата либо отказ. При ERRMODE_EXCEPTION ожидаемый отказ всё равно нужно поймать и сохранить SQLSTATE/код; при другом режиме сравнить результат и errorInfo. Не продолжайте печатать прежнюю строку после неуспешного fetch. closeCursor завершает результат; объект запроса можно освободить присваиванием null, когда он больше не нужен. Полученные раньше обычные PHP-массивы остаются значениями, но не становятся автоматически свежими при изменении базы.
Превью ограниченного каталога: колонка, значение и положение курсора
Менеджер проверяет первые три позиции выгрузки: названия колонок, точную цену, нулевой остаток и отсутствие примечания. Ему нужен небольшой понятный preview.csv, а не массив всего каталога. SQL ограничивает список, метаданные объясняют столбцы, scroll-курсор позволяет вернуться к нужной строке этого списка.
- SQL ORDER BY id LIMIT 3 → ID 1, 2, 3
- getColumnMeta(1) → item_title; цена DECIMAL(10,2)
- FIRST 1 → LAST 3 → ABS 2 → REL -1 возвращает 1
- NULL → [NULL] в CSV; пустое примечание остаётся пустым; qty=0 остаётся 0
Минимальный вызов
$stmt = $db->prepare('SELECT id,title FROM goods ORDER BY id LIMIT 3', [PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL]);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_FIRST);
echo $row['title']; // Кабель O’Reilly в данных проекта
$stmt->closeCursor();Первый аргумент prepare — SQL, второй — настройки запроса. CURSOR_SCROLL относится к результату этого statement. FETCH_ASSOC задаёт именованные ключи строки, FIRST выбирает её положение. Это короткая запись для уже открытого $db и созданной таблицы goods; самостоятельный проект создаёт собственные данные и сначала показывает ограничение forward-only.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Первая строка | 1 / Кабель O'Reilly / 125.50 / 3 / NULL |
| Вторая строка | 2 / Хомут / 1.25 / 0 / пустая строка |
| Последняя строка | 3 / Розетка / 250.00 / 7 / под заказ |
| Названия колонок | item_id, item_title, price, qty, note |
| Навигация FIRST → LAST → ABS 2 → REL -1 | 1 → 3 → 2 → 1 |
| Возврат в forward-only | Отклонён; открыт отдельный scroll-запрос |
| CSV | preview.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);
function schemaRows175(PDO $db,int $type,string $name,?string $second=null): array {
$call=p175Capture(static function()use($db,$type,$name,$second){
return $second===null?$db->cubrid_schema($type,$name):$db->cubrid_schema($type,$name,$second);
},$db);
if ($call['exception'] || $call['warning'] || $call['value']===false || !is_array($call['value'])) {p175Failure('Описание схемы недоступно',$call);}
return $call['value'];
}
function csv175(string $filename,array $records): void {
$fp=fopen(__DIR__.'/'.$filename,'wb');
if (!is_resource($fp)) {throw new RuntimeException('CSV не открыт');}
try {foreach($records as $row){if(fputcsv($fp,$row,';','"','')===false){throw new RuntimeException('Строка CSV не записана');}}}
finally {fclose($fp);}
}
function baseName175(string $value): string {
$parts=explode('.',$value);return end($parts);
}
function orderedColumns175(array $rows,string $column,string $position): array {
usort($rows,static function($a,$b)use($position){return (int)$a[$position]<=>(int)$b[$position];});
return array_column($rows,$column);
}
$name=p175OwnName($db,$owned,'TABLE','preview');$table=p175Qualified($name);
p175Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,title VARCHAR(80) NOT NULL,price DECIMAL(10,2),qty INT DEFAULT 0,note VARCHAR(80))');
foreach([[1,"Кабель O'Reilly",'125.50',3,null],[2,'Хомут','1.25',0,''],[3,'Розетка','250.00',7,'под заказ'],[4,'Коробка','5.00',8,'за пределами списка']] as $record){
p175Command($db,'INSERT INTO '.$table.' (id,title,price,qty,note) VALUES (?,?,?,?,?)',$record);
}
$sql='SELECT id AS item_id,title AS item_title,price,qty,note FROM '.$table.' ORDER BY id LIMIT 3';
$forward=$db->prepare($sql);$firstTwo=[];
try {
$forward->execute();
$firstTwo[]=$forward->fetch(PDO::FETCH_ASSOC);$firstTwo[]=$forward->fetch(PDO::FETCH_ASSOC);
$back=p175Capture(static function()use($forward){return $forward->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_PRIOR);},$forward);
} finally {p175Release($forward);}
$scroll=$db->prepare($sql,[PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL]);$meta=[];$selected=[];
try {
$scroll->execute();
for($column=0;$column<$scroll->columnCount();$column++){
$info=$scroll->getColumnMeta($column);
if(!is_array($info)){throw new RuntimeException('Метаданные колонки отсутствуют');}
$meta[]=$info;
}
$first=$scroll->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_FIRST);
$last=$scroll->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_LAST);
$second=$scroll->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_ABS,2);
$relativeFirst=$scroll->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_REL,-1);
$row=$scroll->fetch(PDO::FETCH_ASSOC,PDO::FETCH_ORI_FIRST);
while($row!==false){$selected[]=$row;$row=$scroll->fetch(PDO::FETCH_ASSOC);}
if($scroll->errorCode()!=='00000'){throw new RuntimeException('Обход результата не завершён корректно');}
} finally {p175Release($scroll);}
$records=[['item_id','item_title','price','qty','note']];
foreach($selected as $row){$records[]=[(string)$row['item_id'],$row['item_title'],(string)$row['price'],(string)$row['qty'],$row['note']===null?'[NULL]':$row['note']];}
csv175('preview.csv',$records);
$rows=[['Первая строка',"1 / Кабель O'Reilly / 125.50 / 3 / NULL"],['Вторая строка','2 / Хомут / 1.25 / 0 / пустая строка'],
['Последняя строка','3 / Розетка / 250.00 / 7 / под заказ'],['Названия колонок',implode(', ',array_column($meta,'name'))],
['Навигация FIRST → LAST → ABS 2 → REL -1',implode(' → ',[(string)$first['item_id'],(string)$last['item_id'],(string)$second['item_id'],(string)$relativeFirst['item_id']])],
['Возврат в forward-only','Отклонён; открыт отдельный scroll-запрос'],['CSV','preview.csv']];
$checks=['limited_three'=>count($selected)===3,'unicode_apostrophe'=>$selected[0]['item_title']==="Кабель O'Reilly",
'null_empty_zero'=>$selected[0]['note']===null&&$selected[1]['note']===''&&(string)$selected[1]['qty']==='0',
'column_aliases'=>array_column($meta,'name')===['item_id','item_title','price','qty','note'],
'decimal_metadata'=>(int)$meta[2]['precision']===10&&(int)$meta[2]['scale']===2,
'fourteen_metadata_fields'=>count(array_intersect(['type','name','table','def','precision','scale','not_null','auto_increment','unique_key','multiple_key','primary_key','foreign_key','reverse_index','reverse_unique'],array_keys($meta[0])))===14,
'pdo_core_metadata'=>array_key_exists('len',$meta[0])&&array_key_exists('pdo_type',$meta[0])&&count($meta[0])===16,
'forward_two'=>(string)$firstTwo[0]['item_id']==='1'&&(string)$firstTwo[1]['item_id']==='2',
'forward_refused'=>$back['value']===false&&$back['sqlstate']!=='00000',
'scroll_positions'=>[(string)$first['item_id'],(string)$last['item_id'],(string)$second['item_id'],(string)$relativeFirst['item_id']]===['1','3','2','1'],
'artifact_rows'=>$records===[['item_id','item_title','price','qty','note'],['1',"Кабель O'Reilly",'125.50','3','[NULL]'],['2','Хомут','1.25','0',''],['3','Розетка','250.00','7','под заказ']]];
$observations=['selected_rows'=>$selected,'column_metadata'=>$meta,'forward_rejection'=>array_diff_key($back,['value'=>true]),
'positions'=>['FIRST'=>$first['item_id'],'LAST'=>$last['item_id'],'ABS_2'=>$second['item_id'],'REL_minus_1'=>$relativeFirst['item_id']],
'csv_null_marker'=>'[NULL]','all_catalog_loaded'=>false];
} finally {
if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}
p175Report('pdoc175-result-preview','Превью ограниченного каталога: колонка, значение и положение курсора',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первая строка — 1 / Кабель O'Reilly / 125.50 / 3 / NULL
Вторая строка — 2 / Хомут / 1.25 / 0 / пустая строка
Последняя строка — 3 / Розетка / 250.00 / 7 / под заказ
Названия колонок — item_id, item_title, price, qty, note
Навигация FIRST → LAST → ABS 2 → REL -1 — 1 → 3 → 2 → 1
Возврат в forward-only — Отклонён; открыт отдельный scroll-запрос
CSV — preview.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать превью каталога CSV
Границы решения
Положение ABS проверяется для закреплённого PDO_CUBRID, а не объявляется одинаковым для всех PDO-драйверов. Scroll не заменяет SQL-пагинацию и не доказывает малое потребление памяти на большом результате. [NULL] — соглашение нашего экспорта, не специальное значение базы. CSV не становится безопасной электронной таблицей для любых пользовательских строк без отдельной политики формул. Полученные metadata не используются как автоматическое разрешение редактировать столбцы.
Попробуйте изменить условие
В CSV нужно добавить вторую колонку с подписью «Комментарий отсутствует» только для SQL NULL. Почему нельзя использовать empty($row['note'])?
Разбор
empty объединит несколько различных значений. Проверяйте $row['note'] === null, затем отдельно пустую строку. Для количества и цены выберите собственное правило преобразования, не заменяйте их все условием «пусто». Для структуры таблицы переходите к PDO::cubrid_schema.
PDO::cubrid_schema: параметры, все 19 форм ответа и границы
Импортёр получает две таблицы: orders с ключом (warehouse, order_no) и order_lines, где каждая строка хранит эти же два значения. Чтобы не прикрепить строку склада 2 к заказу склада 1 с тем же номером 501, нужно прочитать и сохранить обе части связи. PDO::cubrid_schema возвращает описание схемы, не заказы и не готовый JOIN.
- cubrid_schema(PRIMARY_KEY, orders) → строки метаданных
- ATTR_NAME=warehouse, KEY_SEQ=1; ATTR_NAME=order_no, KEY_SEQ=2
- Небольшой массив описания сортируем по KEY_SEQ → [warehouse, order_no]
- Сначала импортируем orders; затем order_lines с обоими значениями
- JOIN сопоставляет warehouse И order_no → только нужный заказ
$keys = $db->cubrid_schema(PDO::CUBRID_SCH_PRIMARY_KEY, $ordersName);
if ($keys === false) { throw new RuntimeException('Описание ключа недоступно'); }
usort($keys, static function ($a, $b) {
return (int)$a['KEY_SEQ'] <=> (int)$b['KEY_SEQ'];
});
foreach ($keys as $key) { echo $key['ATTR_NAME'], '\n'; }Вызов $db-> означает метод конкретного PDO-соединения; PDO::CUBRID_SCH_PRIMARY_KEY — именованный вид ответа. $ordersName — доверенное имя уже созданной таблицы, не SQL-текст пользователя. Внешний массив содержит строки описания; внутри каждой есть именованные поля. Для нашей таблицы после сортировки выводятся warehouse и order_no. Сортируется маленькое описание ключа, а не выгруженный каталог. Полный проект импорта проверяет связи и реальные строки заказа.
PDO::cubrid_schema(
int $schema_type,
string $table_name = ?,
string $col_name = ?
): array|falseЭто запись исторического контракта из справочника, не объявление для копирования в PHP: знак ? здесь означает необязательный аргумент без указанного в документации конкретного default. Метод существует в PDO_CUBRID начиная с PECL 8.3.0.0001. В закреплённом PHP7-исходнике параметры разбираются как целое и до двух строк; пропущенные имена передаются в CCI как NULL. Пустая строка — переданная строка, а не обещанный эквивалент пропуска. Допустимые сочетания зависят от вида схемы.
| Аргумент | Как выбирать |
|---|---|
| $schema_type | Одна из 19 официальных PDO::CUBRID_SCH_* ниже. Эти числа — альтернативные виды ответа, их не складывают через |. |
| $table_name | Нужное имя объекта или поддерживаемый данным видом шаблон. Для ключей и зависимостей передавайте точное доверенное имя. |
| $col_name | Имя/шаблон атрибута у поддерживающих его видов; в CROSS_REFERENCE это имя дочерней таблицы. Краткая фраза документации «только COL_PRIVILEGE» не исчерпывает фактическую передачу аргумента CCI. |
У TABLE и VIEW драйвер сам включает поиск по шаблону имени класса; у ATTRIBUTE и TABLE_ATTRIBUTE — шаблон атрибута, но не произвольный шаблон таблицы. У остальных 19 видов установлен flags=0. Параметра flags у этого PDO-метода нет. Поэтому нельзя переносить сюда четвёртый аргумент native cubrid_schema или предполагать, что % одинаково работает везде. Для поиска собственной таблицы среди шаблонных результатов дополнительно сверяют полученное NAME с ожидаемым именем.
Успех возвращает массив, в том числе [] при отсутствии подходящих описаний. Ошибка возвращает false и сведения PDO; механизм исключения зависит от пути драйвера и выбранного ERRMODE. Сохраняйте errorInfo сразу, до следующего вызова, и явно проверяйте === false. Не превращайте ошибку в «методов нет» через пустую проверку if (!$rows). Внутренний schema-request закрывается драйвером; возвращённый PHP-массив не требует closeCursor.
| PDO::CUBRID_SCH_… | Для чего | Поля по официальному контракту | Толкование и граница |
|---|---|---|---|
| TABLE | Каталог таблиц и представлений | NAME, TYPE | TYPE: 0 — системная таблица, 1 — представление, 2 — обычная таблица. Имя выбирает набор объектов; режим шаблона отличается от точного поиска ключей. |
| VIEW | Только представления | NAME, TYPE | TYPE=1. Представление сохраняет запрос, а не отдельную копию всех его строк. |
| QUERY_SPEC | SQL определения представления | QUERY_SPEC | Этот текст нужен для ревизии зависимостей. Не выполняйте полученную строку автоматически как миграцию. |
| ATTRIBUTE | Атрибуты экземпляров строки | ATTR_NAME, DOMAIN, SCALE, PRECISION, INDEXED, NOT NULL, SHARED, UNIQUE, DEFAULT, ATTR_ORDER, CLASS_NAME, SOURCE_CLASS, IS_KEY | ATTR_ORDER начинается с 1. DOMAIN описывает SQL-тип; SOURCE_CLASS помогает найти происхождение унаследованного атрибута. |
| TABLE_ATTRIBUTE | Атрибуты класса таблицы | ATTR_NAME, DOMAIN, SCALE, PRECISION, INDEXED, NOT NULL, SHARED, UNIQUE, DEFAULT, ATTR_ORDER, CLASS_NAME, SOURCE_CLASS, IS_KEY | Классовый атрибут не является отдельным обычным полем каждой строки. Формат тот же, смысл области хранения другой. |
| METHOD | Методы экземпляров CUBRID | NAME, RET_DOMAIN, ARG_DOMAIN | Описание внешних методов базы; не методы PDO или PHP-класса. Отсутствие записей может быть нормальным результатом. |
| TABLE_METHOD | Методы класса CUBRID | NAME, RET_DOMAIN, ARG_DOMAIN | Классовые методы имеют иную область вызова, но тот же формат описания доменов результата и аргументов. |
| METHOD_FILE | Файлы методов | METHOD_FILE | Инвентаризация внешних зависимостей. Чтение имени не загружает файл и не подтверждает его ABI/безопасность. |
| SUPER_TABLE | Все родительские классы таблицы | CLASS_NAME, TYPE | Нужны при анализе полной цепочки наследования; TYPE имеет ту же классификацию 0/1/2. |
| DIRECT_SUPER_TABLE | Непосредственные родители | CLASS_NAME, TYPE | В длинной цепочке не заменяет SUPER_TABLE. В нашем проекте один прямой родитель. |
| SUB_TABLE | Дочерние классы таблицы | CLASS_NAME, TYPE | Помогает оценить затрагиваемые типы данных до изменения родительской схемы. |
| CONSTRAINT | Описание уникальности и индексов | TYPE, NAME, ATTR_NAME, NUM_PAGES, NUM_KEYS, PRIMARY_KEY, KEY_ORDER | Официальный TYPE: 0 unique, 1 index, 2 reverse unique, 3 reverse index. KEY_ORDER начинается с 1; первичные и внешние ключи читаются также отдельными видами ниже. Статистика не равна числу строк прикладного запроса. |
| TRIGGER | Условия и действие триггера | NAME, STATUS, EVENT, TARGET_CLASS, TARGET_ATTR, ACTION_TIME, ACTION, PRIORITY, CONDITION_TIME, CONDITION | Второй аргумент — целевая таблица, например dba.stock_item, а не имя триггера. Из полученных описаний выбирают нужный NAME. Описание времени/события/условия не заменяет проверку реально появившейся записи журнала. |
| TABLE_PRIVILEGE | Права на таблицу | CLASS_NAME, PRIVILEGE, GRANTABLE | GRANTABLE относится к передаче привилегии. Ответ в административном соединении не доказывает успешный вход и полномочия другого пользователя. |
| COL_PRIVILEGE | Права атрибута | ATTR_NAME, PRIVILEGE, GRANTABLE | Третий аргумент выбирает атрибут. Нельзя превращать такую запись в неподтверждённое обещание, что сервер поддерживает независимый GRANT на любую колонку. |
| PRIMARY_KEY | Состав первичного ключа | CLASS_NAME, ATTR_NAME, KEY_SEQ, KEY_NAME | Одна строка на компонент. KEY_SEQ начинается с 1; порядок возвращённого массива не заменяет этот номер. |
| IMPORTED_KEYS | Связи, входящие в данную таблицу | PKTABLE_NAME, PKCOLUMN_NAME, FKTABLE_NAME, FKCOLUMN_NAME, KEY_SEQ, UPDATE_ACTION, DELETE_ACTION, FK_NAME, PK_NAME | Таблица здесь дочерняя: её FK ссылается на родителя. Пару колонок связывает KEY_SEQ с 1. |
| EXPORTED_KEYS | Ссылки других таблиц на данную | PKTABLE_NAME, PKCOLUMN_NAME, FKTABLE_NAME, FKCOLUMN_NAME, KEY_SEQ, UPDATE_ACTION, DELETE_ACTION, FK_NAME, PK_NAME | Таблица здесь родительская. Полезно до импорта или удаления, чтобы увидеть зависимые записи. |
| CROSS_REFERENCE | Внешние ключи между двумя таблицами | PKTABLE_NAME, PKCOLUMN_NAME, FKTABLE_NAME, FKCOLUMN_NAME, KEY_SEQ, UPDATE_ACTION, DELETE_ACTION, FK_NAME, PK_NAME | По CCI первый аргумент имени задаёт родителя, второй — ребёнка. Это важное расширение смысла третьего аргумента по сравнению с кратким описанием col_name в PHP Manual. |
Как читать поля атрибута, ключа и триггера
| Группа полей | Смысл на данных проекта |
|---|---|
| ATTRIBUTE: ATTR_NAME, CLASS_NAME, SOURCE_CLASS | ATTR_NAME — имя поля. CLASS_NAME относится к описываемому классу таблицы; SOURCE_CLASS показывает место объявления. Для дочернего stock_item атрибут title приходит из base_item, а qty определён локально. |
| ATTRIBUTE: DOMAIN, PRECISION, SCALE | DOMAIN описывает SQL-домен/тип в формате сервера. PRECISION и SCALE уточняют размер/точность. Это не текущая длина строки «Кабель» и не число строк результата; значение типа следует читать вместе с версией CCI. |
| ATTRIBUTE: INDEXED, NOT NULL, SHARED, UNIQUE, IS_KEY | В базовом формате 1 включает соответствующий признак: индексированность, запрет NULL, общий атрибут, уникальность, ключ. Признаки не раскрывают полный состав PK/FK и не заменяют специальные запросы ключей. SHARED в описании не следует путать с массивом PHP, общим между функциями. |
| ATTRIBUTE: DEFAULT, ATTR_ORDER | DEFAULT — серверное описание значения по умолчанию. ATTR_ORDER нумерует атрибуты с 1; это не индекс PHP-массива. Для фиксированного импорта приложение сопоставляет имена и допустимые типы, а не вслепую вставляет столбцы в полученном порядке. |
| CONSTRAINT: TYPE, NAME, ATTR_NAME, PRIMARY_KEY, KEY_ORDER | TYPE определяет вид уникальности/индекса; NAME группирует строки одного объекта, ATTR_NAME называет компонент, KEY_ORDER задаёт его позицию с 1. PRIMARY_KEY — признак в этой форме, но не обещание, что ответ содержит полное описание всех PK/FK. |
| CONSTRAINT: NUM_PAGES, NUM_KEYS | Статистика структуры индекса. Ноль в одном снимке не означает отсутствие прикладных данных или бесполезность индекса. Нужность индекса оценивают по запросу, плану и измерениям. |
| METHOD: NAME, RET_DOMAIN, ARG_DOMAIN | Имя метода, домен результата и описание аргументов внешнего метода базы. METHOD_FILE даёт имя его файла. Наши схемы не устанавливают такие методы, поэтому проверяемое [] означает отсутствие этой зависимости. |
| TRIGGER: NAME, STATUS, EVENT, TARGET_CLASS, TARGET_ATTR | Имя и состояние триггера, событие, целевая таблица и атрибут. В проекте цель — изменение qty у stock_item. Номер события/состояния читают по версии CUBRID, а не как PHP-константу режима fetch. |
| TRIGGER: ACTION_TIME, ACTION, PRIORITY | Когда и какое SQL-действие выполняется, с каким приоритетом. Наше AFTER UPDATE записывает old.qty и obj.qty в qty_audit; текст ACTION не исполняется приложением повторно. |
| TRIGGER: CONDITION_TIME, CONDITION | Время оценки и условие запуска. Условие old.qty<>obj.qty объясняет, почему установка того же количества не должна добавлять новую запись. Последующая выборка журнала проверяет фактический результат. |
| PRIVILEGE: CLASS_NAME/ATTR_NAME, PRIVILEGE, GRANTABLE | Имя объекта, описанная привилегия и возможность её передачи. Это сведения в контексте текущего соединения. Они не доказывают пароль, пользовательскую HTTP-сессию или право другого DB-пользователя. |
| KEY: PKTABLE_NAME/PKCOLUMN_NAME, FKTABLE_NAME/FKCOLUMN_NAME | Имена родительской и дочерней таблиц и их соответствующих полей. В заказе на складе пара warehouse→warehouse дополняет order_no→order_no; потеря одной пары меняет JOIN. |
| KEY: FK_NAME, PK_NAME, KEY_SEQ | Имена внешнего/первичного ограничения и позиция компонента. Сначала группируют по ключу, затем упорядочивают его компоненты по KEY_SEQ, если описывается несколько независимых связей. |
ATTR_ORDER, KEY_ORDER и KEY_SEQ описывают позиции с 1. Индексы внешнего PHP-массива обычно начинаются с 0 — это другой счётчик. В строке официальной таблицы, где «base:1» поставлено рядом с FKTABLE_NAME, речь не может идти о нумерации имени таблицы; для порядка компонентов используется KEY_SEQ. Значения UPDATE_ACTION/DELETE_ACTION в базовом формате: 0 cascade, 1 restrict, 2 no action, 3 set null. Фактические имена колонок разных серверов могут отличаться, например NON_NULL вместо NOT NULL и UPDATE_RULE/DELETE_RULE вместо *_ACTION: проект сохраняет полученную форму и нормализует только явно известный смысл.
Метаданные не заменяют разрешения, проверку входных значений и миграционный план. Описание UNIQUE не объясняет все бизнес-правила; PRIMARY_KEY/FK читаются своими типами. Не выполняйте полученные DEFAULT, QUERY_SPEC или ACTION как доверенный PHP/SQL. Эти поля предназначены для просмотра и анализа. Дополнительная схема ATTR_WITH_SYNONYM присутствует в закреплённой реализации, но не входит в список 19 типов официальной страницы и не подменяет здесь их разбор.
Что реально вернул закреплённый PDO-драйвер
Одна подготовка на PDO_CUBRID из тега v11.2.0.0002 и CUBRID 11.3.5 проверила все 19 форм. Это отдельные наблюдения PDO, не перенос результатов native cubrid_schema. PHP-метаданные модуля сообщают 1.0.0, поэтому происхождение сборки фиксируется также точным тегом/коммитом; одно это число не следует считать номером установленного пакета.
| Базовое описание | Фактическая форма и правило чтения |
|---|---|
| NOT NULL у атрибутов | Ответ содержит NON_NULL; DEFAULT отсутствующего значения выглядит строкой «NULL». SQL NULL в данных строки при этом остаётся PHP null. Поле REMARKS — дополнительная колонка описания. |
| DIRECT_SUPER_TABLE | Вместо краткого формата CLASS_NAME/TYPE получены CLASS_NAME ребёнка и SUPER_CLASS_NAME непосредственного родителя. SUPER_TABLE и SUB_TABLE сохранили CLASS_NAME/TYPE. |
| UPDATE_ACTION / DELETE_ACTION | Получены UPDATE_RULE / DELETE_RULE. В нашем FK оба равны строке «1» — restrict. KEY_SEQ тоже строка: перед числовым сравнением её явно преобразуют. |
| CONSTRAINT | В подготовленной таблице UNIQUE попал в этот ответ; PK и FK читаются своими типами. Добавлено ASC_DESC=«A». Отсутствие PK в этом массиве не означает отсутствие ключа. |
| TRIGGER | STATUS=ACTIVE, EVENT=UPDATE, ACTION_TIME=AFTER и CONDITION_TIME=AFTER — строки. ACTION и CONDITION — SQL-представление сервера; PRIORITY тоже приходит строкой. |
| Пропущенное имя, NULL и пустая строка | VIEW без имени вернул системные представления вместе с собственным; явный NULL при strict_types вызвал TypeError, пустая строка дала []. Это три различных запроса. |
| Несуществующее имя и неизвестный тип | Отсутствующая таблица в подходящем запросе дала []; тип 9999 дал false с HY000 без PDOException даже при выбранном режиме исключений. Поэтому метод проверяется явно. |
Отказ курсора или SQL может сохраниться в драйверных элементах errorInfo после последующего успешного вызова: в подготовке SQLSTATE уже был 00000, а старые код/текст ещё присутствовали. Сначала учитывайте результат конкретной операции, исключение и текущий SQLSTATE; не объявляйте каждую успешную операцию ошибочной только из-за старого errorInfo[1]. Код PDO_CUBRID может отображаться как большое беззнаковое число; не сравнивайте его с отрицательным кодом native API без явного преобразования и проверки версии. Полные проекты сохраняют только безопасные признаки диагностики, без серверных путей и текста реквизитов.
Составить порядок импорта и соединить заказ по полному ключу
Импорт получает заказ № 501 сразу с двух складов. Номер без склада недостаточен: иначе строки могут прицепиться к чужому заказу. Программа читает первичный и внешний ключи, проверяет ожидаемое соответствие, записывает родителей раньше детей и создаёт import-plan.csv. Затем ограниченный SQL JOIN возвращает только заказ склада 1 с двумя товарами и суммой 38150 копеек.
- orders: (1,501) Лаборатория; (2,501) Мастерская
- PRIMARY_KEY → [warehouse, order_no]; IMPORTED_KEYS → те же две пары
- Транзакция: сначала 2 заказа → затем 3 строки → commit
- WHERE warehouse=1 AND order_no=501; JOIN по обоим полям → Кабель/Хомут, 38150 коп.
Минимальный вызов
$keys = $db->cubrid_schema(PDO::CUBRID_SCH_IMPORTED_KEYS, $lineTableName);
if ($keys === false) { throw new RuntimeException('Связи не прочитаны'); }
foreach ($keys as $key) {
echo $key['KEY_SEQ'], ': ', $key['FKCOLUMN_NAME'], ' -> ', $key['PKCOLUMN_NAME'], '\n';
}IMPORTED_KEYS рассматривает выбранную таблицу как ребёнка. Для order_lines ответ описывает warehouse → warehouse и order_no → order_no. KEY_SEQ — номер компонента от 1; он нужен, когда внешний ключ составной. В полном проекте небольшое описание упорядочено по этому номеру и сравнивается с разрешённым договором импорта.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Порядок импорта | orders → order_lines; затем чтение order_view |
| Ключ заказа | warehouse + order_no |
| Связь строки с заказом | warehouse + order_no → warehouse + order_no |
| Заказ склада 1 | 501 / Лаборатория |
| Кабель | 3 × 12550 = 37650 коп. |
| Хомут | 4 × 125 = 500 коп. |
| Итог | 2 строки; 38150 коп. |
| UNIQUE / INDEX | sku / qty; отдельно от PK и FK |
| Файл | import-plan.csv |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);
function schemaRows175(PDO $db,int $type,string $name,?string $second=null): array {
$call=p175Capture(static function()use($db,$type,$name,$second){
return $second===null?$db->cubrid_schema($type,$name):$db->cubrid_schema($type,$name,$second);
},$db);
if ($call['exception'] || $call['warning'] || $call['value']===false || !is_array($call['value'])) {p175Failure('Описание схемы недоступно',$call);}
return $call['value'];
}
function csv175(string $filename,array $records): void {
$fp=fopen(__DIR__.'/'.$filename,'wb');
if (!is_resource($fp)) {throw new RuntimeException('CSV не открыт');}
try {foreach($records as $row){if(fputcsv($fp,$row,';','"','')===false){throw new RuntimeException('Строка CSV не записана');}}}
finally {fclose($fp);}
}
function baseName175(string $value): string {
$parts=explode('.',$value);return end($parts);
}
function orderedColumns175(array $rows,string $column,string $position): array {
usort($rows,static function($a,$b)use($position){return (int)$a[$position]<=>(int)$b[$position];});
return array_column($rows,$column);
}
$ordersName=p175OwnName($db,$owned,'TABLE','orders');$orders=p175Qualified($ordersName);
p175Ddl($db,'CREATE TABLE '.$orders.' (warehouse INT NOT NULL,order_no INT NOT NULL,customer VARCHAR(80),PRIMARY KEY(warehouse,order_no))');
$linesName=p175OwnName($db,$owned,'TABLE','lines');$lines=p175Qualified($linesName);
p175Ddl($db,'CREATE TABLE '.$lines.' (warehouse INT NOT NULL,order_no INT NOT NULL,line_no INT NOT NULL,sku VARCHAR(24) UNIQUE,title VARCHAR(80),qty INT,price_cents INT,PRIMARY KEY(warehouse,order_no,line_no),FOREIGN KEY(warehouse,order_no) REFERENCES '.$orders.' (warehouse,order_no) ON DELETE RESTRICT)');
// Имя индекса составлено только из уже проверенного собственного имени таблицы.
p175Ddl($db,'CREATE INDEX ['.$linesName.'_qty_ix] ON '.$lines.' (qty)');
$viewName=p175OwnName($db,$owned,'VIEW','order_view');$view=p175Qualified($viewName);
$joined='SELECT o.warehouse,o.order_no,o.customer,l.line_no,l.title,l.qty,l.price_cents FROM '.$orders.' o JOIN '.$lines.' l ON l.warehouse=o.warehouse AND l.order_no=o.order_no';
p175Ddl($db,'CREATE VIEW '.$view.' AS '.$joined);
$pk=schemaRows175($db,PDO::CUBRID_SCH_PRIMARY_KEY,$ordersName);
$linePk=schemaRows175($db,PDO::CUBRID_SCH_PRIMARY_KEY,$linesName);
$fk=schemaRows175($db,PDO::CUBRID_SCH_IMPORTED_KEYS,$linesName);
$exported=schemaRows175($db,PDO::CUBRID_SCH_EXPORTED_KEYS,$ordersName);
$cross=schemaRows175($db,PDO::CUBRID_SCH_CROSS_REFERENCE,$ordersName,$linesName);
$attributes=schemaRows175($db,PDO::CUBRID_SCH_ATTRIBUTE,$linesName);
$constraints=schemaRows175($db,PDO::CUBRID_SCH_CONSTRAINT,$linesName);
$tables=schemaRows175($db,PDO::CUBRID_SCH_TABLE,$ordersName);
$views=schemaRows175($db,PDO::CUBRID_SCH_VIEW,$viewName);
$viewSql=schemaRows175($db,PDO::CUBRID_SCH_QUERY_SPEC,$viewName);
$parentColumns=orderedColumns175($pk,'ATTR_NAME','KEY_SEQ');
$childColumns=orderedColumns175($linePk,'ATTR_NAME','KEY_SEQ');
$fkChild=orderedColumns175($fk,'FKCOLUMN_NAME','KEY_SEQ');
$fkParent=orderedColumns175($fk,'PKCOLUMN_NAME','KEY_SEQ');
if($parentColumns!==['warehouse','order_no'] || $childColumns!==['warehouse','order_no','line_no'] || $fkChild!==['warehouse','order_no'] || $fkParent!==['warehouse','order_no']){
throw new RuntimeException('Схема не соответствует ожидаемому договору импорта');
}
p175Begin($db);
try {
foreach([[1,501,'Лаборатория'],[2,501,'Мастерская']] as $record){p175Command($db,'INSERT INTO '.$orders.' (warehouse,order_no,customer) VALUES (?,?,?)',$record);}
foreach([[1,501,1,'CAB-1','Кабель',3,12550],[1,501,2,'CLP-1','Хомут',4,125],[2,501,1,'SOC-2','Розетка',2,25000]] as $record){
p175Command($db,'INSERT INTO '.$lines.' (warehouse,order_no,line_no,sku,title,qty,price_cents) VALUES (?,?,?,?,?,?,?)',$record);
}
p175Commit($db);
} catch(Throwable $failure){p175Rollback($db);throw $failure;}
$actual=p175Read($db,'SELECT customer,line_no,title,qty,price_cents,qty*price_cents AS line_total FROM '.$view.' WHERE warehouse=? AND order_no=? ORDER BY line_no LIMIT 10',[1,501]);
$total=p175Read($db,'SELECT COUNT(*) AS positions,SUM(qty*price_cents) AS total_cents FROM '.$lines.' WHERE warehouse=? AND order_no=?',[1,501]);
$summary=[];foreach($actual as $row){$summary[]=['customer'=>$row['customer'],'line_no'=>(int)$row['line_no'],'title'=>$row['title'],'qty'=>(int)$row['qty'],'line_total'=>(int)$row['line_total']];}
$plan=[['step','target','key','depends_on','example'],['1','orders','warehouse,order_no','-','1/501: Лаборатория'],
['2','order_lines','warehouse,order_no,line_no','orders(warehouse,order_no)','1/501/1: Кабель, 3 шт.'],
['3','order_view','warehouse+order_no JOIN','orders + order_lines','Склад 1: 2 строки, 38150 коп.']];
csv175('import-plan.csv',$plan);
$hasUnique=false;$hasIndex=false;
foreach($constraints as $constraint){
if((int)$constraint['TYPE']===0&&$constraint['ATTR_NAME']==='sku'){$hasUnique=true;}
if((int)$constraint['TYPE']===1&&$constraint['ATTR_NAME']==='qty'){$hasIndex=true;}
}
$rows=[['Порядок импорта','orders → order_lines; затем чтение order_view'],['Ключ заказа',implode(' + ',$parentColumns)],
['Связь строки с заказом',implode(' + ',$fkChild).' → '.implode(' + ',$fkParent)],
['Заказ склада 1','501 / Лаборатория'],['Кабель','3 × 12550 = 37650 коп.'],['Хомут','4 × 125 = 500 коп.'],
['Итог','2 строки; 38150 коп.'],['UNIQUE / INDEX','sku / qty; отдельно от PK и FK'],['Файл','import-plan.csv']];
$checks=['parent_key'=>$parentColumns===['warehouse','order_no'],'child_key'=>$childColumns===['warehouse','order_no','line_no'],
'fk_pairs'=>$fkChild===['warehouse','order_no']&&$fkParent===['warehouse','order_no'],
'directions_agree'=>count($fk)===2&&count($exported)===2&&count($cross)===2,
'attributes_named'=>count($attributes)===7&&in_array('sku',array_column($attributes,'ATTR_NAME'),true),
'table_found'=>count($tables)===1&&baseName175($tables[0]['NAME'])===$ordersName,
'view_found'=>count($views)===1&&baseName175($views[0]['NAME'])===$viewName,
'view_has_sql'=>count($viewSql)===1&&is_string($viewSql[0]['QUERY_SPEC'])&&strpos($viewSql[0]['QUERY_SPEC'],$ordersName)!==false,
'unique_and_index'=>$hasUnique&&$hasIndex,
'join_exact'=>$summary===[['customer'=>'Лаборатория','line_no'=>1,'title'=>'Кабель','qty'=>3,'line_total'=>37650],['customer'=>'Лаборатория','line_no'=>2,'title'=>'Хомут','qty'=>4,'line_total'=>500]],
'sql_total'=>(int)$total[0]['positions']===2&&(int)$total[0]['total_cents']===38150,
'other_warehouse_excluded'=>!in_array('Розетка',array_column($summary,'title'),true),'plan_rows'=>count($plan)===4];
$observations=['parent_key_columns'=>$parentColumns,'child_key_columns'=>$childColumns,'foreign_child_columns'=>$fkChild,'foreign_parent_columns'=>$fkParent,
'schema_forms'=>['primary'=>$pk,'imported'=>$fk,'exported'=>$exported,'cross'=>$cross,'constraints'=>$constraints,'attributes'=>$attributes,'view'=>$viewSql],
'selected_order'=>$summary,'sql_total'=>$total,'import_plan'=>$plan,'metadata_used_for_destructive_changes'=>false];
} finally {
if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}
p175Report('pdoc175-import-schema','Составить порядок импорта и соединить заказ по полному ключу',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Порядок импорта — orders → order_lines; затем чтение order_view
Ключ заказа — warehouse + order_no
Связь строки с заказом — warehouse + order_no → warehouse + order_no
Заказ склада 1 — 501 / Лаборатория
Кабель — 3 × 12550 = 37650 коп.
Хомут — 4 × 125 = 500 коп.
Итог — 2 строки; 38150 коп.
UNIQUE / INDEX — sku / qty; отдельно от PK и FK
Файл — import-plan.csv
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать план импорта CSV
Границы решения
Граф импорта здесь содержит две известные таблицы, поэтому не нужен универсальный сортировщик любой схемы. Для циклических ссылок, больших файлов и нескольких транзакций нужен отдельный план. Метаданные не дают разрешения менять найденные объекты: программа создаёт и удаляет только зарегистрированные собственные таблицы и view. UNIQUE sku и индекс qty описываются отдельно от PK/FK; это не измерение быстродействия индекса. GROUP BY/COUNT и сортировка прикладных строк остаются в SQL, в PHP сортируется только описание составного ключа.
Попробуйте изменить условие
В orders уже есть (1,501) и (2,501). Почему JOIN только по order_no вернёт лишние строки? Какие поля нужно добавить в ON?
Разбор
Обе родительские строки имеют номер 501. Соединяйте l.warehouse=o.warehouse AND l.order_no=o.order_no и дополнительно задавайте нужный заказ в WHERE. Наличие FK не дописывает ON автоматически. Для трёхчастного ключа анализируют все три пары KEY_SEQ, а не только первый элемент.
Паспорт складского типа: происхождение полей и реально сработавший триггер
При сопровождении склада непонятно, откуда у позиции появились id и title и почему изменение остатка пишет журнал. Программа создаёт небольшой тип данных с родительской схемой, читает происхождение полей и триггер, затем меняет количество с 3 на 7. Файл schema-audit.json связывает паспорт с конкретным товаром и реально появившейся записью журнала.
- base_item: id/title + классовая policy=review
- stock_item UNDER base_item: qty/zone_label; Кабель O’Reilly в A-2, qty3
- UPDATE qty7 → AFTER UPDATE с условием old.qty<>obj.qty
- Журнал: item1, old3, new7; SQL-readback + паспорт schema-audit.json
Минимальный вызов
$attributes = $db->cubrid_schema(PDO::CUBRID_SCH_ATTRIBUTE, $stockTableName);
if ($attributes === false) { throw new RuntimeException('Атрибуты не прочитаны'); }
foreach ($attributes as $attribute) {
echo $attribute['ATTR_NAME'], ' <- ', $attribute['SOURCE_CLASS'], '\n';
}SOURCE_CLASS — источник определения атрибута в схеме CUBRID. Для id и title это родитель base_item, для qty и zone_label — stock_item. PHP-класс PDO при этом не получает новых свойств и методов. Отдельные TABLE_ATTRIBUTE и TABLE_METHOD относятся к классу базы, а не к каждой товарной строке.
Что получится после запуска
| Наблюдение | Результат |
|---|---|
| Позиция | 1 / Кабель O'Reilly / A-2 |
| Происхождение полей | id, title ← base_item; qty, zone_label ← stock_item |
| Атрибут класса | policy: DEFAULT 'review' |
| Остаток | 3 → 7 |
| Журнал | 1 запись: item_id=1, old_qty=3, new_qty=7 |
| Внешние методы | 0 методов экземпляра; 0 методов класса; 0 файлов |
| Права | Прочитано описание текущего DBA; права других пользователей не доказаны |
| Неизвестный вид схемы | false + HY000; отсутствие методов отдельно даёт [] |
| Файл | schema-audit.json |
Самостоятельный проект, исходник и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=p175Config();$db=p175Connect();$runtime=p175Runtime($db);
function schemaRows175(PDO $db,int $type,string $name,?string $second=null): array {
$call=p175Capture(static function()use($db,$type,$name,$second){
return $second===null?$db->cubrid_schema($type,$name):$db->cubrid_schema($type,$name,$second);
},$db);
if ($call['exception'] || $call['warning'] || $call['value']===false || !is_array($call['value'])) {p175Failure('Описание схемы недоступно',$call);}
return $call['value'];
}
function csv175(string $filename,array $records): void {
$fp=fopen(__DIR__.'/'.$filename,'wb');
if (!is_resource($fp)) {throw new RuntimeException('CSV не открыт');}
try {foreach($records as $row){if(fputcsv($fp,$row,';','"','')===false){throw new RuntimeException('Строка CSV не записана');}}}
finally {fclose($fp);}
}
function baseName175(string $value): string {
$parts=explode('.',$value);return end($parts);
}
function orderedColumns175(array $rows,string $column,string $position): array {
usort($rows,static function($a,$b)use($position){return (int)$a[$position]<=>(int)$b[$position];});
return array_column($rows,$column);
}
$baseName=p175OwnName($db,$owned,'TABLE','base_item');$base=p175Qualified($baseName);
p175Ddl($db,'CREATE TABLE '.$base.' CLASS ATTRIBUTE (policy VARCHAR(20) DEFAULT \'review\') (id INT,title VARCHAR(80)) DONT_REUSE_OID');
$childName=p175OwnName($db,$owned,'TABLE','stock_item');$child=p175Qualified($childName);
p175Ddl($db,'CREATE TABLE '.$child.' UNDER '.$base.' (qty INT,zone_label VARCHAR(20)) DONT_REUSE_OID');
$auditName=p175OwnName($db,$owned,'TABLE','qty_audit');$audit=p175Qualified($auditName);
p175Ddl($db,'CREATE TABLE '.$audit.' (item_id INT,old_qty INT,new_qty INT)');
$triggerName=p175OwnName($db,$owned,'TRIGGER','qty_change');$trigger=p175Qualified($triggerName);
p175Ddl($db,'CREATE TRIGGER '.$trigger.' AFTER UPDATE ON '.$child.'(qty) IF old.qty<>obj.qty EXECUTE INSERT INTO '.$audit.' (item_id,old_qty,new_qty) VALUES (obj.id,old.qty,obj.qty)');
p175Command($db,'INSERT INTO '.$child.' (id,title,qty,zone_label) VALUES (?,?,?,?)',[1,"Кабель O'Reilly",3,'A-2']);
$attributes=schemaRows175($db,PDO::CUBRID_SCH_ATTRIBUTE,$childName);
$classAttributes=schemaRows175($db,PDO::CUBRID_SCH_TABLE_ATTRIBUTE,$baseName);
$super=schemaRows175($db,PDO::CUBRID_SCH_SUPER_TABLE,$childName);
$direct=schemaRows175($db,PDO::CUBRID_SCH_DIRECT_SUPER_TABLE,$childName);
$sub=schemaRows175($db,PDO::CUBRID_SCH_SUB_TABLE,$baseName);
$methods=schemaRows175($db,PDO::CUBRID_SCH_METHOD,$childName);
$classMethods=schemaRows175($db,PDO::CUBRID_SCH_TABLE_METHOD,$childName);
$methodFiles=schemaRows175($db,PDO::CUBRID_SCH_METHOD_FILE,$childName);
$triggers=schemaRows175($db,PDO::CUBRID_SCH_TRIGGER,'dba.'.$childName);
$tableRights=schemaRows175($db,PDO::CUBRID_SCH_TABLE_PRIVILEGE,$childName);
$columnRights=schemaRows175($db,PDO::CUBRID_SCH_COL_PRIVILEGE,$childName,'qty');
$invalid=p175Capture(static function()use($db,$childName){return $db->cubrid_schema(9999,$childName);},$db);
if($invalid['value']!==false || $invalid['sqlstate']!=='HY000') {throw new RuntimeException('Неизвестный вид схемы не отклонён');}
$origin=[];foreach($attributes as $attribute){$origin[$attribute['ATTR_NAME']]=baseName175((string)$attribute['SOURCE_CLASS']);}
p175Begin($db);
try {p175Command($db,'UPDATE '.$child.' SET qty=? WHERE id=?',[7,1]);p175Commit($db);}
catch(Throwable $failure){p175Rollback($db);throw $failure;}
$item=p175Read($db,'SELECT id,title,qty,zone_label FROM '.$child.' WHERE id=? LIMIT 1',[1]);
$changes=p175Read($db,'SELECT item_id,old_qty,new_qty FROM '.$audit.' WHERE item_id=? LIMIT 10',[1]);
$passport=['item'=>['id'=>(int)$item[0]['id'],'title'=>$item[0]['title'],'zone'=>$item[0]['zone_label'],'qty'=>(int)$item[0]['qty']],
'change'=>['old_qty'=>(int)$changes[0]['old_qty'],'new_qty'=>(int)$changes[0]['new_qty'],'audit_rows'=>count($changes)],
'schema'=>['direct_parent'=>'base_item','child'=>'stock_item','inherited'=>['id','title'],'local'=>['qty','zone_label'],'class_attribute'=>'policy',
'methods'=>count($methods),'class_methods'=>count($classMethods),'method_files'=>count($methodFiles),'trigger'=>'qty_change',
'column_policy'=>'qty update is observed; application permissions are separate'],
'limits'=>['database_inheritance_is_php_inheritance'=>false,'metadata_changes_schema'=>false,'admin_privileges_prove_other_user_access'=>false]];
if(file_put_contents(__DIR__.'/schema-audit.json',json_encode($passport,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_THROW_ON_ERROR)."\n")===false){throw new RuntimeException('Паспорт не записан');}
$rows=[['Позиция',"1 / Кабель O'Reilly / A-2"],['Происхождение полей','id, title ← base_item; qty, zone_label ← stock_item'],
['Атрибут класса',"policy: DEFAULT 'review'"],['Остаток','3 → 7'],['Журнал','1 запись: item_id=1, old_qty=3, new_qty=7'],
['Внешние методы','0 методов экземпляра; 0 методов класса; 0 файлов'],['Права','Прочитано описание текущего DBA; права других пользователей не доказаны'],
['Неизвестный вид схемы','false + HY000; отсутствие методов отдельно даёт []'],['Файл','schema-audit.json']];
$checks=['actual_item'=>count($item)===1&&$item[0]['title']==="Кабель O'Reilly"&&(int)$item[0]['qty']===7,
'audit_written'=>count($changes)===1&&(int)$changes[0]['item_id']===1&&(int)$changes[0]['old_qty']===3&&(int)$changes[0]['new_qty']===7,
'inherited_fields'=>($origin['id']??null)===$baseName&&($origin['title']??null)===$baseName,
'local_fields'=>($origin['qty']??null)===$childName&&($origin['zone_label']??null)===$childName,
'class_policy'=>count($classAttributes)===1&&$classAttributes[0]['ATTR_NAME']==='policy'&&$classAttributes[0]['DEFAULT']==="'review'",
'parent_routes'=>count($super)===1&&count($direct)===1&&baseName175($super[0]['CLASS_NAME'])===$baseName&&baseName175($direct[0]['SUPER_CLASS_NAME'])===$baseName,
'child_route'=>count($sub)===1&&baseName175($sub[0]['CLASS_NAME'])===$childName,
'methods_empty'=>$methods===[]&&$classMethods===[]&&$methodFiles===[],
'trigger_present'=>count($triggers)===1&&baseName175($triggers[0]['NAME'])===$triggerName,
'privilege_shapes'=>in_array('UPDATE',array_column($tableRights,'PRIVILEGE'),true)&&in_array('UPDATE',array_column($columnRights,'PRIVILEGE'),true),
'invalid_not_empty'=>$invalid['value']===false&&$invalid['sqlstate']==='HY000'&&$invalid['message_present'],
'artifact_item'=>$passport['item']['qty']===7&&$passport['change']['audit_rows']===1];
$observations=['passport'=>$passport,'attribute_metadata'=>$attributes,'class_attribute_metadata'=>$classAttributes,
'hierarchy'=>['all_parents'=>$super,'direct_parents'=>$direct,'children'=>$sub],'triggers'=>$triggers,
'current_connection_privileges'=>['table'=>$tableRights,'qty'=>$columnRights],
'actual_audit_rows'=>$changes,'external_methods'=>['instance'=>$methods,'class'=>$classMethods,'files'=>$methodFiles],
'invalid_schema'=>array_diff_key($invalid,['value'=>true])];
} finally {
if ($db instanceof PDO) {try {$removed=p175Cleanup($db,$owned);} finally {p175Close($db);}}
}
p175Report('pdoc175-schema-audit','Паспорт складского типа: происхождение полей и реально сработавший триггер',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Позиция — 1 / Кабель O'Reilly / A-2
Происхождение полей — id, title ← base_item; qty, zone_label ← stock_item
Атрибут класса — policy: DEFAULT 'review'
Остаток — 3 → 7
Журнал — 1 запись: item_id=1, old_qty=3, new_qty=7
Внешние методы — 0 методов экземпляра; 0 методов класса; 0 файлов
Права — Прочитано описание текущего DBA; права других пользователей не доказаны
Неизвестный вид схемы — false + HY000; отсутствие методов отдельно даёт []
Файл — schema-audit.json
Распакуйте весь ZIP и прочитайте README.txt. example.php содержит прикладную логику, lab.php — работу с отдельной базой и собственным реестром объектов. После выполнения откройте result.html; JSON и полезный файл доступны ниже.
Открыть результат выполнения · Посмотреть данные JSON · Скачать паспорт схемы JSON
Границы решения
В этом примере наследование нужно объяснить существующий объектный механизм CUBRID; оно не объявляется обязательной моделью склада. Часто достаточно обычных таблиц с FK. Пустые методы — настоящий ответ об отсутствии внешних зависимостей, а не заглушка. Паспорт не даёт автоматического разрешения удалять триггер или менять схему. Описание прав текущего DBA не заменяет тест отдельной учётной записи приложения; новый USER ради формального вызова API не создаётся.
Попробуйте изменить условие
Повторно установили qty=7. Должна ли появиться новая запись в журнале? Как отделить проверку определения триггера от проверки данных?
Разбор
Условие old.qty<>obj.qty ложно для 7→7, поэтому этот триггер не должен добавлять запись. Сначала смотрят CONDITION/ACTION в описании, затем отдельно проверяют COUNT и строки журнала. Метаданные помогают понять механизм, но итог выполнения доказывают реальные данные. В новом варианте упражнения нужна отдельная проверка после запуска.
Применить описание по назначению и развить проект
Описание выбранной колонки помогает подписать экран; описание ключей — согласовать импорт; паспорт триггера — понять причину изменения. Сравнивайте метаданные с реальными данными своей задачи. Не превращайте один флаг в универсальный генератор интерфейса или разрешение на изменение любой найденной таблицы.
Имя результата и исходное поле. Вопрос об alias и исходном имени показывает, почему подпись экрана не следует считать разрешением на редактирование произвольного столбца. Мы используем явный SELECT и отдельную schema-информацию, а не пытаемся разобрать любой SQL регулярным выражением.
Переход к нужной строке результата. Запрос на аналог mysql_data_seek не доказывает поддержку scroll каждым PDO-драйвером. Здесь явно заказывается CURSOR_SCROLL и проверяются конкретные направления PDO_CUBRID; весь каталог ради этого не выгружается.
Составная связь и ошибочное удвоение JOIN. Вопрос связывает повторяющийся номер родителя и лишнюю строку JOIN. Наш заказ 501 тоже существует на двух складах: правильное соединение использует warehouse вместе с order_no. Код другого движка и ошибочные варианты из ответов не копируются.
Вернитесь к подключению, транзакциям и нескольким результатам PDO_CUBRID для границ операции. Следующий связанный раздел — вложения и состав заказа через PDO. Общая карточка PDO::cubrid_schema сохраняет постоянный адрес, а эта глава раскрывает её на самостоятельных проектах. Ранее созданные контекстные примеры не получают отметку реального запуска автоматически: здесь отдельно проверяется новый код с собственными фикстурами.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


