﻿<rss version="2.0">
    <channel>    
        <title>Vlad Kostyanetsky</title>
        <description>Hello! My name is Vlad, I'm business app developer.</description>
        <language>en</language>
        <link>https://kostyanetsky.me</link>
        <lastBuildDate>Sat, 28 Mar 2026 19:42:53 +0700</lastBuildDate>
        
        <item>
            <title>The Year In The Date Literal Exceeds 3999</title>
            <link>https://kostyanetsky.me/notes/date-literal-exceeds-3999-once-again</link>
            <guid isPermaLink="false">note-date-literal-exceeds-3999-once-again</guid>
            <pubDate>Sat, 28 Mar 2026 19:42:53 +0700</pubDate>
            <description><p>I already wrote about the error mentioned in this post’s title <a href="https://kostyanetsky.me/notes/date-literal-exceeds-3999" target="_blank">earlier</a>, but here's the refresher: the platform chokes on dates later than the year 3999 (things start blowing up at 3999-12-01, I guess). In theory, dates like that should never make it into the database in the first place. In practice... Well, thanks to bugs in application code — and in the platform itself — they sometimes do.</p>
<p>The usual symptoms are pretty random and annoying: some reports stop working, some documents refuse to post, totals recalculation crashes, and so on. Basically, any code that touches records with those broken dates is now having a bad day.</p>
<p>The fix I outlined in the post linked above does work, but it's relatively slow: you need to configure the tech log, collect the output, and then parse it. The problem is that the platform crashes the moment it touches the first bad date, and there may be lots of them scattered across different tables. So you often end up doing this in multiple passes: run a check, hit an error, fix it, run again, hit the next one, repeat.</p>
<p><img alt="Thank You, Mario!" src="https://kostyanetsky.me/notes/date-literal-exceeds-3999-once-again/thanks.jpg"/></p>
<p>A few years ago, because I wanted a faster way to deal with this, I wrote a <a href="https://gist.github.com/vkostyanetsky/a58ff201d2a87a35e70c4c8f4112ad4c" target="_blank">PostgreSQL query</a> for it. The idea is simple:</p>
<ol>
<li>Find all date fields in the database.</li>
<li>Build one giant query against those fields to look for dates beyond 1C's limit.</li>
</ol>
<p>So yes, this query generates another query. Very normal behavior. Run the generated query, and you get the full picture: a list of tables containing invalid dates. After that, it's just engineering work — inspect the tables and decide what to do. In our case, bad dates sometimes show up in totals and turnovers, so we can just delete those rows and recalculate totals using the standard tools.</p>
<p>Why do this at the DBMS level? Because 1C itself can't really help here — as a reminder, the platform crashes as soon as it touches the bad records. That includes reads, not just writes. Also, in this case, going directly against the database is simply faster and more convenient.</p>
<p>The other day I rewrote the same <a href="https://gist.github.com/vkostyanetsky/5990a16caacc4a9057b577c6a5694512" target="_blank">query for MS SQL</a>. It turned out longer — because, well, MS SQL likes to make you earn it — but the idea is exactly the same.</p>
<p>If you want to use it, keep this in mind:</p>
<ul>
<li>The <code>_Fld626</code> field in the query text is the Fresh separator. In your database it may have a different name, or it may not exist at all.</li>
<li>The query is written for a database that uses a 2000-year offset. If your database does not use that offset, you'll need to adjust the condition accordingly — see <code>DATEADD()</code>.</li>
<li>I added the XML output trick (<code>FOR XML</code>) to stop SSMS from truncating the generated mega-query. That seemed faster than messing around with type casts. The side effect is that before running the generated query, you need to replace <code>&amp;gt;</code> with <code>&gt;</code>.</li>
</ul></description>
        </item>
        
        <item>
            <title>With Side Effect</title>
            <link>https://kostyanetsky.me/notes/with-side-effect</link>
            <guid isPermaLink="false">note-with-side-effect</guid>
            <pubDate>Sun, 15 Mar 2026 12:24:12 +0700</pubDate>
            <description><p>In the latest release of <a href="https://firstbit.ae" target="_blank">our ERP</a>, we optimized several heavy dynamic lists — orders, invoices, proforma invoices, and so on. Over time they had accumulated a pretty solid pile of technical debt, mostly in the form of mountains of helper tables bolted onto the main queries: contact info, technical attributes, balances, turnovers, you name it.</p>
