• Python в Windows 10

    Виндуз 10 — мягко говоря, странная вещь. Во всех операционках Питон идет из коробки, а на Винде его нет. Вместо него — заглушка. Если набрать в консоли python foo.py, появится надпись: друг, у тебя нет Питона. Чтобы поставить его, просто введи python.

    Хорошо, ввожу python. Открывается магазин приложений и тут же выстреливает алертами: и какая-то ошибка, и регион не тот, и токен протух. Еле закрыл к чертям, а то и дальше бы кидался ошибками.

    Скачал Питон с официального сайта, поставил. Ввожу python foo.py — опять та же самая заглушка. Минуточку, где Питон, который я поставил минуту назад? Начинаю искать. В дни моей молодости он ставился в корень: C:\Python27. Там его нет. Ищу в C:\Program Files (обоих папках) — тоже нет. Наконец догадался: проверил ярлык в меню Пуск. Знаете куда он ведет? Вот:

    C:\Users\ivan\AppData\Local\Programs\Python\Python3.8\...
    

    Удивляюсь, какими же уродами надо быть, чтобы так сделать? Мало того что засунули его глубже некуда, так еще не сменили заглушку для магазина!

    Ладно, может быть заглушку нельзя трогать по условиям лицензии. Тогда почему инсталятор не добавил путь к Питону в PATH?

    Ничего не работает как надо. Какое-то сборище клоунов, честное слово.

  • Непонятный код

    Одна из самых бесячих вещей — когда кто-то вбрасывает непонятный код и спрашивает, что он выведет. Откуда я знаю, что он выведет? Запусти и посмотри. Еще лучше исправить код, чтобы он был понятен сразу.

    Я еще могу понять, если это какой-нибудь сишный код, оптимизированный по самые помидоры, чтобы умещаться в кэше процессора. Это чужой монастырь, и там свои погремушки. Но когда это обычный бизнес-код типа “принять две мапы и взять наибольший ключ”, оправданий быть не может.

    Почему-то мало кто понимает: писать непонятный код легко, а понятный — трудно. На это уходят годы практики, нужно пройти много ситуаций, чтобы понять, какой код и при каких обстоятельсвах понятен. Вываливать непонятный код — то же самое, что мочиться в штаны. Нужно тихо решить проблему, а не тыкать ей в лицо.

    Вообще, угадывание — худшее, что есть в айти. Когда угадываешь, фактически ты на дне. Ты исчерпал все нормальные варианты, ну или их не было. Должна быть легкая возможность запустить код — привет, программисты без репла. Должны быть тесты, линтеры, логи, метрики, бенчмарки, сбор ошибок и так далее.

    Угадайка — это билет в проигрыш, иначе быть не может. Да, бывают истории, когда человек раз — и решил сложную проблему, не имея никаких улик. Но это ошибка выжившего: на каждую такую историю приходится тысяча других, когда человек просидел выходные, решая проблему брутфорсом — и на двадцать пятой итерации что-то помогло. Я бы не рассчитывал на такую удачу.

    Короче, видишь плохой код — исправь, не гадай.

  • Без исключений

    Меня искренне изумляют ребята, которые обходят исключения стороной. Типа, вернем null и запишем в лог. Или вернем мапу {"success": false}. Или кортеж (nil, "error"). Или еще какой-то финт ушами.

    Мне интересно: а кто будет читать логи? Тот чел, который молча пишет в лог, у него что, в договоре прописано каждое утро их читать? С какой частотой? И что делать, если логи нашлись? Как реагировать? И кто следит, чтобы он их читал?

    Или какой-то калека вернул мапу {"success": false}. Что с ней делать? Какая была ошибка? В каком направлении двигаться? Просто неопытный разработчик переложил на других то, что должен делать сам.

    Замалчивание ошибок — признак неопытности программиста. В единичном случае это еще не страшно. Но когда каждая строчка возвращает null или мапу вместо ошибки, работать невозможно. Приходиться писать строгие обертки вокруг таких функций, которые кидают исключение с понятным сообщением: что я собирался делать и что пошло не так.

    Один коллега предложил: давай я буду возвращать сообщение об ошибке, если файла нет? Я аж чуть кофе не выплюнул: минуточку, все это время ты молчал о том, что файла нет? Блин, уж будь добр, потрудись сообщить о проблеме.

    При либом отклонении от нормы нужно кидать исключение с максимально детальным текстом. Вот, вобщем-то, простой секрет хорошего кода.

  • Фотографии на Маке

    Моя семейная жизнь течет спокойно и счастливо, однако нет-нет да окажется на грани развода. Всему виной они – фотографии.

    Когда у тебя трое детей, часто фотографируешь их по поводу и без, а потом наступает тот самый момент: дорогой, давай я выберу фотографии для фоторамок в кухне. Я оттягиваю его как могу, потому что знаю – будут крики и ругань.

    Даю жене яблочный ноут. Не проходит и пяти минут, как слышу: почему файл не открывается по нажатию Enter? Почему, открыв фотографию, нельзя перейти к соседней стрелочками? Почему сочетания кнопок такие неудобные? Почему нет бегунка, чтобы превьюшки стали больше?

    Ни на один упрек я не знаю ответа. В этом плане я обычная тряпка, яблочный приспособленец. Я не знаю, почему по нажатию Enter система предлагает переименовать файл, а не открыть его. Возможно, Стив Джобс был под наркотой, когда придумал это. Иначе как объяснить, что вместо Enter нужно либо дважды кликнуть по файлу, либо нажать Command+Down?

    Как часто мы открываем файл, а как часто меняем его имя? Что в приоритете? Ничего, что на клавише написано Enter – по-английски “войти”, а не “переименовать”? Сколько ЛСД принял Стив в тот вечер?

    То же самое с переходом между фотографиями. Когда человек открыл одно фото, он захочет посмотреть соседнее и нажмет стрелку влево или вправо. Логично же? Эпл предлагает выделить фотографии в Finder, нажать правую кнопку мыши, открыть в Preview. После этого можно просматривать несколько фотографий, – но только те, что выбрал.

    Есть быстрый просмотр при помощи пробела, и внезапно в нем работают стрелочки. Однако их поведение зависит от того, в каком виде показаны файлы. Если это список, то работают клавиши вверх и вниз, а влево и вправо бездействуют. Если файлы в режиме значков, перемещение по ним работает во все стороны. Нажав вниз, вы перейдете к такой же позиции следующего ряда, пропустив столько фотографий, сколько их в ряду. Это значит, нельзя просмотерть файлы один за другим, используя одну кнопку. Нужно следить за курсором и нажимать влево, вправо и вниз – другими словами, обходить файлы змейкой.

    Файлы в режиме значков не подстраиваются под ширину окна. Если в ряду их десять и вы ужали окно, матрица не перестроится. Появится горизонтальный скроллинг, и часть файлов вы не увидете.

    Выбор фотографий превратился в маленький адок, который нужно пережить. Однако и я стал умнее. Зная, что супруга управляется с Виндой, я теперь делаю так. Отбираю фотки, которые ее интересуют, и помещаю в какой-то альбом. Это легко сделать на телефоне: достаточно выделить файлы и пометить тегом.

    Далее открываю яблочный ноут, приложение Photos. В нем работает экспорт файлов на диск. Разумеется, можно достучаться до файлов и обычным способом, но это нелегко. Фотографии хранятся в чертовом HEIC – яблочном формате, который совмещает в себе JPEG и HDR. Это добро откроется только на яблочной машине. У файлов машинные имена-уиды, а вся мета о них лежит во внутренней базе данных.

    Я думал, Эпл позволит экспортировать только штучные файлы. Но все оказалось проще: можно экспортировать целый альбом, и на диске появятся нормальные джипеги и mp4.

    Осталось перетащить их на Винду. Это тоже проблематично, потому что шаринг файлов между Эплом и Виндой максимально костыльный. Есть вариант с флешкой в exFAT. Но еще лучше воткнуть флешку в роутер и поднять на нем FTP- или SMB-сервер. Все просто: на яблоке закинул, на Винде скачал.

    И знаете, жить стало намного легче!

    Вот так технологии спасают семейную жизнь.

  • Чат вслух

    Бывает, пишешь коллеге: дружище, я вызываю такой-то сервис, посылаю мапу:

    {:foo 42
     :data ["some-type"]
     :items [:kek :lol :crap]}
    

    Сервис возвращает не то. Вот логи, вот ссылки, вот трассировочный заголовок. Все для тебя, мой милый-хороший.

    В ответ человек срет сообщениями:

    привет :)

    надо items передать через точку с запятой :)

    вроде так было в последнем коммите

    или погоди его не выкатили :)

    спроси девопсов выкатили или нет

    и еще foo надо не 42 а 41 :)

    по ходу да

    или не :)

    а да

    мы так тестировали работало :)

    на пре-проде работало помню

    и без двоеточий передай

    так заработает :)

    Я смотрю на это и думаю: ладно, есть люди, которые думают вслух. Им легче писать и проговаривать про себя, чтобы что-то вспомнить. Бывает. С этим можно смириться.

    Однако в конце этого выхлопа я ожидаю вердикт: какой запрос все-таки передать. Но этого не происходит: собеседник решил, что уже помог. Приходится идти по списку сообщений и применять каждое утверждение (“по ходу да”), а возможно, откатывать (“или не”).

    Что тут сказать? Других мы не исправим, а вот себя исправить можно. Не вываливайте на собеседника поток сознания, а если это имело место, подведите итог. Достаточно смотаться к первому сообщению и коротко на него ответить.

  • Интерфейс музыкальных сервисов

    Не знаю, как так вышло, но сегодня ни один — буквально ни один — музыкальный сервис не может сделать хороший интерфейс. Ни Гугл, ни Яндекс, ни кто бы то ни было. Не помогают ни миллионы денег, которые сервисы гребут за подписку, ни дизайнеры за 400 тыщщ долларов в год.

    Яндекс-музыка уже давно стала мемом а-ля Medium: хорошее начинание превратилось в музей багов. Без преувеличения можно сказать, что каждый квадратный сантиметр ее дизайна несет бред. Гугл со своей Музыкой не лучше: там все прыгает, переключается, показывает выпадашки.

    Прикладываю картинки. Слушаю музыку из мультика, вроде бы все в порядке. Но что-то нажал — открылось то же самое, но в другом лейауте. Почему? Без понятия. Как вернуться обратно? Тоже без понятия.

    Снизу без конца вылазит нотификашка, что контент для детей не доступен в мини-плеере. Предлагает куда-то тапнуть, чтобы вернуться. О чем речь вообще?

    И разумеется, сервису не хватает места. Даже на 4к-мониторе дизайнер не может все уместить. Треки приходят с сервера пачками по 20 штук, нужно проматывать страницу, чтобы они подгрузились, иначе поиск на странице не работает.

    Ради интереса сравните с Винампом: на ЭЛТ-мониторе в разрешении 800x600 он занимал только часть экрана. Там было все: кнопки, перемотка, плейлист, эквалайзер. Можно было поставить рядом Total Commander, и места хватало на две программы.

    Умели же люди делать плеер для монитора 800x600! А сегодня это сродни навыку писать на ассемблере.

    Секрет-то на самом деле простой. Я бы выдал дизайнерам ЭЛТ-мониторы и сказал: все должно помещаться на экране. За каждую выпадашку этим монитором тебя будут бить по голове. И тогда бы все наладилось — я гарантирую это (с).

  • Пробел и пауза

    Как-то давно я писал об ошибке в интерфейсе Ютуба. В нем клавиша “пробел” означает не паузу, а действие по умолчанию для текущего виджета. По умолчанию текущий виджет – экран, но если кликнуть по кнопке субтитров или громкости, текущим станет другой виджет, и пробел тоже будет делать что-то другое.

    Сценарии я тоже описывал: посадил ребенка смотреть мультик, объяснил, что пробел – это пауза. Но в последний момент поправил звук, и фокус остался на виджете громкости. Ребенок жмет пробел – пропадает звук. То же самое с субтитрами: усадил пожилого родственника, все объяснил и перед уходом отключил субтитры, чтобы не загораживали экран. Родственник хочет паузу, жмет пробел и в результате включает субчики.

    Ту же самую херню затащил Яндекс в свою Музыку. Играет трек, мне нравится, жму сердечко. Потом хочу поставить паузу, жму пробел – трек играет, и написано “удалено из избранного”. Оказалось, сердечко – это отдельный виджет, и теперь когда фокус на нем, он добавляет и удаляет из избранного. То же самое с другими кнопками и панелями.

    В общем-то, критиковать тут не за что: ребята стащили чужой подход вместе с багами. В больших компаниях это безопасная стратегия: повторяй за гигантом, и вопросов к тебе не будет. Касается не только менеджеров, но и разработчиков: фраза “как в Гугле” всегда спасет твою задницу.

    И все-таки: если кто-то из читателей связан с Яндексом, будьте добры, передайте им, что у пробела должна быть строго одна функция.

  • Зависимости S3

    Число для справки: сколько зависимостей нужно, чтобы сделать запрос к S3? Вот:

    software/amazon/awssdk/s3/2.33.1/s3-2.33.1.pom
    software/amazon/awssdk/services/2.33.1/services-2.33.1.pom
    software/amazon/awssdk/aws-sdk-java-pom/2.33.1/aws-sdk-java-pom-2.33.1.pom
    software/amazon/awssdk/bom-internal/2.33.1/bom-internal-2.33.1.pom
    software/amazon/awssdk/aws-xml-protocol/2.33.1/aws-xml-protocol-2.33.1.pom
    software/amazon/awssdk/http-auth/2.33.1/http-auth-2.33.1.pom
    software/amazon/awssdk/http-auth-aws/2.33.1/http-auth-aws-2.33.1.pom
    software/amazon/awssdk/profiles/2.33.1/profiles-2.33.1.pom
    software/amazon/awssdk/protocol-core/2.33.1/protocol-core-2.33.1.pom
    software/amazon/awssdk/arns/2.33.1/arns-2.33.1.pom
    software/amazon/awssdk/retries-spi/2.33.1/retries-spi-2.33.1.pom
    software/amazon/awssdk/checksums/2.33.1/checksums-2.33.1.pom
    software/amazon/awssdk/checksums-spi/2.33.1/checksums-spi-2.33.1.pom
    software/amazon/awssdk/identity-spi/2.33.1/identity-spi-2.33.1.pom
    software/amazon/awssdk/http-auth-spi/2.33.1/http-auth-spi-2.33.1.pom
    software/amazon/awssdk/crt-core/2.33.1/crt-core-2.33.1.pom
    software/amazon/awssdk/protocols/2.33.1/protocols-2.33.1.pom
    software/amazon/awssdk/core/2.33.1/core-2.33.1.pom
    software/amazon/awssdk/regions/2.33.1/regions-2.33.1.pom
    software/amazon/awssdk/netty-nio-client/2.33.1/netty-nio-client-2.33.1.pom
    software/amazon/awssdk/annotations/2.33.1/annotations-2.33.1.pom
    software/amazon/awssdk/apache-client/2.33.1/apache-client-2.33.1.pom
    software/amazon/awssdk/endpoints-spi/2.33.1/endpoints-spi-2.33.1.pom
    software/amazon/awssdk/json-utils/2.33.1/json-utils-2.33.1.pom
    software/amazon/awssdk/utils/2.33.1/utils-2.33.1.pom
    software/amazon/awssdk/aws-core/2.33.1/aws-core-2.33.1.pom
    software/amazon/awssdk/auth/2.33.1/auth-2.33.1.pom
    software/amazon/awssdk/metrics-spi/2.33.1/metrics-spi-2.33.1.pom
    software/amazon/awssdk/http-client-spi/2.33.1/http-client-spi-2.33.1.pom
    software/amazon/awssdk/sdk-core/2.33.1/sdk-core-2.33.1.pom
    software/amazon/awssdk/http-clients/2.33.1/http-clients-2.33.1.pom
    io/netty/netty-codec/4.1.124.Final/netty-codec-4.1.124.Final.pom
    io/netty/netty-transport/4.1.124.Final/netty-transport-4.1.124.Final.pom
    io/netty/netty-codec-http/4.1.124.Final/netty-codec-http-4.1.124.Final.pom
    io/netty/netty-codec-http2/4.1.124.Final/netty-codec-http2-4.1.124.Final.pom
    io/netty/netty-buffer/4.1.124.Final/netty-buffer-4.1.124.Final.pom
    io/netty/netty-handler/4.1.124.Final/netty-handler-4.1.124.Final.pom
    io/netty/netty-common/4.1.124.Final/netty-common-4.1.124.Final.pom
    io/netty/netty-parent/4.1.124.Final/netty-parent-4.1.124.Final.pom
    software/amazon/awssdk/aws-query-protocol/2.33.1/aws-query-protocol-2.33.1.pom
    io/netty/netty-resolver/4.1.124.Final/netty-resolver-4.1.124.Final.pom
    io/netty/netty-transport-classes-epoll/4.1.124.Final/netty-transport-classes-epoll-4.1.124.Final.pom
    software/amazon/awssdk/third-party-jackson-core/2.33.1/third-party-jackson-core-2.33.1.pom
    software/amazon/awssdk/http-auth-aws-eventstream/2.33.1/http-auth-aws-eventstream-2.33.1.pom
    software/amazon/awssdk/retries/2.33.1/retries-2.33.1.pom
    software/amazon/awssdk/third-party/2.33.1/third-party-2.33.1.pom
    io/netty/netty-transport-native-unix-common/4.1.124.Final/netty-transport-native-unix-common-4.1.124.Final.pom
    io/netty/netty-codec/4.1.124.Final/netty-codec-4.1.124.Final.jar
    io/netty/netty-common/4.1.124.Final/netty-common-4.1.124.Final.jar
    io/netty/netty-transport-classes-epoll/4.1.124.Final/netty-transport-classes-epoll-4.1.124.Final.jar
    io/netty/netty-codec-http2/4.1.124.Final/netty-codec-http2-4.1.124.Final.jar
    io/netty/netty-buffer/4.1.124.Final/netty-buffer-4.1.124.Final.jar
    io/netty/netty-handler/4.1.124.Final/netty-handler-4.1.124.Final.jar
    software/amazon/awssdk/checksums-spi/2.33.1/checksums-spi-2.33.1.jar
    software/amazon/awssdk/profiles/2.33.1/profiles-2.33.1.jar
    software/amazon/awssdk/aws-xml-protocol/2.33.1/aws-xml-protocol-2.33.1.jar
    software/amazon/awssdk/aws-query-protocol/2.33.1/aws-query-protocol-2.33.1.jar
    software/amazon/awssdk/third-party-jackson-core/2.33.1/third-party-jackson-core-2.33.1.jar
    software/amazon/awssdk/http-auth/2.33.1/http-auth-2.33.1.jar
    software/amazon/awssdk/apache-client/2.33.1/apache-client-2.33.1.jar
    software/amazon/awssdk/retries-spi/2.33.1/retries-spi-2.33.1.jar
    io/netty/netty-transport/4.1.124.Final/netty-transport-4.1.124.Final.jar
    software/amazon/awssdk/endpoints-spi/2.33.1/endpoints-spi-2.33.1.jar
    io/netty/netty-transport-native-unix-common/4.1.124.Final/netty-transport-native-unix-common-4.1.124.Final.jar
    software/amazon/awssdk/crt-core/2.33.1/crt-core-2.33.1.jar
    io/netty/netty-codec-http/4.1.124.Final/netty-codec-http-4.1.124.Final.jar
    software/amazon/awssdk/http-auth-aws-eventstream/2.33.1/http-auth-aws-eventstream-2.33.1.jar
    software/amazon/awssdk/auth/2.33.1/auth-2.33.1.jar
    software/amazon/awssdk/http-client-spi/2.33.1/http-client-spi-2.33.1.jar
    io/netty/netty-resolver/4.1.124.Final/netty-resolver-4.1.124.Final.jar
    software/amazon/awssdk/metrics-spi/2.33.1/metrics-spi-2.33.1.jar
    software/amazon/awssdk/arns/2.33.1/arns-2.33.1.jar
    software/amazon/awssdk/json-utils/2.33.1/json-utils-2.33.1.jar
    software/amazon/awssdk/identity-spi/2.33.1/identity-spi-2.33.1.jar
    software/amazon/awssdk/regions/2.33.1/regions-2.33.1.jar
    software/amazon/awssdk/aws-core/2.33.1/aws-core-2.33.1.jar
    software/amazon/awssdk/sdk-core/2.33.1/sdk-core-2.33.1.jar
    software/amazon/awssdk/protocol-core/2.33.1/protocol-core-2.33.1.jar
    software/amazon/awssdk/checksums/2.33.1/checksums-2.33.1.jar
    software/amazon/awssdk/http-auth-aws/2.33.1/http-auth-aws-2.33.1.jar
    software/amazon/awssdk/retries/2.33.1/retries-2.33.1.jar
    software/amazon/awssdk/http-auth-spi/2.33.1/http-auth-spi-2.33.1.jar
    software/amazon/awssdk/netty-nio-client/2.33.1/netty-nio-client-2.33.1.jar
    software/amazon/awssdk/annotations/2.33.1/annotations-2.33.1.jar
    software/amazon/awssdk/s3/2.33.1/s3-2.33.1.jar
    software/amazon/awssdk/utils/2.33.1/utils-2.33.1.jar
    
  • New library: PG.bin

    PG.bin is a library to parse Postgres COPY dumps made in binary format.

    Postgres has a great API to transfer data into and out from a database called COPY. What is special about it is that it supports three different formats: CSV, text and binary. Both CSV and text are trivial: values are passed using their text representation. Only quoting rules and separating characters differ.

    Binary format is special in that direction that values are not text. They’re passed exactly how they’re stored in Postgres. Thus, binary format is more compact: it’s 30% less in size than CSV or text. The same applies to performance: COPY-ing a binary data back and forth takes about 15-25% less time.

    To parse a binary dump, one must know its structure. This is what the library does: it knows how to parse such dumps. It supports most of the built-in Postgres types including JSON(b). The API is simple an extensible.

    Installation

    Add this to your project:

    ;; lein
    [com.github.igrishaev/pg-bin "0.1.0"]
    
    ;; deps
    com.github.igrishaev/pg-bin {:mvn/version "0.1.0"}
    

    Usage

    Let’s prepare a binary dump as follows:

    create temp table test(
        f_01 int2,
        f_02 int4,
        f_03 int8,
        f_04 boolean,
        f_05 float4,
        f_06 float8,
        f_07 text,
        f_08 varchar(12),
        f_09 time,
        f_10 timetz,
        f_11 date,
        f_12 timestamp,
        f_13 timestamptz,
        f_14 bytea,
        f_15 json,
        f_16 jsonb,
        f_17 uuid,
        f_18 numeric(12,3),
        f_19 text null,
        f_20 decimal
    );
    
    insert into test values (
        1,
        2,
        3,
        true,
        123.456,
        654.321,
        'hello',
        'world',
        '10:42:35',
        '10:42:35+0030',
        '2025-11-30',
        '2025-11-30 10:42:35',
        '2025-11-30 10:42:35.123567+0030',
        '\xDEADBEEF',
        '{"foo": [1, 2, 3, {"kek": [true, false, null]}]}',
        '{"foo": [1, 2, 3, {"kek": [true, false, null]}]}',
        '4bda6037-1c37-4051-9898-13b82f1bd712',
        '123456.123456',
        null,
        '123999.999100500'
    );
    
    \copy test to '/Users/ivan/dump.bin' with (format binary);
    

    Let’s peek what’s inside:

    xxd -d /Users/ivan/dump.bin
    
    00000000: 5047 434f 5059 0aff 0d0a 0000 0000 0000  PGCOPY..........
    00000016: 0000 0000 1400 0000 0200 0100 0000 0400  ................
    00000032: 0000 0200 0000 0800 0000 0000 0000 0300  ................
    00000048: 0000 0101 0000 0004 42f6 e979 0000 0008  ........B..y....
    00000064: 4084 7291 6872 b021 0000 0005 6865 6c6c  @.r.hr.!....hell
    00000080: 6f00 0000 0577 6f72 6c64 0000 0008 0000  o....world......
    00000096: 0008 fa0e 9cc0 0000 000c 0000 0008 fa0e  ................
    00000112: 9cc0 ffff f8f8 0000 0004 0000 24f9 0000  ............$...
    00000128: 0008 0002 e7cc 4a0a fcc0 0000 0008 0002  ......J.........
    00000144: e7cb dec3 0d6f 0000 0004 dead beef 0000  .....o..........
    00000160: 0030 7b22 666f 6f22 3a20 5b31 2c20 322c  .0{"foo": [1, 2,
    00000176: 2033 2c20 7b22 6b65 6b22 3a20 5b74 7275   3, {"kek": [tru
    00000192: 652c 2066 616c 7365 2c20 6e75 6c6c 5d7d  e, false, null]}
    00000208: 5d7d 0000 0031 017b 2266 6f6f 223a 205b  ]}...1.{"foo": [
    00000224: 312c 2032 2c20 332c 207b 226b 656b 223a  1, 2, 3, {"kek":
    00000240: 205b 7472 7565 2c20 6661 6c73 652c 206e   [true, false, n
    00000256: 756c 6c5d 7d5d 7d00 0000 104b da60 371c  ull]}]}....K.`7.
    00000272: 3740 5198 9813 b82f 1bd7 1200 0000 0e00  7@Q..../........
    00000288: 0300 0100 0000 0300 0c0d 8004 ceff ffff  ................
    00000304: ff00 0000 1000 0400 0100 0000 0900 0c0f  ................
    00000320: 9f27 0700 32ff ff                        .'..2..
    

    Now the library comes into play:

    (ns some.ns
      (:require
       [clojure.java.io :as io]
       [pg-bin.core :as copy]
       taggie.core))
    
    (def FIELDS
      [:int2
       :int4
       :int8
       :boolean
       :float4
       :float8
       :text
       :varchar
       :time
       :timetz
       :date
       :timestamp
       :timestamptz
       :bytea
       :json
       :jsonb
       :uuid
       :numeric
       :text
       :decimal])
    
    (copy/parse "/Users/ivan/dump.bin" FIELDS)
    
    [[1
      2
      3
      true
      (float 123.456)
      654.321
      "hello"
      "world"
      #LocalTime "10:42:35"
      #OffsetTime "10:42:35+00:30"
      #LocalDate "2025-11-30"
      #LocalDateTime "2025-11-30T10:42:35"
      #OffsetDateTime "2025-11-30T10:12:35.123567Z"
      (=bytes [-34, -83, -66, -17])
      "{\"foo\": [1, 2, 3, {\"kek\": [true, false, null]}]}"
      "{\"foo\": [1, 2, 3, {\"kek\": [true, false, null]}]}"
      #uuid "4bda6037-1c37-4051-9898-13b82f1bd712"
      123456.123M
      nil
      123999.999100500M]]
    

    Here and below: I use Taggie to render complex values like date & time, byte arrays and so on. Really useful!

    This is what is going on here: we parse a source pointing to a dump using the parse function. A source might be a file, a byte array, an input stream and so on – anything that can be coerced to an input stream using the clojure.java.io/input-stream function.

    Binary files produced by Postgres don’t know their structure. Unfortunately, there is no information about types, only data. One should help the library traverse a binary dump by specifying a vector of types. The FIELDS variable declares the structure of the file. See below what types are supported.

    API

    There are two functions to parse, namely:

    • pg-bin.core/parse accepts any source and returns a vector of parsed lines. This function is eager meaning it consumes the whole source and accumulates lines in a vector.

    • pg-bin.core/parse-seq accepts an InputStream and returns a lazy sequence of parsed lines. It must be called under the with-open macro as follows:

    (with-open [in (io/input-stream "/Users/ivan/dump.bin")]
      (let [lines (copy/parse-seq in FIELDS)]
        (doseq [line lines]
          ...)))
    

    Both functions accept a list of fields as the second argument.

    Skipping fields

    When parsing, it’s likely that you don’t need all fields to be parsed. You may keep only the leading ones:

    (copy/parse DUMP_PATH [:int2 :int4 :int8])
    [[1 2 3]]
    

    To skip fields located in the middle, use either :skip or an underscore:

    (copy/parse DUMP_PATH [:int2 :skip :_ :boolean])
    [[1 true]]
    

    Raw fields

    If, for any reason, you have a type in your dump that the library is not aware about, or you’d like to examine its binary representation, specify :raw or :bytes. Each value will be a byte array then. It’s up to you how to deal with those bytes:

    (copy/parse DUMP_PATH [:raw :raw :bytes])
    [[#bytes [0, 1]
      #bytes [0, 0, 0, 2]
      #bytes [0, 0, 0, 0, 0, 0, 0, 3]]]
    

    Handling JSON

    Postgres is well-known for its vast JSON capabilities, and sometimes tables that we dump have json(b) columns. Above, you saw that by default, they’re parsed as plain strings. This is because there is no a built-in JSON parser in Java and I don’t want to tie this library to a certain JSON implementation.

    But the library provides a number of macros to extend undelrying multi-methods. With a line of code, you can enable parsing json(b) types with Chesire, Jsonista, Clojure.data.json, Charred, and JSam. This is how to do it:

    (ns some.ns
      (:require
       [pg-bin.core :as copy]
       [pg-bin.json :as json]))
    
    (json/set-cheshire keyword) ;; overrides multimethods
    
    (copy/parse DUMP_PATH FIELDS)
    
    [[...
      {:foo [1 2 3 {:kek [true false nil]}]}
      {:foo [1 2 3 {:kek [true false nil]}]}
      ...]]
    

    The set-cheshire macro extends multimethods assuming you have Cheshire installed. Now the parse function, when facing json(b) types, will decode them properly.

    The pg-bin.json namespace provides the following macros:

    • set-string: parse json(b) types as strings again;
    • set-cheshire: parse using Cheshire;
    • set-data-json: parse using clojure.data.json;
    • set-jsonista: parse using Jsonista;
    • set-charred: parse using Charred;
    • set-jsam: parse using JSam.

    All of them accept optional parameters that are passed into the underlying parsing function.

    PG.Bin doesn’t introduce any JSON-related dependencies. Each macro assumes you have added a required library into the classpath.

    Metadata

    Each parsed line tracks its length in bytes, offset from the beginning of a file (or a stream) and a unique index:

    (-> (copy/parse DUMP_PATH FIELDS)
        first
        meta)
    
    #:pg{:length 306, :index 0, :offset 19}
    

    Knowing these values might help reading a dump by chunks.

    Supported types

    • :raw :bytea :bytes for raw access and bytea
    • :skip :_ nil to skip a certain field
    • :uuid to parse UUIDs
    • :int2 :short :smallint :smallserial 2-byte integer (short)
    • :int4 :int :integer :oid :serial 4-byte integer (integer)
    • :int8 :bigint :long :bigserial 8-byte integer (long)
    • :numeric :decimal numeric type (becomes BigDecimal)
    • :float4 :float :real 4-byte float (float)
    • :float8 :double :double-precision 8-byte float (double)
    • :boolean :bool boolean
    • :text :varchar :enum :name :string text values
    • :date becomes java.time.LocalDate
    • :time :time-without-time-zone becomes java.time.LocalTime
    • :timetz :time-with-time-zone becomes java.time.OffsetTime
    • :timestamp :timestamp-without-time-zone becomes java.time.LocalDateTime
    • :timestamptz :timestamp-with-time-zone becomes java.time.OffsetDateTime

    Ping me for more types, if needed.

    On Writing

    At the moment, the library only parses binary dumps. Writing them is possible yet requires extra work. Ping me if you really need writing binary files.

    Scenarios

    Why using this library ever? Imagine you have to fetch a mas-s-s-ive chunk of rows from a database, say 2-3 million to build a report. That might be an issue: you don’t want to saturate memory, neither you want to paginate using LIMIT/OFFSET as it’s slow. A simple solution would be to dump the data you need into a file and process it. You won’t keep the database constantly busy as you’re working with a dump! Here is a small demo:

    (ns some.ns
      (:require
       [pg-bin.core :as copy]
       [pg-bin.json :as json]))
    
    (defn make-copy-manager
      "
      Build an instance of CopyManager from a connection.
      "
      ^CopyManager [^Connection conn]
      (new CopyManager (.unwrap conn BaseConnection)))
    
    (let [conn (jdbc/get-connection data-source)
          mgr (make-copy-manager conn)
          sql "copy table_name(col1, col2...) to stdout with (format binary)"
          ;; you can use a query without parameters as well
          sql "copy (select... from... where...) to stdout with (format binary)"
          ]
      (with-open [out (io/output-stream "/path/to/dump.bin")]
        (.copyOut mgr sql out)))
    
    (with-open [in (io/input-stream "/path/to/dump.bin")]
      (let [lines (copy/parse-seq in [:int2 :text ...])]
        (doseq [line lines]
          ...)))
    

    Above, we dump the data into a file and then process it. There is a way to process lines on the fly using another thread. The second demo:

    (let [conn
          (jdbc/get-connection data-source)
    
          mgr
          (make-copy-manager conn)
    
          sql
          "copy table_name(col1, col2...) to stdout with (format binary)"
    
          in
          (new PipedInputStream)
    
          started? (promise)
    
          fut ;; a future to process the output
          (future
            (with-open [_ in] ;; must close it afterward
              (deliver started? true) ;; must report we have started
              (let [lines (copy/parse-seq in [:int2 :text ...])]
                (doseq [line lines] ;; process on the fly
                  ;; without touching the disk
                  ...))))]
    
      ;; ensure the future has started
      @started?
    
      ;; drain down to the piped output stream
      (with-open [out (new PipedOutputStream in)]
        (.copyOut mgr sql out))
    
      @fut ;; wait for the future to complete
      )
    
  • Место работы

    Я, кстати, придумал вот что: прошелся по всем профилям и убрал текущее место работы. Оставил только vast fintech с описанием технологий — и довольно.

    Почему? Считаю, рекрутерам не нужно знать, где я работаю. Прошлые места смотрите ради бога, а текущее вас не касается. Буду искать работу — созвонимся, и я все расскажу. А пока держите от меня фигу с маслом.

    Есть и моральный аспект. Когда ведешь бложик, работа часто подкидывает темы — в том числе основанные на противоречии, когда не согласен и хочешь выговориться. Чтобы не бросать на кого-то тень, считаю правильным не раскрывать, где работаешь.

    Когда-то я волновался о том, как выгляжу на Линкед-ине. Но когда в пятый раз находишь работу по знакомству, это становится неважно.

    В целом я считаю излишним, когда о тебе знает каждый угол интернета. Еще давно я удалился из всех соцсетей и ни разу не пожалел. Оставил только Гитхаб и Линкед-ин: этих двух хватает, чтобы найти работу где угодно — однако им необязательно знать все.

Страница 1 из 100