Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 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 в таблице относятся к одному чтению, но имеют разные задачи.

Один каталог — два разных описания
  1. Таблица goods: id, title, price, qty, note
  2. SELECT id AS item_id, title AS item_title, price, qty, note ORDER BY id LIMIT 3
  3. getColumnMeta(1): подпись item_title и сведения выбранной колонки
  4. cubrid_schema(ATTRIBUTE, goods): описание атрибута title таблицы
  5. 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-курсор позволяет вернуться к нужной строке этого списка.

Путь данных и полученный результат
  1. SQL ORDER BY id LIMIT 3 → ID 1, 2, 3
  2. getColumnMeta(1) → item_title; цена DECIMAL(10,2)
  3. FIRST 1 → LAST 3 → ABS 2 → REL -1 возвращает 1
  4. 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 -11 → 3 → 2 → 1
Возврат в forward-onlyОтклонён; открыт отдельный scroll-запрос
CSVpreview.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.

Как читается составной ключ
  1. cubrid_schema(PRIMARY_KEY, orders) → строки метаданных
  2. ATTR_NAME=warehouse, KEY_SEQ=1; ATTR_NAME=order_no, KEY_SEQ=2
  3. Небольшой массив описания сортируем по KEY_SEQ → [warehouse, order_no]
  4. Сначала импортируем orders; затем order_lines с обоими значениями
  5. 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, TYPETYPE: 0 — системная таблица, 1 — представление, 2 — обычная таблица. Имя выбирает набор объектов; режим шаблона отличается от точного поиска ключей.
VIEWТолько представленияNAME, TYPETYPE=1. Представление сохраняет запрос, а не отдельную копию всех его строк.
QUERY_SPECSQL определения представленияQUERY_SPECЭтот текст нужен для ревизии зависимостей. Не выполняйте полученную строку автоматически как миграцию.
ATTRIBUTEАтрибуты экземпляров строкиATTR_NAME, DOMAIN, SCALE, PRECISION, INDEXED, NOT NULL, SHARED, UNIQUE, DEFAULT, ATTR_ORDER, CLASS_NAME, SOURCE_CLASS, IS_KEYATTR_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Методы экземпляров CUBRIDNAME, RET_DOMAIN, ARG_DOMAINОписание внешних методов базы; не методы PDO или PHP-класса. Отсутствие записей может быть нормальным результатом.
TABLE_METHODМетоды класса CUBRIDNAME, 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, GRANTABLEGRANTABLE относится к передаче привилегии. Ответ в административном соединении не доказывает успешный вход и полномочия другого пользователя.
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_CLASSATTR_NAME — имя поля. CLASS_NAME относится к описываемому классу таблицы; SOURCE_CLASS показывает место объявления. Для дочернего stock_item атрибут title приходит из base_item, а qty определён локально.
ATTRIBUTE: DOMAIN, PRECISION, SCALEDOMAIN описывает SQL-домен/тип в формате сервера. PRECISION и SCALE уточняют размер/точность. Это не текущая длина строки «Кабель» и не число строк результата; значение типа следует читать вместе с версией CCI.
ATTRIBUTE: INDEXED, NOT NULL, SHARED, UNIQUE, IS_KEYВ базовом формате 1 включает соответствующий признак: индексированность, запрет NULL, общий атрибут, уникальность, ключ. Признаки не раскрывают полный состав PK/FK и не заменяют специальные запросы ключей. SHARED в описании не следует путать с массивом PHP, общим между функциями.
ATTRIBUTE: DEFAULT, ATTR_ORDERDEFAULT — серверное описание значения по умолчанию. ATTR_ORDER нумерует атрибуты с 1; это не индекс PHP-массива. Для фиксированного импорта приложение сопоставляет имена и допустимые типы, а не вслепую вставляет столбцы в полученном порядке.
CONSTRAINT: TYPE, NAME, ATTR_NAME, PRIMARY_KEY, KEY_ORDERTYPE определяет вид уникальности/индекса; 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 в этом массиве не означает отсутствие ключа.
TRIGGERSTATUS=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 копеек.

Путь данных и полученный результат
  1. orders: (1,501) Лаборатория; (2,501) Мастерская
  2. PRIMARY_KEY → [warehouse, order_no]; IMPORTED_KEYS → те же две пары
  3. Транзакция: сначала 2 заказа → затем 3 строки → commit
  4. 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
Заказ склада 1501 / Лаборатория
Кабель3 × 12550 = 37650 коп.
Хомут4 × 125 = 500 коп.
Итог2 строки; 38150 коп.
UNIQUE / INDEXsku / 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 связывает паспорт с конкретным товаром и реально появившейся записью журнала.

Путь данных и полученный результат
  1. base_item: id/title + классовая policy=review
  2. stock_item UNDER base_item: qty/zone_label; Кабель O’Reilly в A-2, qty3
  3. UPDATE qty7 → AFTER UPDATE с условием old.qty<>obj.qty
  4. Журнал: 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.