<p>At some point even in fairly small deployments the DB optimizer started producing complete nonsense instead of query plans, and the resource cost of letting that happen was getting less and less funny.</p>
<p>So, we patched things up using several different approaches. One of them was loading extra row data inside the <code>OnGetDataAtServer()</code> handler (I actually <a href="https://kostyanetsky.me/notes/desire-paths" target="_blank">mentioned</a> this mechanism not that long ago). We built a nice little framework around it, rolled it out, tested it, and... Well, now we're all sitting here with those deeply unimpressed engineer faces.</p>
<p>To be fair, performance really did get a lot better. Inside a compact handler, you can tune queries against heavy virtual tables almost perfectly. The problem is somewhere else: field values populated by this handler are not passed into the standard dynamic list mechanisms. Which means search, sorting, and grouping simply do not work for those fields.</p>
<p>So you type a value that is clearly visible in the column — and the row is not found. Or not all matching rows are found. Or rows show up that visibly should not match at all. From the user's point of view, this looks absolutely terrible. A bug is a bug. Those fields don't look any different in the UI, so how exactly are you supposed to explain that this is "just how the platform works™"?</p>
<p>And if you explicitly exclude such a field from the mechanisms that can't handle it, that's somehow even worse. For example, try to sort by it — and boom, giant error message. Explaining why sorting blows up on this "Amount" column while the one right next to it works just fine is... Not exactly a beginner-friendly support conversation.</p>
<p>Honestly, how do you come up with such a great handler concept and then completely fumble the platform-level implementation?</p>
<p>This randomly reminded me of Reddit. Some communities love threads like "invent a superpower, but with a side effect". Like, one person comments: "I can run at the speed of the wind!" and someone replies: "yeah, but you can't stop". That kind of thing. Sometimes it gets pretty funny.</p>
<p><img alt="Superpower" src="https://kostyanetsky.me/notes/with-side-effect/reddit.png"/></p>
<p>That's basically the kind of conversation we're having with the platform developers. You can massively speed up dynamic lists, but the UI will make your users furious. You can automatically send binary data to an S3 bucket, but lose it all with one careless click. You can teleport anywhere, but it takes exactly as long as walking. You can shapeshift, but only into an elderly pug. You have thick, silky, luxurious hair, but on your ass.</p></description>
        </item>
        
        <item>
            <title>Ikigai</title>
            <link>https://kostyanetsky.me/notes/ikigai</link>
            <guid isPermaLink="false">note-ikigai</guid>
            <pubDate>Mon, 09 Mar 2026 22:01:43 +0700</pubDate>
            <description><p>In Japanese, there's a wonderful word: ikigai. It's kind of like your reason for living, but not in some huge existential sense — more in a grounded, everyday way. A source of inner energy, a source of joy. Basically, the thing that makes you want to get out of bed in the morning.</p>
<p>A person can have several of those anchors, and they're different for everyone. Coffee, a cat, a craft you love, taking care of the people close to you, a garden, walks, video games...</p>
<p>Anyway, that's what got me thinking. I stumbled across a channel called "<a href="https://t.me/aleshkino_svoe" target="_blank">Alyoshkino Svoe</a>". From what I understand, the guy behind it is a lawyer from St. Petersburg who breeds fancy chickens and regularly posts videos about specific breeds, incubation details, feeding, and so on.</p>
<p>I'm not into birds at all, but I caught myself just sitting there for twenty minutes, completely mesmerized, listening without looking away. It almost works like therapy, honestly. And it struck me that this is actually a pretty solid kind of ikigai: something you'll love the way this guy loves his chickens.</p></description>
        </item>
        
        <item>
            <title>Autosummary</title>
            <link>https://kostyanetsky.me/notes/autosummary</link>
            <guid isPermaLink="false">note-autosummary</guid>
            <pubDate>Sun, 08 Feb 2026 09:53:48 +0700</pubDate>
            <description><p>I started recording all my meetings back in 2020 — at least, that’s what my own <a href="https://kostyanetsky.me/notes/video-recording" target="_blank">notes</a> say. Video is always more accurate than memory, and clicking "Start Recording" in <a href="https://obsproject.com" target="_blank">OBS</a> is the cheapest way to not lose some random-but-important detail.</p>
<p>The downsides are obvious, though: you can't quickly grasp the gist of a meeting from a video, searching through it is basically impossible, it eats disk space like it's bulking season, and it's painfully easy to accidentally capture something private. To partially compensate, I used to jot down key points in bullet form during the meeting, and later either throw them into a task tracker or write a mini-summary for myself: who I met with, what we discussed, what decisions we landed on. If I forgot something or missed details, I'd double-check the video.</p>
<p>But this method isn't perfect either. Even a rough outline steals focus from the actual meeting. And some "oh right, that detail matters" moments only reveal themselves when it's already too late.</p>
<p><img alt="Forgot" src="https://kostyanetsky.me/notes/autosummary/forgot.jpeg"/></p>
<p>So I ended up with a better approach: extract the meeting audio from the video, turn it into text (with a neural net), then turn that transcript into a detailed meeting summary (with another neural net). Yes, it's neural nets all the way down.</p>
<p>The easiest way to rip the audio is with <a href="https://www.ffmpeg.org" target="_blank">ffmpeg</a> (a command-line utility for working with audio/video). Here's an example (mono, 16 kHz sampling rate + loudness normalization):</p>
<pre>
ffmpeg.exe -y -i "D:\video.mkv" -vn -ac 1 -ar 16000 -af loudnorm -c:a pcm_s16le "D:\audio.wav"
</pre>
<p>As for speech-to-text: I experimented with <a href="https://alphacephei.com/vosk/" target="_blank">Vosk</a> + <a href="https://github.com/benob/recasepunc" target="_blank">recasepunc</a>, but... Yeah, no. Let's just say I'd rather not relive that experience. Meanwhile <s>Boromir</s> <a href="https://openai.com/index/whisper" target="_blank">Whisper</a> (OpenAI's speech recognition model) installs in the background in about 10 minutes:</p>
<pre>
py -3.10 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install setuptools wheel
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install openai-whisper
</pre>
<p>Example run:</p>
<pre>
whisper "D:\audio.wav" --model medium --language Russian --output_format txt
</pre>
<p>The result is a plain text transcript you can shove into any chatbot and get a fairly coherent summary. Sure, you still need to proofread it — fix mistakes and hallucinations, rephrase a couple things — but it's still way better than trying to write notes live.</p>
<p>And that’s basically the whole method. The only thing left is writing a simple script so you don’t have to run two commands manually every time. If you’re on Windows and you can't be bothered to vibe-code, you can grab <a href="https://gist.github.com/vkostyanetsky/4f4760097f1b417cc85d71d11662a642" target="_blank">my script</a> and tweak it.</p>
<p>The script finds the first .mkv in its folder, runs it through ffmpeg + Whisper, and saves the result back into the same folder. If you use it: note that it runs via <a href="https://developer.nvidia.com/cuda" target="_blank">CUDA</a> (CPU works too, just much slower), and it stores downloaded Whisper models not in the default cache, but inside the current Python virtual environment folder.</p>
<p>(if you really want, you can wire up an API call or point it at some local model via <a href="https://lmstudio.ai" target="_blank">LM Studio</a> — but for my personal setup I decided: nope, that's already too much engineering for a lazy win)</p></description>
        </item>
        
        <item>
            <title>New Blog&#x27;s UI</title>
            <link>https://kostyanetsky.me/notes/new-ui</link>
            <guid isPermaLink="false">note-new-ui</guid>
            <pubDate>Sat, 17 Jan 2026 18:18:43 +0700</pubDate>
            <description><p>Over the New Year holidays I randomly got obsessed and rewrote my blog's UI. I only wanted to add search for my notes — there are a lot of them now, and every so often I need to quickly fish something out of the pile (like "here's that link" to a coworker).</p>
<p>The blog runs on <a href="https://pages.github.com" target="_blank">GitHub Pages</a>, so the options aren’t exactly infinite: either outsource search to Google, or build a static index and ship it to the user's browser so it can grep through it locally. I went with the second route: faster, more controllable, <s>and, yes, an excuse to write code</s>. The first time you search it has to download <a href="https://kostyanetsky.me/notes.json" target="_blank">the index file</a>, but... It's 200 KB. That's basically one medium-sized sigh in 2026 internet terms.</p>
<p>And then, you know how it goes... Scope creep grabbed me by the hoodie. First I couldn't get <a href="https://tachyons.io" target="_blank">Tachyons</a> to play nice with the search input — got annoyed and migrated everything to <a href="https://tailwindcss.com" target="_blank">Tailwind</a> (I'd been meaning to try it anyway, just never had a "good reason", so I invented one). While I was writing the search code, I figured it'd be dumb not to add tags too, because why do the same work twice. Next thing I know I "wake up" and there's a whole tag cloud sitting above my notes. At that point it felt only logical to do the same for the projects page — except there it's not tags, it's tech stacks...</p>
<p>So yeah, it turned into that "Fear and Loathing in Las Vegas" meme. The tendency was to push it as far as I can, kinda.</p>
<p>Now the only thing left is to make myself actually write into the new project log. There's always a ton of work, and it's genuinely interesting — but if I don't write things down... Welp. Everything sinks into coffee.</p></description>
        </item>
        
        <item>
            <title>Backup Management</title>
            <link>https://kostyanetsky.me/notes/backup-ui</link>
            <guid isPermaLink="false">note-backup-ui</guid>
            <pubDate>Sat, 06 Dec 2025 20:57:48 +0700</pubDate>
            <description><p>At the end of the year we shipped a big update to our internal tool (I briefly <a href="https://kostyanetsky.me/notes/easter-eggs" target="_blank">wrote</a> about it before). The goal: give teammates sane, usable access to backups of customer apps. In a SaaS company, everyone needs backups all the time — dev, QA, incident investigations, you name it. Without proper tracking the whole thing turns into a petting zoo: three people, same moment, three requests for almost identical copies of the same database. Sure, it "works", but you end up chewing through 3x the resources for zero extra value.</p>
<p>We did have a solution built on top of the Bitrix UI, but due to, uh... the unique "evolutionary path" of that product, it delivered more pain than gain. So we rethought the workflow and rewrote everything. Frontend is 1C; backend is PostgREST, PostgreSQL, PowerShell, and a bunch of other bits and bobs. The internal logic is fairly gnarly, but for the user it's a clean, friendly UI where you can request a backup in literally two clicks.</p>
<p>You can pick one of three backup types:</p>
<ul>
<li>Cloud backup (a copy of the real app deployed in the cloud and accessible — including via the browser);</li>
<li>File backup (a regular .dt file you can download and spin up locally);</li>
<li>Configuration + extensions backup (.cf + .cfe).</li>
</ul>
<p>Also, the new solution detects when someone tries to request a backup for an app that's already being generated right now. And it rate-limits things too: you can't back up the same app more than once per hour.</p>
<p>And yes — we're still sneaking jokes into the UI. Obviously.</p>
<p><img alt="But Still!" src="https://kostyanetsky.me/notes/backup-ui/but-still.png"/></p>
<p><img alt="Coffee First!" src="https://kostyanetsky.me/notes/backup-ui/coffee-first.png"/></p></description>
        </item>
        
        <item>
            <title>Desire Paths</title>
            <link>https://kostyanetsky.me/notes/desire-paths</link>
            <guid isPermaLink="false">note-desire-paths</guid>
            <pubDate>Sat, 29 Nov 2025 21:00:48 +0700</pubDate>
            <description><p>Alright, Jean Fresco's riddle. You've got a table — say, ~50k rows. How do you end up reading half a billion?</p>
<p>Easy-peasy: Nested Loops + Clustered Index Seek:</p>
<p><img alt="Half a billion" src="https://kostyanetsky.me/notes/desire-paths/500_million.png"/></p>
<p>Clustered Index Seek is a bit of a marketing name here, of course. In reality, on every execution the operator walks the entire table (the whole clustered index) and checks every row against the predicates. And it does that 10 730 times for 51 391 rows. Result: 551 425 430 rows read, 13 343 returned.</p>
<p><img alt="Ouch" src="https://kostyanetsky.me/notes/desire-paths/ouch.gif"/></p>
<p>So yeah — a perfect textbook example of a horrible query plan in a vacuum. Put it in a museum. Nested Loops, if you've forgotten, works roughly like this:</p>
<pre>
For Each Table1Row In Table1 Do
    For Each Table2Row In Table2 Do
        ...
</pre>
<p>That's fine for small tables, but the DB can also pick it for bigger ones — for example, if it runs out of time to build a proper plan.</p>
<p>That's exactly what happened here. Zooming out to the platform level: we've got a dynamic list that queries a documents table, and the devs bolted on like a dozen virtual tables from accumulation registers.</p>
<p>Some of those registers were huge on their own, and the virtual tables poured gasoline on the fire (each one turns into 2+ nested queries). The DB honestly tried to come up with an efficient strategy, but at some point it basically decided: "a garbage plan is still better than no plan at all".</p>
<p>And the user? They tried to search a document by number — and the client app just straight-up froze.</p>
<p>So, about virtual tables in dynamic lists. There's a nice English phrase: "desire path" — the trail people naturally carve because it's the easiest way. Slapping a virtual table onto the main one really is often the simplest, fastest, most familiar way to solve the task. But it's <strong>not efficient</strong>.</p>
<p>There's an alternative, for example the <code>OnGetDataAtServer()</code> handler. It takes longer to implement, but it lets you properly tune the virtual table and avoid the scenario above. Scrolling the list will produce more queries, sure — but they'll be smaller, faster, and way more efficient than one single giant monster query.</p></description>
        </item>
        
        <item>
            <title>Food Diary In Obsidian Bases</title>
            <link>https://kostyanetsky.me/notes/obsidian-foodiary-bases</link>
            <guid isPermaLink="false">note-obsidian-foodiary-bases</guid>
            <pubDate>Sun, 23 Nov 2025 00:01:58 +0700</pubDate>
            <description><p>I've rebuilt last year’s <a href="https://kostyanetsky.me/notes/obsidian-foodiary" target="_blank">plugin</a> with <a href="https://help.obsidian.md/bases" target="_blank">Obsidian Bases</a> — it now tracks calories, protein, fat, and carbs in your food.</p>
<p>The result is way more flexible and customizable than the plugin version: if you suddenly decide you want to count fiber as well or just shuffle some columns in the report, you don't have to rewrite, rebuild, or release anything. You just tweak it to your heart's content.</p>
<p>And it doesn't look half bad either:</p>
<p><img alt="UI" src="https://kostyanetsky.me/notes/obsidian-foodiary-bases/base.png"/></p>
<p>All the necessary settings and scripts live in the <a href="https://github.com/vkostyanetsky/ObsidianFoodiaryBases" target="_blank">GitHub repo</a>.</p></description>
        </item>
        
        <item>
            <title>Well, There Are Some</title>
            <link>https://kostyanetsky.me/notes/well-there-are-some</link>
            <guid isPermaLink="false">note-well-there-are-some</guid>
            <pubDate>Mon, 17 Nov 2025 13:26:00 +0700</pubDate>
            <description><p>I'm out for a walk in the evening, and behind me there's some mom and her little kid — about five years old, I guess. I don't see them, I just hear them talking. The mom is explaining university to the kid: that you have to get in, study, there will be exams and all that.</p>
<p>The boy is quiet for a bit, then says sadly:</p>
<p>— I thought there was only school, but it turns out there are more difficulties too...</p></description>
        </item>
        
        <item>
            <title>Harmless Harm</title>
            <link>https://kostyanetsky.me/notes/harmless-harm</link>
            <guid isPermaLink="false">note-harmless-harm</guid>
            <pubDate>Sun, 16 Nov 2025 10:36:00 +0700</pubDate>
            <description><p>The other day my colleague and I were debugging an issue, nothing fancy, just another "why is this query behaving weird?" moment.</p>
<p>Simplified, the idea was: we read a table from the database and dump the result into a temp table. If a certain condition is met, we still want the temp table to be created, but it must be empty (regardless of whether the source table has rows or not).</p>
<p>The query looked roughly like this:</p>
<pre>
SELECT
    Table.Field1 AS Field1
FROM
    Table AS Table
WHERE 
    &amp;Parameter
</pre>
<p>If we needed to copy rows from the source table into the temp table, we passed TRUE into the parameter; if we wanted the temp table to be empty, we passed FALSE.</p>
<p>Looks simple at first glance, but this trick is a silent performance foot-gun if the table being read is large.</p>
<p>The reason is how DB engines work with parameterized queries. Both MS SQL and PostgreSQL build the execution plan based on the query text, and in this example the parameter value does <strong>not</strong> affect the decision of whether the table should be read or not.</p>
<p>As a result, when this query runs, both engines will pedantically scan the whole table (or its index), even if the parameter is FALSE. In that case each row is just discarded, so the logic is technically correct, but we're wasting resources on a pointless scan and polluting the buffer cache, slowing down the system overall and diligently contributing to global warming :)</p>
<p>The fix is simple: inline TRUE/FALSE in the query body as a constant instead of using a parameter. Or use TOP so the query text is even simpler:</p>
<pre>
SELECT TOP 0
    Table.Field1 AS Field1
FROM
    Table AS Table
</pre>
<p>At the SQL level this turns into something like "SELECT TOP 0 ... FROM Table" for MS SQL and "SELECT ... FROM Table LIMIT 0" for PostgreSQL. The final plan will still contain a read operator, but the executor won't actually request any rows, so there's no real data scan (yay).</p>
<p>P.S. If you don't care about having proper column types in the temp table, you can even do this:</p>
<pre>
SELECT TOP 0
    UNDEFINED AS Field1
</pre>
<p>The performance gain, however, is so tiny that it's probably not worth over-optimizing for this. There are better hills to die on.</p></description>
        </item>
        
    </channel>
</rss>