[{"data":1,"prerenderedAt":15292},["ShallowReactive",2],{"navigation_docs":3,"-java-pr2-integration-testing-testcontainers":2949,"-java-pr2-integration-testing-testcontainers-surround":15288},[4,1640,1765,2219,2352,2559,2641,2691,2748,2782,2908,2945],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"C#","i-devicon-csharp","/csharp","01.csharp",[10,13,60,90,120,202,219,253,379,404,457,650,1346,1636],{"title":11,"path":7,"stem":12},"C# Roadmap","01.csharp/index",{"title":14,"icon":15,"path":16,"stem":17,"children":18,"page":59},"Fundamentals","i-lucide-book-open","/csharp/fundamentals","01.csharp/01.fundamentals",[19,23,27,31,35,39,43,47,51,55],{"title":20,"path":21,"stem":22},"Вступ до екосистеми .NET","/csharp/fundamentals/introduction-to-ecosystem","01.csharp/01.fundamentals/01.introduction-to-ecosystem",{"title":24,"path":25,"stem":26},"Структура програми на C#","/csharp/fundamentals/program-structure","01.csharp/01.fundamentals/02.program-structure",{"title":28,"path":29,"stem":30},"Змінні та Типи Даних","/csharp/fundamentals/variables-data-types","01.csharp/01.fundamentals/03.variables-data-types",{"title":32,"path":33,"stem":34},"Масиви","/csharp/fundamentals/arrays","01.csharp/01.fundamentals/04.arrays",{"title":36,"path":37,"stem":38},"Strings & Text Handling","/csharp/fundamentals/strings-text-handling","01.csharp/01.fundamentals/05.strings-text-handling",{"title":40,"path":41,"stem":42},"Дати і Час","/csharp/fundamentals/dates-time-handling","01.csharp/01.fundamentals/06.dates-time-handling",{"title":44,"path":45,"stem":46},"Потік Керування","/csharp/fundamentals/control-flow","01.csharp/01.fundamentals/07.control-flow",{"title":48,"path":49,"stem":50},"Методи","/csharp/fundamentals/methods","01.csharp/01.fundamentals/08.methods",{"title":52,"path":53,"stem":54},"Основи Відлагодження","/csharp/fundamentals/debugging-basics","01.csharp/01.fundamentals/09.debugging-basics",{"title":56,"path":57,"stem":58},"Інтерактивна Консоль (Classic)","/csharp/fundamentals/interactive-console","01.csharp/01.fundamentals/10.interactive-console",false,{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":59},"OOP","i-lucide-box","/csharp/oop","01.csharp/02.oop",[66,70,74,78,82,86],{"title":67,"path":68,"stem":69},"Package Management (Управління Пакетами)","/csharp/oop/package-management","01.csharp/02.oop/01.package-management",{"title":71,"path":72,"stem":73},"Класи та Об'єкти","/csharp/oop/classes-objects","01.csharp/02.oop/02.classes-objects",{"title":75,"path":76,"stem":77},"Властивості та Поля","/csharp/oop/properties-fields","01.csharp/02.oop/03.properties-fields",{"title":79,"path":80,"stem":81},"Стовпи ООП","/csharp/oop/oop-pillars","01.csharp/02.oop/04.oop-pillars",{"title":83,"path":84,"stem":85},"Advanced Types","/csharp/oop/advanced-types","01.csharp/02.oop/05.advanced-types",{"title":87,"path":88,"stem":89},"Namespaces (Простори Імен)","/csharp/oop/namespaces","01.csharp/02.oop/06.namespaces",{"title":91,"icon":92,"path":93,"stem":94,"children":95,"page":59},"Advanced Core","i-lucide-zap","/csharp/advanced-core","01.csharp/03.advanced-core",[96,100,104,108,112,116],{"title":97,"path":98,"stem":99},"Generics (Узагальнення)","/csharp/advanced-core/generics","01.csharp/03.advanced-core/01.generics",{"title":101,"path":102,"stem":103},"Делегати, Події та Лямбда-вирази","/csharp/advanced-core/delegates-events-lambdas","01.csharp/03.advanced-core/02.delegates-events-lambdas",{"title":105,"path":106,"stem":107},"Interfaces Deep Dive (Інтерфейси: Поглиблений Розгляд)","/csharp/advanced-core/interfaces-deep-dive","01.csharp/03.advanced-core/03.interfaces-deep-dive",{"title":109,"path":110,"stem":111},"Обробка Винятків","/csharp/advanced-core/exception-handling","01.csharp/03.advanced-core/04.exception-handling",{"title":113,"path":114,"stem":115},"Pattern Matching","/csharp/advanced-core/pattern-matching","01.csharp/03.advanced-core/05.pattern-matching",{"title":117,"path":118,"stem":119},"Додаткові Можливості C#","/csharp/advanced-core/additional-features","01.csharp/03.advanced-core/06.additional-features",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":59},"Architecture Best Practices","i-lucide-building-2","/csharp/architecture-best-practices","01.csharp/04.architecture-best-practices",[126,130,149,153,157,161,165,169],{"title":127,"path":128,"stem":129},"Software Design Principles (Частина 1)","/csharp/architecture-best-practices/software-design-principles","01.csharp/04.architecture-best-practices/01.software-design-principles",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":59},"Design Patterns","i-lucide-folder","/csharp/architecture-best-practices/design-patterns","01.csharp/04.architecture-best-practices/02.design-patterns",[136],{"title":137,"icon":132,"path":138,"stem":139,"children":140,"page":59},"Creational","/csharp/architecture-best-practices/design-patterns/creational","01.csharp/04.architecture-best-practices/02.design-patterns/creational",[141,145],{"title":142,"path":143,"stem":144},"Singleton (Одинак)","/csharp/architecture-best-practices/design-patterns/creational/singleton","01.csharp/04.architecture-best-practices/02.design-patterns/creational/01.singleton",{"title":146,"path":147,"stem":148},"Builder (Будівельник)","/csharp/architecture-best-practices/design-patterns/creational/builder","01.csharp/04.architecture-best-practices/02.design-patterns/creational/02.builder",{"title":150,"path":151,"stem":152},"Building Professional CLIs","/csharp/architecture-best-practices/building-professional-clis","01.csharp/04.architecture-best-practices/03.building-professional-clis",{"title":154,"path":155,"stem":156},"Validation & Flow Control","/csharp/architecture-best-practices/validation-flow-control","01.csharp/04.architecture-best-practices/04.validation-flow-control",{"title":158,"path":159,"stem":160},"The Modern .NET Host (Microsoft.Extensions)","/csharp/architecture-best-practices/modern-dotnet-host","01.csharp/04.architecture-best-practices/05.modern-dotnet-host",{"title":162,"path":163,"stem":164},"Data Mapper: Repository та DAO патерни (Частина 1)","/csharp/architecture-best-practices/data-mapper-part1","01.csharp/04.architecture-best-practices/06.data-mapper-part1",{"title":166,"path":167,"stem":168},"Data Mapper: Repository та DAO патерни (Частина 2)","/csharp/architecture-best-practices/data-mapper-part2","01.csharp/04.architecture-best-practices/07.data-mapper-part2",{"title":170,"icon":132,"path":171,"stem":172,"children":173,"page":59},"Di Ioc","/csharp/architecture-best-practices/di-ioc","01.csharp/04.architecture-best-practices/08.di-ioc",[174,178,182,186,190,194,198],{"title":175,"path":176,"stem":177},"Проблема залежностей та Інверсія Контролю","/csharp/architecture-best-practices/di-ioc/the-dependency-problem","01.csharp/04.architecture-best-practices/08.di-ioc/01.the-dependency-problem",{"title":179,"path":180,"stem":181},"Будуємо власний Service Container","/csharp/architecture-best-practices/di-ioc/build-your-own-container","01.csharp/04.architecture-best-practices/08.di-ioc/02.build-your-own-container",{"title":183,"path":184,"stem":185},"Service Locator: Паттерн та Анти-паттерн","/csharp/architecture-best-practices/di-ioc/service-locator-pattern","01.csharp/04.architecture-best-practices/08.di-ioc/03.service-locator-pattern",{"title":187,"path":188,"stem":189},"Паттерни Dependency Injection","/csharp/architecture-best-practices/di-ioc/dependency-injection-patterns","01.csharp/04.architecture-best-practices/08.di-ioc/04.dependency-injection-patterns",{"title":191,"path":192,"stem":193},"Microsoft DI: IServiceCollection та IServiceProvider","/csharp/architecture-best-practices/di-ioc/microsoft-di-deep-dive","01.csharp/04.architecture-best-practices/08.di-ioc/05.microsoft-di-deep-dive",{"title":195,"path":196,"stem":197},"Service Lifetimes та Scopes","/csharp/architecture-best-practices/di-ioc/service-lifetimes-and-scopes","01.csharp/04.architecture-best-practices/08.di-ioc/06.service-lifetimes-and-scopes",{"title":199,"path":200,"stem":201},"DI Анти-паттерни та Найкращі Практики","/csharp/architecture-best-practices/di-ioc/di-anti-patterns-and-best-practices","01.csharp/04.architecture-best-practices/08.di-ioc/07.di-anti-patterns-and-best-practices",{"title":203,"icon":132,"path":204,"stem":205,"children":206,"page":59},"Standard Library","/csharp/standard-library","01.csharp/05.standard-library",[207,211,215],{"title":208,"path":209,"stem":210},"Collections (Колекції)","/csharp/standard-library/collections","01.csharp/05.standard-library/01.collections",{"title":212,"path":213,"stem":214},"High Performance Types (Високопродуктивні Типи)","/csharp/standard-library/high-performance-types","01.csharp/05.standard-library/02.high-performance-types",{"title":216,"path":217,"stem":218},"LINQ (Language Integrated Query)","/csharp/standard-library/linq","01.csharp/05.standard-library/03.linq",{"title":220,"icon":221,"path":222,"stem":223,"children":224,"page":59},"System Internals Concurrency","i-lucide-server","/csharp/system-internals-concurrency","01.csharp/06.system-internals-concurrency",[225,229,233,237,241,245,249],{"title":226,"path":227,"stem":228},"Memory Management","/csharp/system-internals-concurrency/memory-management","01.csharp/06.system-internals-concurrency/01.memory-management",{"title":230,"path":231,"stem":232},"Reflection API: System.Type та Метадані","/csharp/system-internals-concurrency/reflection-fundamentals","01.csharp/06.system-internals-concurrency/02.reflection-fundamentals",{"title":234,"path":235,"stem":236},"Attributes та Dynamic Language Runtime","/csharp/system-internals-concurrency/attributes-dynamic","01.csharp/06.system-internals-concurrency/03.attributes-dynamic",{"title":238,"path":239,"stem":240},"Expression Trees: Швидка Альтернатива Рефлексії","/csharp/system-internals-concurrency/expression-trees-compiled","01.csharp/06.system-internals-concurrency/04.expression-trees-compiled",{"title":242,"path":243,"stem":244},"Source Generators: Compile-Time Code Generation","/csharp/system-internals-concurrency/source-generators","01.csharp/06.system-internals-concurrency/05.source-generators",{"title":246,"path":247,"stem":248},"Multithreading Fundamentals","/csharp/system-internals-concurrency/multithreading-fundamentals","01.csharp/06.system-internals-concurrency/06.multithreading-fundamentals",{"title":250,"path":251,"stem":252},"Synchronization Primitives","/csharp/system-internals-concurrency/synchronization-primitives","01.csharp/06.system-internals-concurrency/07.synchronization-primitives",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":59},"System Programming Windows","i-lucide-cpu","/csharp/system-programming-windows","01.csharp/07.system-programming-windows",[259,263,267,271,275,279,283,287,291,295,299,303,307,311,315,319,323,327,331,335,339,343,347,351,355,359,363,367,371,375],{"title":260,"path":261,"stem":262},"Як Працює Операційна Система","/csharp/system-programming-windows/how-os-works","01.csharp/07.system-programming-windows/01.how-os-works",{"title":264,"path":265,"stem":266},"Процеси в .NET — API та Запуск","/csharp/system-programming-windows/processes-in-dotnet","01.csharp/07.system-programming-windows/02.processes-in-dotnet",{"title":268,"path":269,"stem":270},"Процеси в .NET — IPC та Міжпроцесна Комунікація","/csharp/system-programming-windows/02a.processes-ipc","01.csharp/07.system-programming-windows/02a.processes-ipc",{"title":272,"path":273,"stem":274},"Application Domains та Збірки — AppDomain і AssemblyLoadContext","/csharp/system-programming-windows/appdomains-assemblies","01.csharp/07.system-programming-windows/03.appdomains-assemblies",{"title":276,"path":277,"stem":278},"Application Domains та Збірки — Plug-in Система з Hot-Reload","/csharp/system-programming-windows/03a.appdomains-plugin-system","01.csharp/07.system-programming-windows/03a.appdomains-plugin-system",{"title":280,"path":281,"stem":282},"Потоки — Основи та API Thread","/csharp/system-programming-windows/thread-fundamentals","01.csharp/07.system-programming-windows/04.thread-fundamentals",{"title":284,"path":285,"stem":286},"Потоки — Lifecycle, Пріоритети та Безпечне Завершення","/csharp/system-programming-windows/04a.thread-lifecycle-priorities","01.csharp/07.system-programming-windows/04a.thread-lifecycle-priorities",{"title":288,"path":289,"stem":290},"Проблеми Спільного Стану — Race Condition та Data Race","/csharp/system-programming-windows/shared-state-problems","01.csharp/07.system-programming-windows/05.shared-state-problems",{"title":292,"path":293,"stem":294},"Проблеми Спільного Стану — Memory Model та volatile","/csharp/system-programming-windows/05a.shared-state-memory-model","01.csharp/07.system-programming-windows/05a.shared-state-memory-model",{"title":296,"path":297,"stem":298},"Синхронізація — Monitor, lock та еволюція примітивів","/csharp/system-programming-windows/synchronization-fundamentals","01.csharp/07.system-programming-windows/06.synchronization-fundamentals",{"title":300,"path":301,"stem":302},"Синхронізація — Наскрізний Приклад та Deadlock Detection","/csharp/system-programming-windows/06a.synchronization-walkthrough","01.csharp/07.system-programming-windows/06a.synchronization-walkthrough",{"title":304,"path":305,"stem":306},"Синхронізація — Mutex, Semaphore та Event-Based Primitives","/csharp/system-programming-windows/synchronization-advanced","01.csharp/07.system-programming-windows/07.synchronization-advanced",{"title":308,"path":309,"stem":310},"Синхронізація — Interlocked, Volatile та Lock-Free Структури","/csharp/system-programming-windows/07a.synchronization-advanced-walkthrough","01.csharp/07.system-programming-windows/07a.synchronization-advanced-walkthrough",{"title":312,"path":313,"stem":314},"Interlocked, CAS та Lock-Free Структури","/csharp/system-programming-windows/interlocked-cas-lockfree","01.csharp/07.system-programming-windows/08.interlocked-cas-lockfree",{"title":316,"path":317,"stem":318},"Volatile, Memory Model та Spinning","/csharp/system-programming-windows/08a.volatile-memory-model","01.csharp/07.system-programming-windows/08a.volatile-memory-model",{"title":320,"path":321,"stem":322},"ThreadPool — Пул Потоків для Ефективного Виконання","/csharp/system-programming-windows/thread-pool","01.csharp/07.system-programming-windows/09.thread-pool",{"title":324,"path":325,"stem":326},"ThreadPool — Просунуті Сценарії та Внутрішня Будова","/csharp/system-programming-windows/09a.thread-pool-advanced","01.csharp/07.system-programming-windows/09a.thread-pool-advanced",{"title":328,"path":329,"stem":330},"Concurrent та Immutable Collections","/csharp/system-programming-windows/concurrent-collections","01.csharp/07.system-programming-windows/10.concurrent-collections",{"title":332,"path":333,"stem":334},"TPL, Task та Композиція — Від Thread до Task","/csharp/system-programming-windows/tpl-parallel-plinq","01.csharp/07.system-programming-windows/11.tpl-parallel-plinq",{"title":336,"path":337,"stem":338},"Parallel Class та PLINQ — Data Parallelism","/csharp/system-programming-windows/11a.tpl-parallel-plinq-advanced","01.csharp/07.system-programming-windows/11a.tpl-parallel-plinq-advanced",{"title":340,"path":341,"stem":342},"Async/Await — Фундамент Асинхронного Програмування","/csharp/system-programming-windows/async-fundamentals","01.csharp/07.system-programming-windows/12.async-fundamentals",{"title":344,"path":345,"stem":346},"SynchronizationContext та ConfigureAwait — Контекст Виконання","/csharp/system-programming-windows/async-context-configureawait","01.csharp/07.system-programming-windows/13.async-context-configureawait",{"title":348,"path":349,"stem":350},"Async — Просунуті Паттерни","/csharp/system-programming-windows/async-advanced","01.csharp/07.system-programming-windows/14.async-advanced",{"title":352,"path":353,"stem":354},"System.Threading.Channels — Async Producer-Consumer","/csharp/system-programming-windows/channels","01.csharp/07.system-programming-windows/15.channels",{"title":356,"path":357,"stem":358},"Асинхронна Синхронізація","/csharp/system-programming-windows/async-synchronization","01.csharp/07.system-programming-windows/16.async-synchronization",{"title":360,"path":361,"stem":362},"Unsafe Code та Вказівники","/csharp/system-programming-windows/unsafe-code","01.csharp/07.system-programming-windows/17.unsafe-code",{"title":364,"path":365,"stem":366},"P/Invoke та Windows API — Міст між .NET та Native Code","/csharp/system-programming-windows/pinvoke-winapi","01.csharp/07.system-programming-windows/18.pinvoke-winapi",{"title":368,"path":369,"stem":370},"Реєстр Windows — Центральна База Конфігурації Системи","/csharp/system-programming-windows/windows-registry","01.csharp/07.system-programming-windows/19.windows-registry",{"title":372,"path":373,"stem":374},"Windows Hooks, Hotkeys та Services — Глибока Інтеграція з ОС","/csharp/system-programming-windows/windows-hooks-services","01.csharp/07.system-programming-windows/20.windows-hooks-services",{"title":376,"path":377,"stem":378},"Системне Програмування C# (Windows) — 07.system-programming-windows","/csharp/system-programming-windows/implementation_plan","01.csharp/07.system-programming-windows/implementation_plan",{"title":380,"icon":132,"path":381,"stem":382,"children":383,"page":59},"Io","/csharp/io","01.csharp/08.io",[384,388,392,396,400],{"title":385,"path":386,"stem":387},"8.1.1. Основи роботи з файловою системою","/csharp/io/file-system-basics","01.csharp/08.io/01.file-system-basics",{"title":389,"path":390,"stem":391},"8.1.2. Потоки (Streams) та Серіалізація Даних","/csharp/io/streams-serialization","01.csharp/08.io/02.streams-serialization",{"title":393,"path":394,"stem":395},"8.2.1. JSON Serialization з System.Text.Json","/csharp/io/json-serialization","01.csharp/08.io/03.json-serialization",{"title":397,"path":398,"stem":399},"8.2.2. XML Serialization та LINQ to XML","/csharp/io/xml-serialization","01.csharp/08.io/04.xml-serialization",{"title":401,"path":402,"stem":403},"8.2.3. Binary Serialization: MessagePack та Protocol Buffers","/csharp/io/binary-serialization","01.csharp/08.io/05.binary-serialization",{"title":405,"icon":132,"path":406,"stem":407,"children":408,"page":59},"Ado Net","/csharp/ado-net","01.csharp/09.ado-net",[409,413,417,421,425,429,433,437,441,445,449,453],{"title":410,"path":411,"stem":412},"9.1. Введення в ADO.NET","/csharp/ado-net/introduction-to-adonet","01.csharp/09.ado-net/01.introduction-to-adonet",{"title":414,"path":415,"stem":416},"9.2. Клас DbConnection — з'єднання з базою даних","/csharp/ado-net/connection","01.csharp/09.ado-net/02.connection",{"title":418,"path":419,"stem":420},"9.3. Клас DbCommand — виконання SQL-запитів","/csharp/ado-net/command-and-queries","01.csharp/09.ado-net/03.command-and-queries",{"title":422,"path":423,"stem":424},"9.4. Клас DbDataReader — ефективне читання даних","/csharp/ado-net/datareader","01.csharp/09.ado-net/04.datareader",{"title":426,"path":427,"stem":428},"9.5. Параметризовані запити та захист від SQL Injection","/csharp/ado-net/parameters-and-sql-injection","01.csharp/09.ado-net/05.parameters-and-sql-injection",{"title":430,"path":431,"stem":432},"9.6. Транзакції в ADO.NET","/csharp/ado-net/transactions","01.csharp/09.ado-net/06.transactions",{"title":434,"path":435,"stem":436},"9.7. DbProviderFactory — провайдер-незалежний код","/csharp/ado-net/provider-factory","01.csharp/09.ado-net/07.provider-factory",{"title":438,"path":439,"stem":440},"9.8. Асинхронний доступ до даних","/csharp/ado-net/async-data-access","01.csharp/09.ado-net/08.async-data-access",{"title":442,"path":443,"stem":444},"9.9. Від'єднаний режим: DataSet, DataTable, DataRow","/csharp/ado-net/disconnected-mode-dataset","01.csharp/09.ado-net/09.disconnected-mode-dataset",{"title":446,"path":447,"stem":448},"9.10. DataAdapter — міст між DataSet та базою даних","/csharp/ado-net/data-adapter","01.csharp/09.ado-net/10.data-adapter",{"title":450,"path":451,"stem":452},"9.11. Data Mapper та Repository: Архітектура доступу до даних","/csharp/ado-net/data-mapper-repository","01.csharp/09.ado-net/11.data-mapper-repository",{"title":454,"path":455,"stem":456},"9.12. Identity Map, Unit of Work та Specification Pattern","/csharp/ado-net/advanced-patterns","01.csharp/09.ado-net/12.advanced-patterns",{"title":458,"icon":255,"path":459,"stem":460,"children":461,"page":59},"Ef Core","/csharp/ef-core","01.csharp/10.ef-core",[462,466,470,474,478,482,486,490,494,498,502,506,510,514,518,522,526,532,538,542,546,550,554,558,562,566,570,574,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646],{"title":463,"path":464,"stem":465},"Що таке ORM? Від SQL до об'єктів","/csharp/ef-core/what-is-orm","01.csharp/10.ef-core/01.what-is-orm",{"title":467,"path":468,"stem":469},"Перший проєкт — від нуля до CRUD","/csharp/ef-core/first-project","01.csharp/10.ef-core/02.first-project",{"title":471,"path":472,"stem":473},"DbContext — Серце EF Core","/csharp/ef-core/dbcontext-deep-dive","01.csharp/10.ef-core/03.dbcontext-deep-dive",{"title":475,"path":476,"stem":477},"Провайдери баз даних — Архітектура та Вибір СУБД","/csharp/ef-core/database-providers","01.csharp/10.ef-core/04.database-providers",{"title":479,"path":480,"stem":481},"Конвенції EF Core — Магія без конфігурації","/csharp/ef-core/conventions","01.csharp/10.ef-core/05.conventions",{"title":483,"path":484,"stem":485},"Fluent API та Data Annotations — Явна конфігурація моделі","/csharp/ef-core/fluent-api-vs-annotations","01.csharp/10.ef-core/06.fluent-api-vs-annotations",{"title":487,"path":488,"stem":489},"Зв'язки — One-to-One та One-to-Many","/csharp/ef-core/relationships-basics","01.csharp/10.ef-core/07.relationships-basics",{"title":491,"path":492,"stem":493},"Зв'язки Advanced — Many-to-Many та Складні Сценарії","/csharp/ef-core/relationships-advanced","01.csharp/10.ef-core/08.relationships-advanced",{"title":495,"path":496,"stem":497},"Властивості — Типи, Конвертери, Компаратори (Частина 1)","/csharp/ef-core/property-configuration-part1","01.csharp/10.ef-core/09.property-configuration-part1",{"title":499,"path":500,"stem":501},"Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)","/csharp/ef-core/property-configuration-part2","01.csharp/10.ef-core/09.property-configuration-part2",{"title":503,"path":504,"stem":505},"Складні типи — Owned Types та Complex Types (Частина 1)","/csharp/ef-core/complex-types-owned-part1","01.csharp/10.ef-core/10.complex-types-owned-part1",{"title":507,"path":508,"stem":509},"Складні типи — Complex Types, Keyless Entities, Порівняння (Частина 2)","/csharp/ef-core/complex-types-owned-part2","01.csharp/10.ef-core/10.complex-types-owned-part2",{"title":511,"path":512,"stem":513},"JSON Columns — Складні дані у JSON (Частина 1)","/csharp/ef-core/json-columns-part1","01.csharp/10.ef-core/11.json-columns-part1",{"title":515,"path":516,"stem":517},"JSON Columns — Value Comparers, Індекси, Провайдери (Частина 2)","/csharp/ef-core/json-columns-part2","01.csharp/10.ef-core/11.json-columns-part2",{"title":519,"path":520,"stem":521},"Успадкування — Абстрактні класи та TPH (Частина 1)","/csharp/ef-core/inheritance-part1","01.csharp/10.ef-core/12.inheritance-part1",{"title":523,"path":524,"stem":525},"Успадкування — TPT, TPC та Порівняння Стратегій (Частина 2)","/csharp/ef-core/inheritance-part2","01.csharp/10.ef-core/12.inheritance-part2",{"title":527,"path":528,"stem":529,"children":530},"Індекси, Обмеження та Схема (Частина 1)","/csharp/ef-core/indexes-constraints-part1","01.csharp/10.ef-core/13.indexes-constraints-part1",[531],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"Індекси, Обмеження та Схема (Частина 2)","/csharp/ef-core/indexes-constraints-part2","01.csharp/10.ef-core/13.indexes-constraints-part2",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541},"Seed Data — Початкові Дані (Частина 1)","/csharp/ef-core/seeding-part1","01.csharp/10.ef-core/14.seeding-part1",{"title":543,"path":544,"stem":545},"Seed Data — SQL-скрипти, Bogus та Стратегії (Частина 2)","/csharp/ef-core/seeding-part2","01.csharp/10.ef-core/14.seeding-part2",{"title":547,"path":548,"stem":549},"Global Query Filters — Глобальні Фільтри (Частина 1)","/csharp/ef-core/global-query-filters-part1","01.csharp/10.ef-core/15.global-query-filters-part1",{"title":551,"path":552,"stem":553},"Global Query Filters — Підводні камені та Інтеграція (Частина 2)","/csharp/ef-core/global-query-filters-part2","01.csharp/10.ef-core/15.global-query-filters-part2",{"title":555,"path":556,"stem":557},"LINQ-запити в EF Core (Частина 1)","/csharp/ef-core/linq-queries-part1","01.csharp/10.ef-core/16.linq-queries-part1",{"title":559,"path":560,"stem":561},"LINQ-запити в EF Core (Частина 2)","/csharp/ef-core/linq-queries-part2","01.csharp/10.ef-core/16.linq-queries-part2",{"title":563,"path":564,"stem":565},"Завантаження Пов'язаних Даних (Частина 1)","/csharp/ef-core/loading-related-data-part1","01.csharp/10.ef-core/17.loading-related-data-part1",{"title":567,"path":568,"stem":569},"Завантаження Пов'язаних Даних (Частина 2)","/csharp/ef-core/loading-related-data-part2","01.csharp/10.ef-core/17.loading-related-data-part2",{"title":571,"path":572,"stem":573},"Raw SQL, Views та Stored Procedures (Частина 1)","/csharp/ef-core/raw-sql-part1","01.csharp/10.ef-core/18.raw-sql-part1",{"title":575,"path":576,"stem":577},"Raw SQL — Stored Procedures, DbFunction та Bulk Operations (Частина 2)","/csharp/ef-core/raw-sql-part2","01.csharp/10.ef-core/18.raw-sql-part2",{"title":579,"path":580,"stem":581},"Продвинуті Запити — Compiled Queries, Bulk та Оптимізація (Частина 1)","/csharp/ef-core/advanced-queries-part1","01.csharp/10.ef-core/19.advanced-queries-part1",{"title":583,"path":584,"stem":585},"Продвинуті Запити — Query Tags, Bulk та Interceptors (Частина 2)","/csharp/ef-core/advanced-queries-part2","01.csharp/10.ef-core/19.advanced-queries-part2",{"title":587,"path":588,"stem":589},"Change Tracker — Відстеження Змін (Частина 1)","/csharp/ef-core/change-tracking-part1","01.csharp/10.ef-core/20.change-tracking-part1",{"title":591,"path":592,"stem":593},"Change Tracker — Графи Об'єктів та Disconnected (Частина 2)","/csharp/ef-core/change-tracking-part2","01.csharp/10.ef-core/20.change-tracking-part2",{"title":595,"path":596,"stem":597},"Збереження Даних та Транзакції (Частина 1)","/csharp/ef-core/saving-data-part1","01.csharp/10.ef-core/21.saving-data-part1",{"title":599,"path":600,"stem":601},"Збереження Даних — Concurrency та Outbox (Частина 2)","/csharp/ef-core/saving-data-part2","01.csharp/10.ef-core/21.saving-data-part2",{"title":603,"path":604,"stem":605},"Конкурентність та Блокування (Частина 1)","/csharp/ef-core/concurrency-part1","01.csharp/10.ef-core/22.concurrency-part1",{"title":607,"path":608,"stem":609},"Конкурентність — Дедлоки та Queue Processing (Частина 2)","/csharp/ef-core/concurrency-part2","01.csharp/10.ef-core/22.concurrency-part2",{"title":611,"path":612,"stem":613},"Міграції в EF Core — Основи (Частина 1)","/csharp/ef-core/migrations-basics-part1","01.csharp/10.ef-core/23.migrations-basics-part1",{"title":615,"path":616,"stem":617},"Міграції в EF Core — Основи (Частина 2)","/csharp/ef-core/migrations-basics-part2","01.csharp/10.ef-core/23.migrations-basics-part2",{"title":619,"path":620,"stem":621},"Міграції — Просунуті Сценарії (Частина 1)","/csharp/ef-core/migrations-advanced-part1","01.csharp/10.ef-core/24.migrations-advanced-part1",{"title":623,"path":624,"stem":625},"Міграції — Просунуті Сценарії (Частина 2)","/csharp/ef-core/migrations-advanced-part2","01.csharp/10.ef-core/24.migrations-advanced-part2",{"title":627,"path":628,"stem":629},"Управління Схемою та Database-First (Частина 1)","/csharp/ef-core/schema-management-part1","01.csharp/10.ef-core/25.schema-management-part1",{"title":631,"path":632,"stem":633},"Управління Схемою та Database-First (Частина 2)","/csharp/ef-core/schema-management-part2","01.csharp/10.ef-core/25.schema-management-part2",{"title":635,"path":636,"stem":637},"Продуктивність EF Core — Основи (Частина 1)","/csharp/ef-core/performance-fundamentals-part1","01.csharp/10.ef-core/26.performance-fundamentals-part1",{"title":639,"path":640,"stem":641},"Interceptors в EF Core (Частина 1)","/csharp/ef-core/interceptors-part1","01.csharp/10.ef-core/29.interceptors-part1",{"title":643,"path":644,"stem":645},"Interceptors в EF Core — Connection, Transaction та Materialization (Частина 2)","/csharp/ef-core/interceptors-part2","01.csharp/10.ef-core/29.interceptors-part2",{"title":647,"path":648,"stem":649},"План вивчення Entity Framework Core — Повний курс","/csharp/ef-core/implementation_plan","01.csharp/10.ef-core/implementation_plan",{"title":651,"icon":652,"path":653,"stem":654,"children":655,"page":59},"ASP.NET","i-devicon-dotnetcore","/csharp/aspnet","01.csharp/11.aspnet",[656,730,791,869,927,941,967,1057,1111,1182,1212,1289],{"title":657,"icon":658,"path":659,"stem":660,"children":661,"page":59},"Minimal API","i-lucide-network","/csharp/aspnet/minimal-api","01.csharp/11.aspnet/01.minimal-api",[662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726],{"title":663,"path":664,"stem":665},"Вступ до ASP.NET та еволюція фреймворку","/csharp/aspnet/minimal-api/introduction","01.csharp/11.aspnet/01.minimal-api/01.introduction",{"title":667,"path":668,"stem":669},"Перший додаток на ASP.NET Core","/csharp/aspnet/minimal-api/first-application","01.csharp/11.aspnet/01.minimal-api/02.first-application",{"title":671,"path":672,"stem":673},"WebApplication, Builder та Dependency Injection","/csharp/aspnet/minimal-api/webapplication-builder","01.csharp/11.aspnet/01.minimal-api/03.webapplication-builder",{"title":675,"path":676,"stem":677},"Конвеєр запитів та Middleware","/csharp/aspnet/minimal-api/request-pipeline-middleware","01.csharp/11.aspnet/01.minimal-api/04.request-pipeline-middleware",{"title":679,"path":680,"stem":681},"Маршрутизація в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/routing-basics","01.csharp/11.aspnet/01.minimal-api/05.routing-basics",{"title":683,"path":684,"stem":685},"Маршрутизація в ASP.NET Core: Розширені можливості","/csharp/aspnet/minimal-api/routing-advanced","01.csharp/11.aspnet/01.minimal-api/06.routing-advanced",{"title":687,"path":688,"stem":689},"Статичні файли в ASP.NET Core","/csharp/aspnet/minimal-api/static-files","01.csharp/11.aspnet/01.minimal-api/07.static-files",{"title":691,"path":692,"stem":693},"Статичні Активи: MapStaticAssets (ASP.NET Core 9.0)","/csharp/aspnet/minimal-api/static-assets","01.csharp/11.aspnet/01.minimal-api/08.static-assets",{"title":695,"path":696,"stem":697},"Конфігурація в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/configuration-fundamentals","01.csharp/11.aspnet/01.minimal-api/09.configuration-fundamentals",{"title":699,"path":700,"stem":701},"Конфігурація: Паттерн Options","/csharp/aspnet/minimal-api/configuration-options","01.csharp/11.aspnet/01.minimal-api/10.configuration-options",{"title":703,"path":704,"stem":705},"Логування в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/logging-basics","01.csharp/11.aspnet/01.minimal-api/11.logging-basics",{"title":707,"path":708,"stem":709},"Логування: Serilog та Middleware","/csharp/aspnet/minimal-api/logging-advanced","01.csharp/11.aspnet/01.minimal-api/12.logging-advanced",{"title":711,"path":712,"stem":713},"Управління станом: HttpContext.Items та Cookies","/csharp/aspnet/minimal-api/state-management","01.csharp/11.aspnet/01.minimal-api/13.state-management",{"title":715,"path":716,"stem":717},"Стан сесії: Sessions","/csharp/aspnet/minimal-api/session-state","01.csharp/11.aspnet/01.minimal-api/14.session-state",{"title":719,"path":720,"stem":721},"Структура проєкту: від хаосу до архітектури","/csharp/aspnet/minimal-api/project-structure","01.csharp/11.aspnet/01.minimal-api/15.project-structure",{"title":723,"path":724,"stem":725},"Scalar у Minimal API: повний проєкт і Fluent OpenAPI","/csharp/aspnet/minimal-api/scalar-openapi-fluent","01.csharp/11.aspnet/01.minimal-api/16.scalar-openapi-fluent",{"title":727,"path":728,"stem":729},"Swagger / Swashbuckle у Minimal API: окремий класичний шлях","/csharp/aspnet/minimal-api/swagger-swashbuckle","01.csharp/11.aspnet/01.minimal-api/17.swagger-swashbuckle",{"title":731,"icon":658,"path":732,"stem":733,"children":734,"page":59},"API","/csharp/aspnet/api","01.csharp/11.aspnet/02.api",[735,739,743,747,751,755,759,763,767,771,775,779,783,787],{"title":736,"path":737,"stem":738},"Що таке API. Клієнт-серверна архітектура","/csharp/aspnet/api/what-is-api","01.csharp/11.aspnet/02.api/01.what-is-api",{"title":740,"path":741,"stem":742},"Формати даних: JSON, XML, TOML та бінарні формати","/csharp/aspnet/api/data-formats","01.csharp/11.aspnet/02.api/02.data-formats",{"title":744,"path":745,"stem":746},"Парадигми API та концепція REST","/csharp/aspnet/api/api-paradigms-rest","01.csharp/11.aspnet/02.api/03.api-paradigms-rest",{"title":748,"path":749,"stem":750},"HTTP-методи, статус-коди та заголовки","/csharp/aspnet/api/http-methods-status-codes","01.csharp/11.aspnet/02.api/04.http-methods-status-codes",{"title":752,"path":753,"stem":754},"Організація HTTP API за принципами REST","/csharp/aspnet/api/rest-organizing","01.csharp/11.aspnet/02.api/05.rest-organizing",{"title":756,"path":757,"stem":758},"Номенклатура URL та CRUD-операції","/csharp/aspnet/api/url-nomenclature-crud","01.csharp/11.aspnet/02.api/06.url-nomenclature-crud",{"title":760,"path":761,"stem":762},"Правила дизайну: іменування та стандарти","/csharp/aspnet/api/api-design-naming","01.csharp/11.aspnet/02.api/07.api-design-naming",{"title":764,"path":765,"stem":766},"Валідація, ліміти та обробка помилок","/csharp/aspnet/api/api-design-validation","01.csharp/11.aspnet/02.api/08.api-design-validation",{"title":768,"path":769,"stem":770},"Обробка помилок у Minimal API","/csharp/aspnet/api/error-handling-http","01.csharp/11.aspnet/02.api/09.error-handling-http",{"title":772,"path":773,"stem":774},"Ідемпотентність та синхронізація стану","/csharp/aspnet/api/idempotency-sync","01.csharp/11.aspnet/02.api/10.idempotency-sync",{"title":776,"path":777,"stem":778},"Пагінація та організація списків","/csharp/aspnet/api/pagination-lists","01.csharp/11.aspnet/02.api/11.pagination-lists",{"title":780,"path":781,"stem":782},"Безпека API, кешування та інтернаціоналізація","/csharp/aspnet/api/security-auth","01.csharp/11.aspnet/02.api/12.security-auth",{"title":784,"path":785,"stem":786},"Процес проєктування API та документування","/csharp/aspnet/api/api-design-process","01.csharp/11.aspnet/02.api/13.api-design-process",{"title":788,"path":789,"stem":790},"OpenAPI: контракт, специфікація та документація API","/csharp/aspnet/api/openapi","01.csharp/11.aspnet/02.api/14.openapi",{"title":792,"icon":793,"path":794,"stem":795,"children":796,"page":59},"Auth","i-lucide-shield-check","/csharp/aspnet/auth","01.csharp/11.aspnet/03.auth",[797,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865],{"title":798,"path":799,"stem":800},"Основи аутентифікації та авторизації","/csharp/aspnet/auth/auth-fundamentals","01.csharp/11.aspnet/03.auth/01.auth-fundamentals",{"title":802,"path":803,"stem":804},"JWT-аутентифікація","/csharp/aspnet/auth/jwt-authentication","01.csharp/11.aspnet/03.auth/02.jwt-authentication",{"title":806,"path":807,"stem":808},"Авторизація: ролі, політики та resource-based доступ","/csharp/aspnet/auth/authorization-policies","01.csharp/11.aspnet/03.auth/03.authorization-policies",{"title":810,"path":811,"stem":812},"Cookie-аутентифікація та ASP.NET Core Identity","/csharp/aspnet/auth/cookie-auth-identity","01.csharp/11.aspnet/03.auth/04.cookie-auth-identity",{"title":814,"path":815,"stem":816},"JWT + Refresh Tokens (HttpOnly Cookie)","/csharp/aspnet/auth/04b.identity-auth-jwt","01.csharp/11.aspnet/03.auth/04b.identity-auth-jwt",{"title":818,"path":819,"stem":820},"Identity: Підтвердження Email та Скидання Пароля","/csharp/aspnet/auth/identity-email-confirmation","01.csharp/11.aspnet/03.auth/05.identity-email-confirmation",{"title":822,"path":823,"stem":824},"Identity: Двофакторна Аутентифікація (2FA)","/csharp/aspnet/auth/identity-two-factor","01.csharp/11.aspnet/03.auth/06.identity-two-factor",{"title":826,"path":827,"stem":828},"Identity: Внутрішня Архітектура та Кастомізація","/csharp/aspnet/auth/identity-internals","01.csharp/11.aspnet/03.auth/07.identity-internals",{"title":830,"path":831,"stem":832},"OAuth 2.0 та зовнішні провайдери","/csharp/aspnet/auth/oauth-external-providers","01.csharp/11.aspnet/03.auth/08.oauth-external-providers",{"title":834,"path":835,"stem":836},"Безпека на практиці: CORS, HTTPS та захист від атак","/csharp/aspnet/auth/security-hardening","01.csharp/11.aspnet/03.auth/09.security-hardening",{"title":838,"path":839,"stem":840},"Теорія OAuth 2.0: Поняття, Аналогії та Флоу","/csharp/aspnet/auth/oauth-theory","01.csharp/11.aspnet/03.auth/10.oauth-theory",{"title":842,"path":843,"stem":844},"OIDC, OAuth 2.0 та Keycloak в ASP.NET Core","/csharp/aspnet/auth/oidc-keycloak","01.csharp/11.aspnet/03.auth/10.oidc-keycloak",{"title":846,"path":847,"stem":848},"API Keys аутентифікація в ASP.NET Core","/csharp/aspnet/auth/api-keys","01.csharp/11.aspnet/03.auth/11.api-keys",{"title":850,"path":851,"stem":852},"Rate Limiting та Throttling в ASP.NET Core","/csharp/aspnet/auth/rate-limiting","01.csharp/11.aspnet/03.auth/12.rate-limiting",{"title":854,"path":855,"stem":856},"Refresh Token Rotation в ASP.NET Core","/csharp/aspnet/auth/refresh-token-rotation","01.csharp/11.aspnet/03.auth/13.refresh-token-rotation",{"title":858,"path":859,"stem":860},"Certificate Authentication та mTLS в ASP.NET Core","/csharp/aspnet/auth/certificate-auth","01.csharp/11.aspnet/03.auth/14.certificate-auth",{"title":862,"path":863,"stem":864},"RBAC, ABAC та ReBAC в ASP.NET Core","/csharp/aspnet/auth/rbac-abac-rebac","01.csharp/11.aspnet/03.auth/15.rbac-abac-rebac",{"title":866,"path":867,"stem":868},"Multi-tenancy та ізоляція даних в ASP.NET Core","/csharp/aspnet/auth/multi-tenancy","01.csharp/11.aspnet/03.auth/16.multi-tenancy",{"title":870,"icon":871,"path":872,"stem":873,"children":874,"page":59},"Нотифікації","i-lucide-bell","/csharp/aspnet/notifications","01.csharp/11.aspnet/04.notifications",[875,879,883,887,891,895,899,903,907,911,915,919,923],{"title":876,"path":877,"stem":878},"In-App нотифікації через базу даних","/csharp/aspnet/notifications/in-app-database-notifications","01.csharp/11.aspnet/04.notifications/01.in-app-database-notifications",{"title":880,"path":881,"stem":882},"Polling: Регулярний запит оновлень","/csharp/aspnet/notifications/polling","01.csharp/11.aspnet/04.notifications/02.polling",{"title":884,"path":885,"stem":886},"Server-Sent Events: Однострімовий push від сервера","/csharp/aspnet/notifications/server-sent-events","01.csharp/11.aspnet/04.notifications/03.server-sent-events",{"title":888,"path":889,"stem":890},"WebSockets: Двостороннє з'єднання в реальному часі","/csharp/aspnet/notifications/websockets","01.csharp/11.aspnet/04.notifications/04.websockets",{"title":892,"path":893,"stem":894},"SignalR: Абстракція над транспортами реального часу","/csharp/aspnet/notifications/signalr","01.csharp/11.aspnet/04.notifications/05.signalr",{"title":896,"path":897,"stem":898},"Background Services: Фонові задачі в ASP.NET Core","/csharp/aspnet/notifications/background-services","01.csharp/11.aspnet/04.notifications/06.background-services",{"title":900,"path":901,"stem":902},"Web Push нотифікації","/csharp/aspnet/notifications/web-push","01.csharp/11.aspnet/04.notifications/07.web-push",{"title":904,"path":905,"stem":906},"Email нотифікації","/csharp/aspnet/notifications/email-notifications","01.csharp/11.aspnet/04.notifications/08.email-notifications",{"title":908,"path":909,"stem":910},"Порівняння підходів: Як вибрати правильну технологію нотифікацій","/csharp/aspnet/notifications/choosing-the-right-approach","01.csharp/11.aspnet/04.notifications/09.choosing-the-right-approach",{"title":912,"path":913,"stem":914},"Hangfire: Надійне планування фонових задач","/csharp/aspnet/notifications/hangfire","01.csharp/11.aspnet/04.notifications/10.hangfire",{"title":916,"path":917,"stem":918},"Практика: Конвертація зображень у WebP через Hangfire","/csharp/aspnet/notifications/hangfire-image-webp","01.csharp/11.aspnet/04.notifications/11.hangfire-image-webp",{"title":920,"path":921,"stem":922},"Практика: Підготовка відео до HLS-стрімінгу через Hangfire","/csharp/aspnet/notifications/hangfire-video-hls","01.csharp/11.aspnet/04.notifications/12.hangfire-video-hls",{"title":924,"path":925,"stem":926},"Telegram-нотифікації: від одного повідомлення до масових розсилок і мульти-канального підходу","/csharp/aspnet/notifications/telegram-notifications","01.csharp/11.aspnet/04.notifications/13.telegram-notifications",{"title":928,"icon":929,"path":930,"stem":931,"children":932,"page":59},"Інтернаціоналізація","i-lucide-languages","/csharp/aspnet/i18n","01.csharp/11.aspnet/05.i18n",[933,937],{"title":934,"path":935,"stem":936},"Інтернаціоналізація (i18n) у Minimal API: від A до Я","/csharp/aspnet/i18n/internationalization","01.csharp/11.aspnet/05.i18n/01.internationalization",{"title":938,"path":939,"stem":940},"Humanizer: людиномовні рядки у .NET","/csharp/aspnet/i18n/humanizer","01.csharp/11.aspnet/05.i18n/02.humanizer",{"title":942,"icon":943,"path":944,"stem":945,"children":946,"page":59},"Кешування","i-lucide-layers","/csharp/aspnet/caching","01.csharp/11.aspnet/06.caching",[947,951,955,959,963],{"title":948,"path":949,"stem":950},"Огляд кешування: чотири рівні і коли що обирати","/csharp/aspnet/caching/caching","01.csharp/11.aspnet/06.caching/01.caching",{"title":952,"path":953,"stem":954},"IMemoryCache: кеш в оперативній пам'яті","/csharp/aspnet/caching/memory-cache","01.csharp/11.aspnet/06.caching/02.memory-cache",{"title":956,"path":957,"stem":958},"IDistributedCache і Redis: розподілений кеш","/csharp/aspnet/caching/distributed-cache","01.csharp/11.aspnet/06.caching/03.distributed-cache",{"title":960,"path":961,"stem":962},"Response Cache: HTTP-кешування через Cache-Control","/csharp/aspnet/caching/response-cache","01.csharp/11.aspnet/06.caching/04.response-cache",{"title":964,"path":965,"stem":966},"Output Cache: серверний кеш HTTP-відповідей (.NET 7+)","/csharp/aspnet/caching/output-cache","01.csharp/11.aspnet/06.caching/05.output-cache",{"title":968,"icon":969,"path":970,"stem":971,"children":972,"page":59},"Тестування","i-lucide-test-tube","/csharp/aspnet/testing","01.csharp/11.aspnet/07.testing",[973,977,981,985,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1033,1037,1041,1045,1049,1053],{"title":974,"path":975,"stem":976},"Що таке тестування? Від інтуїції до науки","/csharp/aspnet/testing/what-is-testing","01.csharp/11.aspnet/07.testing/01.what-is-testing",{"title":978,"path":979,"stem":980},"Піраміда тестування — Стратегія, а не Догма","/csharp/aspnet/testing/testing-pyramid","01.csharp/11.aspnet/07.testing/02.testing-pyramid",{"title":982,"path":983,"stem":984},"Дві Школи Тестування — Лондон проти Детройту","/csharp/aspnet/testing/testing-schools","01.csharp/11.aspnet/07.testing/03.testing-schools",{"title":986,"path":987,"stem":988},"TDD та BDD — Тести як Дизайн-інструмент","/csharp/aspnet/testing/tdd-and-bdd","01.csharp/11.aspnet/07.testing/04.tdd-and-bdd",{"title":990,"path":991,"stem":992},"Що саме тестувати — Техніки аналізу та Циклomatична складність","/csharp/aspnet/testing/what-to-test","01.csharp/11.aspnet/07.testing/05.what-to-test",{"title":994,"path":995,"stem":996},"Тестові Фреймворки — Навіщо вони і що всередині","/csharp/aspnet/testing/test-frameworks","01.csharp/11.aspnet/07.testing/06.test-frameworks",{"title":998,"path":999,"stem":1000},"xUnit — Факти, Теорії та Lifecycle тестів","/csharp/aspnet/testing/xunit-basics","01.csharp/11.aspnet/07.testing/07.xunit-basics",{"title":1002,"path":1003,"stem":1004},"xUnit Advanced — Fixtures, Кастомізація та Розширення","/csharp/aspnet/testing/xunit-advanced","01.csharp/11.aspnet/07.testing/08.xunit-advanced",{"title":1006,"path":1007,"stem":1008},"Moq — Глибоке занурення в мокування","/csharp/aspnet/testing/mocking-with-moq","01.csharp/11.aspnet/07.testing/09.mocking-with-moq",{"title":1010,"path":1011,"stem":1012},"Тестування Баз Даних — EF Core, SQLite та Testcontainers","/csharp/aspnet/testing/database-testing","01.csharp/11.aspnet/07.testing/10.database-testing",{"title":1014,"path":1015,"stem":1016},"Integration Testing — Частина 1 [Теорія та WebApplicationFactory]","/csharp/aspnet/testing/integration-testing","01.csharp/11.aspnet/07.testing/11.integration-testing",{"title":1018,"path":1019,"stem":1020},"Інтеграційне тестування — Практика","/csharp/aspnet/testing/11a.integration-testing-practice","01.csharp/11.aspnet/07.testing/11a.integration-testing-practice",{"title":1022,"path":1023,"stem":1024},"Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]","/csharp/aspnet/testing/integration-testing-advanced","01.csharp/11.aspnet/07.testing/12.integration-testing-advanced",{"title":1026,"path":1027,"stem":1028},"Професійний Postman: Колекції, Змінні та GitHub Інтеграція","/csharp/aspnet/testing/postman-professional","01.csharp/11.aspnet/07.testing/13.postman-professional",{"title":1030,"path":1031,"stem":1032},"HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler","/csharp/aspnet/testing/httpclient-testing","01.csharp/11.aspnet/07.testing/14.httpclient-testing",{"title":1034,"path":1035,"stem":1036},"HttpClient у Тестах Частина 2: WireMock.Net та Resilience","/csharp/aspnet/testing/wiremock-net","01.csharp/11.aspnet/07.testing/15.wiremock-net",{"title":1038,"path":1039,"stem":1040},"Патерни та Анти-патерни Тестування: Test Smells","/csharp/aspnet/testing/testing-patterns","01.csharp/11.aspnet/07.testing/16.testing-patterns",{"title":1042,"path":1043,"stem":1044},"Просунуті інструменти: Time, Snapshots та Властивості","/csharp/aspnet/testing/advanced-testing-tools","01.csharp/11.aspnet/07.testing/17.advanced-testing-tools",{"title":1046,"path":1047,"stem":1048},"Тестування Архітектури з NetArchTest","/csharp/aspnet/testing/architecture-testing","01.csharp/11.aspnet/07.testing/18.architecture-testing",{"title":1050,"path":1051,"stem":1052},"Тестування Продуктивності: BenchmarkDotNet, NBomber та k6","/csharp/aspnet/testing/performance-testing","01.csharp/11.aspnet/07.testing/19.performance-testing",{"title":1054,"path":1055,"stem":1056},"Залишок плану для курсу \"Тестування ASP.NET Minimal API\"","/csharp/aspnet/testing/remaining_plan","01.csharp/11.aspnet/07.testing/remaining_plan",{"title":1058,"icon":1059,"path":1060,"stem":1061,"children":1062,"page":59},"Платежі","i-lucide-credit-card","/csharp/aspnet/payments","01.csharp/11.aspnet/08.payments",[1063,1067,1071,1075,1079,1083,1087,1091,1095,1099,1103,1107],{"title":1064,"path":1065,"stem":1066},"Основи платіжної інфраструктури","/csharp/aspnet/payments/payment-fundamentals","01.csharp/11.aspnet/08.payments/01.payment-fundamentals",{"title":1068,"path":1069,"stem":1070},"Методи оплати в Україні","/csharp/aspnet/payments/payment-methods-ukraine","01.csharp/11.aspnet/08.payments/02.payment-methods-ukraine",{"title":1072,"path":1073,"stem":1074},"PCI DSS та безпека платежів","/csharp/aspnet/payments/pci-dss-security","01.csharp/11.aspnet/08.payments/03.pci-dss-security",{"title":1076,"path":1077,"stem":1078},"Архітектура платіжної підсистеми","/csharp/aspnet/payments/payment-architecture","01.csharp/11.aspnet/08.payments/04.payment-architecture",{"title":1080,"path":1081,"stem":1082},"Інтеграція LiqPay (ПриватБанк)","/csharp/aspnet/payments/liqpay-integration","01.csharp/11.aspnet/08.payments/05.liqpay-integration",{"title":1084,"path":1085,"stem":1086},"Інтеграція Monobank Acquiring API","/csharp/aspnet/payments/monobank-acquiring","01.csharp/11.aspnet/08.payments/06.monobank-acquiring",{"title":1088,"path":1089,"stem":1090},"Інтеграція Stripe","/csharp/aspnet/payments/stripe-integration","01.csharp/11.aspnet/08.payments/07.stripe-integration",{"title":1092,"path":1093,"stem":1094},"Webhooks — глибоке занурення","/csharp/aspnet/payments/webhooks-deep-dive","01.csharp/11.aspnet/08.payments/08.webhooks-deep-dive",{"title":1096,"path":1097,"stem":1098},"Підписки та рекурентні платежі","/csharp/aspnet/payments/subscriptions-recurring","01.csharp/11.aspnet/08.payments/09.subscriptions-recurring",{"title":1100,"path":1101,"stem":1102},"Повернення коштів та диспути","/csharp/aspnet/payments/refunds-disputes","01.csharp/11.aspnet/08.payments/10.refunds-disputes",{"title":1104,"path":1105,"stem":1106},"Тестування платіжних інтеграцій","/csharp/aspnet/payments/testing-payments","01.csharp/11.aspnet/08.payments/11.testing-payments",{"title":1108,"path":1109,"stem":1110},"Чекліст виходу в Production","/csharp/aspnet/payments/production-checklist","01.csharp/11.aspnet/08.payments/12.production-checklist",{"title":1112,"icon":1113,"items":1114,"path":1127,"stem":1128,"children":1129,"page":59},"Популярні бібліотеки","lucide:box",[1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126],"01.fluent-validation","02.mapster","03.erroror-result-pattern","04.serilog","05.mediatr","06.polly","07.health-checks","08.feature-management","09.fluent-email","10.quest-pdf","11.bogus","12.humanizer-guard","/csharp/aspnet/libraries","01.csharp/11.aspnet/09.libraries",[1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178],{"title":1131,"path":1132,"stem":1133},"Валідація з FluentValidation в ASP.NET Core","/csharp/aspnet/libraries/fluent-validation","01.csharp/11.aspnet/09.libraries/01.fluent-validation",{"title":1135,"path":1136,"stem":1137},"Маппінг об","/csharp/aspnet/libraries/mapster","01.csharp/11.aspnet/09.libraries/02.mapster",{"title":1139,"path":1140,"stem":1141},"Обробка помилок з ErrorOr та Result Pattern в ASP.NET Core","/csharp/aspnet/libraries/erroror-result-pattern","01.csharp/11.aspnet/09.libraries/03.erroror-result-pattern",{"title":1143,"path":1144,"stem":1145},"Структуроване логування з Serilog в ASP.NET Core","/csharp/aspnet/libraries/serilog","01.csharp/11.aspnet/09.libraries/04.serilog",{"title":1147,"path":1148,"stem":1149},"CQRS та Mediator з MediatR в ASP.NET Core","/csharp/aspnet/libraries/mediatr","01.csharp/11.aspnet/09.libraries/05.mediatr",{"title":1151,"path":1152,"stem":1153},"Відмовостійкість з Polly в ASP.NET Core","/csharp/aspnet/libraries/polly","01.csharp/11.aspnet/09.libraries/06.polly",{"title":1155,"path":1156,"stem":1157},"Health Checks в ASP.NET Core","/csharp/aspnet/libraries/health-checks","01.csharp/11.aspnet/09.libraries/07.health-checks",{"title":1159,"path":1160,"stem":1161},"Feature Management та Feature Flags в ASP.NET Core","/csharp/aspnet/libraries/feature-management","01.csharp/11.aspnet/09.libraries/08.feature-management",{"title":1163,"path":1164,"stem":1165},"Відправка Email з FluentEmail в ASP.NET Core","/csharp/aspnet/libraries/fluent-email","01.csharp/11.aspnet/09.libraries/09.fluent-email",{"title":1167,"path":1168,"stem":1169},"Генерація PDF з QuestPDF в ASP.NET Core","/csharp/aspnet/libraries/quest-pdf","01.csharp/11.aspnet/09.libraries/10.quest-pdf",{"title":1171,"path":1172,"stem":1173},"Генерація тестових даних з Bogus в ASP.NET Core","/csharp/aspnet/libraries/bogus","01.csharp/11.aspnet/09.libraries/11.bogus",{"title":1175,"path":1176,"stem":1177},"Humanizer та Guard Clauses в ASP.NET Core","/csharp/aspnet/libraries/humanizer-guard","01.csharp/11.aspnet/09.libraries/12.humanizer-guard",{"title":1179,"path":1180,"stem":1181},"План модуля 10.libraries — Популярні бібліотеки ASP.NET","/csharp/aspnet/libraries/plan","01.csharp/11.aspnet/09.libraries/plan",{"title":1183,"icon":1184,"path":1185,"stem":1186,"children":1187,"page":59},"Razor Pages","i-lucide-layout-template","/csharp/aspnet/razor-pages","01.csharp/11.aspnet/10.razor-pages",[1188,1192,1196,1200,1204,1208],{"title":1189,"path":1190,"stem":1191},"Від Minimal API до Razor Pages: концептуальний перехід","/csharp/aspnet/razor-pages/from-minimal-api","01.csharp/11.aspnet/10.razor-pages/01.from-minimal-api",{"title":1193,"path":1194,"stem":1195},"PageModel: логіка сторінки Razor Pages","/csharp/aspnet/razor-pages/page-model","01.csharp/11.aspnet/10.razor-pages/02.page-model",{"title":1197,"path":1198,"stem":1199},"Razor синтаксис: шаблонізатор у .cshtml","/csharp/aspnet/razor-pages/razor-syntax","01.csharp/11.aspnet/10.razor-pages/03.razor-syntax",{"title":1201,"path":1202,"stem":1203},"Tag Helpers: типізований HTML","/csharp/aspnet/razor-pages/tag-helpers","01.csharp/11.aspnet/10.razor-pages/04.tag-helpers",{"title":1205,"path":1206,"stem":1207},"Форми і валідація: повний цикл обробки даних","/csharp/aspnet/razor-pages/forms-validation","01.csharp/11.aspnet/10.razor-pages/05.forms-validation",{"title":1209,"path":1210,"stem":1211},"Практичний проєкт: TaskManager на Razor Pages","/csharp/aspnet/razor-pages/project-task-manager","01.csharp/11.aspnet/10.razor-pages/06.project-task-manager",{"title":1213,"path":1214,"stem":1215,"children":1216,"page":59},"ASP.NET Core MVC","/csharp/aspnet/mvc","01.csharp/11.aspnet/11.mvc",[1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285],{"title":1218,"path":1219,"stem":1220},"Патерн MVC: архітектура, що змінила веб","/csharp/aspnet/mvc/mvc-pattern","01.csharp/11.aspnet/11.mvc/01.mvc-pattern",{"title":1222,"path":1223,"stem":1224},"Від Razor Pages до MVC: концептуальний перехід","/csharp/aspnet/mvc/from-razor-pages","01.csharp/11.aspnet/11.mvc/02.from-razor-pages",{"title":1226,"path":1227,"stem":1228},"Controllers та Actions: серце MVC","/csharp/aspnet/mvc/controllers-actions","01.csharp/11.aspnet/11.mvc/03.controllers-actions",{"title":1230,"path":1231,"stem":1232},"Маршрутизація в MVC: Convention vs Attribute Routing","/csharp/aspnet/mvc/routing-mvc","01.csharp/11.aspnet/11.mvc/04.routing-mvc",{"title":1234,"path":1235,"stem":1236},"Model Binding: від HTTP до C#","/csharp/aspnet/mvc/model-binding","01.csharp/11.aspnet/11.mvc/05.model-binding",{"title":1238,"path":1239,"stem":1240},"Views, ViewData, ViewBag, TempData і ViewModel","/csharp/aspnet/mvc/views-viewdata-tempdata","01.csharp/11.aspnet/11.mvc/06.views-viewdata-tempdata",{"title":1242,"path":1243,"stem":1244},"Filters: аспектно-орієнтоване програмування в MVC","/csharp/aspnet/mvc/filters","01.csharp/11.aspnet/11.mvc/07.filters",{"title":1246,"path":1247,"stem":1248},"Areas: структурування великих застосунків","/csharp/aspnet/mvc/areas","01.csharp/11.aspnet/11.mvc/08.areas",{"title":1250,"path":1251,"stem":1252},"View Components: повторювані незалежні блоки UI","/csharp/aspnet/mvc/view-components","01.csharp/11.aspnet/11.mvc/09.view-components",{"title":1254,"path":1255,"stem":1256},"Display та Editor Templates","/csharp/aspnet/mvc/display-editor-templates","01.csharp/11.aspnet/11.mvc/10.display-editor-templates",{"title":1258,"path":1259,"stem":1260},"Валідація: IValidatableObject та FluentValidation","/csharp/aspnet/mvc/validation-advanced","01.csharp/11.aspnet/11.mvc/11.validation-advanced",{"title":1262,"path":1263,"stem":1264},"HTMX: інтерактивність через HTML-атрибути","/csharp/aspnet/mvc/htmx","01.csharp/11.aspnet/11.mvc/12.htmx",{"title":1266,"path":1267,"stem":1268},"HTMX у ASP.NET Core MVC: серверна інтеграція","/csharp/aspnet/mvc/ajax-htmx-mvc","01.csharp/11.aspnet/11.mvc/13.ajax-htmx-mvc",{"title":1270,"path":1271,"stem":1272},"Практичний проєкт: Каталог товарів з HTMX","/csharp/aspnet/mvc/htmx-project","01.csharp/11.aspnet/11.mvc/14.htmx-project",{"title":1274,"path":1275,"stem":1276},"Завантаження та обробка файлів","/csharp/aspnet/mvc/file-upload","01.csharp/11.aspnet/11.mvc/15.file-upload",{"title":1278,"path":1279,"stem":1280},"Глобалізація та Локалізація MVC","/csharp/aspnet/mvc/globalization-localization","01.csharp/11.aspnet/11.mvc/16.globalization-localization",{"title":1282,"path":1283,"stem":1284},"Підсумковий проєкт: Блог-платформа","/csharp/aspnet/mvc/mvc-project","01.csharp/11.aspnet/11.mvc/17.mvc-project",{"title":1286,"path":1287,"stem":1288},"План курсу: ASP.NET Core MVC","/csharp/aspnet/mvc/plan","01.csharp/11.aspnet/11.mvc/plan",{"title":1290,"path":1291,"stem":1292,"children":1293,"page":59},"Web Api","/csharp/aspnet/web-api","01.csharp/11.aspnet/12.web-api",[1294,1298,1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342],{"title":1295,"path":1296,"stem":1297},"Від Minimal API до Controller-based API","/csharp/aspnet/web-api/from-minimal-api-to-controllers","01.csharp/11.aspnet/12.web-api/01.from-minimal-api-to-controllers",{"title":1299,"path":1300,"stem":1301},"ControllerBase, ActionResult\u003CT> та Response Types","/csharp/aspnet/web-api/controller-base-actionresult","01.csharp/11.aspnet/12.web-api/02.controller-base-actionresult",{"title":1303,"path":1304,"stem":1305},"Content Negotiation - JSON, XML та власні форматери","/csharp/aspnet/web-api/content-negotiation","01.csharp/11.aspnet/12.web-api/03.content-negotiation",{"title":1307,"path":1308,"stem":1309},"Версіонування API","/csharp/aspnet/web-api/api-versioning","01.csharp/11.aspnet/12.web-api/04.api-versioning",{"title":1311,"path":1312,"stem":1313},"ProblemDetails та структурована обробка помилок","/csharp/aspnet/web-api/problemdetails-error-handling","01.csharp/11.aspnet/12.web-api/05.problemdetails-error-handling",{"title":1315,"path":1316,"stem":1317},"Фільтри у Web API контексті","/csharp/aspnet/web-api/filters-for-api","01.csharp/11.aspnet/12.web-api/06.filters-for-api",{"title":1319,"path":1320,"stem":1321},"Пагінація, фільтрація та сортування","/csharp/aspnet/web-api/pagination-filtering-sorting","01.csharp/11.aspnet/12.web-api/07.pagination-filtering-sorting",{"title":1323,"path":1324,"stem":1325},"HATEOAS та Resource Expansion","/csharp/aspnet/web-api/hateoas-resource-expansion","01.csharp/11.aspnet/12.web-api/08.hateoas-resource-expansion",{"title":1327,"path":1328,"stem":1329},"Гібридна архітектура - Minimal API + Controllers","/csharp/aspnet/web-api/minimal-api-vs-controllers-hybrid","01.csharp/11.aspnet/12.web-api/09.minimal-api-vs-controllers-hybrid",{"title":1331,"path":1332,"stem":1333},"Документація API - Swashbuckle, NSwag та генерація клієнтів","/csharp/aspnet/web-api/api-documentation-generation","01.csharp/11.aspnet/12.web-api/10.api-documentation-generation",{"title":1335,"path":1336,"stem":1337},"Health Checks та моніторинг API","/csharp/aspnet/web-api/health-checks-monitoring","01.csharp/11.aspnet/12.web-api/11.health-checks-monitoring",{"title":1339,"path":1340,"stem":1341},"Підсумковий проєкт - Production-Ready REST API","/csharp/aspnet/web-api/web-api-project","01.csharp/11.aspnet/12.web-api/12.web-api-project",{"title":1343,"path":1344,"stem":1345},"План курсу: ASP.NET Core Web API (Controllers)","/csharp/aspnet/web-api/plan","01.csharp/11.aspnet/12.web-api/plan",{"title":1347,"icon":1348,"path":1349,"stem":1350,"children":1351,"page":59},"Desktop UI","i-lucide-app-window","/csharp/desktop-ui","01.csharp/12.desktop-ui",[1352,1356,1360,1364,1368,1372,1376,1380,1384,1388,1392,1396,1400,1404,1408,1412,1416,1420,1424,1428,1432,1436,1440,1444,1448,1452,1456,1460,1464,1468,1472,1476,1480,1484,1488,1492,1496,1500,1504,1508,1512,1516,1520,1524,1528,1532,1536,1540,1544,1548,1552,1556,1560,1564,1568,1572,1576,1580,1584,1588,1592,1596,1600,1604,1608,1612,1616,1620,1624,1628,1632],{"title":1353,"path":1354,"stem":1355},"Що таке десктопна розробка?","/csharp/desktop-ui/what-is-desktop-dev","01.csharp/12.desktop-ui/01.what-is-desktop-dev",{"title":1357,"path":1358,"stem":1359},"Архітектура WPF — як влаштований графічний інтерфейс","/csharp/desktop-ui/wpf-architecture","01.csharp/12.desktop-ui/02.wpf-architecture",{"title":1361,"path":1362,"stem":1363},"Перший WPF-проєкт — від нуля до вікна","/csharp/desktop-ui/first-wpf-app","01.csharp/12.desktop-ui/03.first-wpf-app",{"title":1365,"path":1366,"stem":1367},"Перший Avalonia-проєкт: WPF для всіх платформ","/csharp/desktop-ui/03a.first-avalonia-app","01.csharp/12.desktop-ui/03a.first-avalonia-app",{"title":1369,"path":1370,"stem":1371},"XAML: декларативний інтерфейс","/csharp/desktop-ui/xaml-basics","01.csharp/12.desktop-ui/04.xaml-basics",{"title":1373,"path":1374,"stem":1375},"Fluent UI у WPF — сучасний дизайн Windows 11","/csharp/desktop-ui/04a.wpf-fluent-ui","01.csharp/12.desktop-ui/04a.wpf-fluent-ui",{"title":1377,"path":1378,"stem":1379},"WPF UI — сучасна бібліотека Fluent контролів","/csharp/desktop-ui/04b.wpf-ui-library","01.csharp/12.desktop-ui/04b.wpf-ui-library",{"title":1381,"path":1382,"stem":1383},"HandyControl — велика бібліотека UI контролів для WPF","/csharp/desktop-ui/04c.handycontrol-library","01.csharp/12.desktop-ui/04c.handycontrol-library",{"title":1385,"path":1386,"stem":1387},"Простори імен та ресурси XAML","/csharp/desktop-ui/xaml-namespaces-resources","01.csharp/12.desktop-ui/05.xaml-namespaces-resources",{"title":1389,"path":1390,"stem":1391},"XAML в Avalonia: ключові відмінності від WPF","/csharp/desktop-ui/05a.avalonia-xaml-differences","01.csharp/12.desktop-ui/05a.avalonia-xaml-differences",{"title":1393,"path":1394,"stem":1395},"Розширення розмітки XAML (Markup Extensions)","/csharp/desktop-ui/xaml-markup-extensions","01.csharp/12.desktop-ui/06.xaml-markup-extensions",{"title":1397,"path":1398,"stem":1399},"Панелі Layout: StackPanel, WrapPanel, DockPanel","/csharp/desktop-ui/layout-panels-part1","01.csharp/12.desktop-ui/07.layout-panels-part1",{"title":1401,"path":1402,"stem":1403},"Grid, Canvas, UniformGrid","/csharp/desktop-ui/layout-panels-part2","01.csharp/12.desktop-ui/07.layout-panels-part2",{"title":1405,"path":1406,"stem":1407},"Просунуті техніки Layout","/csharp/desktop-ui/layout-advanced","01.csharp/12.desktop-ui/08.layout-advanced",{"title":1409,"path":1410,"stem":1411},"Адаптивний Layout та найкращі практики","/csharp/desktop-ui/layout-responsive","01.csharp/12.desktop-ui/09.layout-responsive",{"title":1413,"path":1414,"stem":1415},"Layout в Avalonia: відмінності та нові можливості","/csharp/desktop-ui/09a.layout-avalonia","01.csharp/12.desktop-ui/09a.layout-avalonia",{"title":1417,"path":1418,"stem":1419},"Button, Image, ProgressBar та інші базові контроли","/csharp/desktop-ui/basic-controls","01.csharp/12.desktop-ui/10.basic-controls",{"title":1421,"path":1422,"stem":1423},"Контроли в Avalonia: відмінності від WPF","/csharp/desktop-ui/10a.controls-avalonia","01.csharp/12.desktop-ui/10a.controls-avalonia",{"title":1425,"path":1426,"stem":1427},"Текстові контроли — TextBlock, TextBox, RichTextBox","/csharp/desktop-ui/text-controls","01.csharp/12.desktop-ui/11.text-controls",{"title":1429,"path":1430,"stem":1431},"Контроли вибору — CheckBox, RadioButton, ComboBox, ListBox, DatePicker","/csharp/desktop-ui/selection-controls","01.csharp/12.desktop-ui/12.selection-controls",{"title":1433,"path":1434,"stem":1435},"Content Model — GroupBox, Expander, TabControl, StatusBar","/csharp/desktop-ui/content-controls","01.csharp/12.desktop-ui/13.content-controls",{"title":1437,"path":1438,"stem":1439},"UI/UX принципи десктопних застосунків","/csharp/desktop-ui/13a.ui-ux-principles","01.csharp/12.desktop-ui/13a.ui-ux-principles",{"title":1441,"path":1442,"stem":1443},"Dependency Properties — Концепція та Value Resolution","/csharp/desktop-ui/dependency-properties-part1","01.csharp/12.desktop-ui/14.dependency-properties-part1",{"title":1445,"path":1446,"stem":1447},"Avalonia Property System — StyledProperty та DirectProperty","/csharp/desktop-ui/14a.avalonia-property-system","01.csharp/12.desktop-ui/14a.avalonia-property-system",{"title":1449,"path":1450,"stem":1451},"Attached Properties — Властивості без меж","/csharp/desktop-ui/attached-properties","01.csharp/12.desktop-ui/15.attached-properties",{"title":1453,"path":1454,"stem":1455},"Routed Events — Маршрутизація подій у WPF","/csharp/desktop-ui/routed-events","01.csharp/12.desktop-ui/16.routed-events",{"title":1457,"path":1458,"stem":1459},"Data Binding — Від Code-Behind до Декларативності","/csharp/desktop-ui/data-binding-basics-part1","01.csharp/12.desktop-ui/17.data-binding-basics-part1",{"title":1461,"path":1462,"stem":1463},"INotifyPropertyChanged — Живе оновлення UI","/csharp/desktop-ui/data-binding-basics-part2","01.csharp/12.desktop-ui/17.data-binding-basics-part2",{"title":1465,"path":1466,"stem":1467},"Compiled Bindings в Avalonia — Безпека на етапі компіляції","/csharp/desktop-ui/17a.avalonia-compiled-bindings","01.csharp/12.desktop-ui/17a.avalonia-compiled-bindings",{"title":1469,"path":1470,"stem":1471},"Просунутий Data Binding — ElementName, RelativeSource, MultiBinding","/csharp/desktop-ui/data-binding-advanced","01.csharp/12.desktop-ui/18.data-binding-advanced",{"title":1473,"path":1474,"stem":1475},"Value Converters — Перетворення типів даних у Data Binding","/csharp/desktop-ui/value-converters","01.csharp/12.desktop-ui/19.value-converters",{"title":1477,"path":1478,"stem":1479},"Data Templates — Візуалізація об'єктів у WPF","/csharp/desktop-ui/data-templates","01.csharp/12.desktop-ui/20.data-templates",{"title":1481,"path":1482,"stem":1483},"Collections Binding Part 1 — ObservableCollection та ItemsControl","/csharp/desktop-ui/collections-binding-part1","01.csharp/12.desktop-ui/21.collections-binding-part1",{"title":1485,"path":1486,"stem":1487},"Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization","/csharp/desktop-ui/collections-binding-part2","01.csharp/12.desktop-ui/21.collections-binding-part2",{"title":1489,"path":1490,"stem":1491},"MVVM Pattern — Від Spaghetti Code до архітектури","/csharp/desktop-ui/mvvm-pattern","01.csharp/12.desktop-ui/22.mvvm-pattern",{"title":1493,"path":1494,"stem":1495},"ViewModel Implementation — Від BaseViewModel до валідації","/csharp/desktop-ui/viewmodel-implementation","01.csharp/12.desktop-ui/23.viewmodel-implementation",{"title":1497,"path":1498,"stem":1499},"Commands — Від event handlers до декларативних команд","/csharp/desktop-ui/commands","01.csharp/12.desktop-ui/24.commands",{"title":1501,"path":1502,"stem":1503},"MVVM Toolkit — MVVM без boilerplate через Source Generators","/csharp/desktop-ui/mvvm-toolkit","01.csharp/12.desktop-ui/25.mvvm-toolkit",{"title":1505,"path":1506,"stem":1507},"Messenger Pattern — Комунікація між ViewModel без прямих посилань","/csharp/desktop-ui/messenger-pattern","01.csharp/12.desktop-ui/26.messenger-pattern",{"title":1509,"path":1510,"stem":1511},"Стилі WPF — CSS для десктопу","/csharp/desktop-ui/styles-basics","01.csharp/12.desktop-ui/27.styles-basics",{"title":1513,"path":1514,"stem":1515},"CSS-like стилі Avalonia","/csharp/desktop-ui/27a.avalonia-css-styling","01.csharp/12.desktop-ui/27a.avalonia-css-styling",{"title":1517,"path":1518,"stem":1519},"Control Templates — Частина 1. Концепція та TemplateBinding","/csharp/desktop-ui/control-templates-part1","01.csharp/12.desktop-ui/28.control-templates-part1",{"title":1521,"path":1522,"stem":1523},"Control Templates — Частина 2. Named Parts та ContentPresenter","/csharp/desktop-ui/control-templates-part2","01.csharp/12.desktop-ui/28.control-templates-part2",{"title":1525,"path":1526,"stem":1527},"Control Themes в Avalonia — нова ера стилізації","/csharp/desktop-ui/28a.avalonia-control-themes","01.csharp/12.desktop-ui/28a.avalonia-control-themes",{"title":1529,"path":1530,"stem":1531},"Triggers та Visual State Manager у WPF","/csharp/desktop-ui/triggers-visual-states","01.csharp/12.desktop-ui/29.triggers-visual-states",{"title":1533,"path":1534,"stem":1535},"Pseudo-classes в Avalonia — замість WPF Triggers","/csharp/desktop-ui/29a.avalonia-pseudo-classes","01.csharp/12.desktop-ui/29a.avalonia-pseudo-classes",{"title":1537,"path":1538,"stem":1539},"Теми та ресурсні словники у WPF","/csharp/desktop-ui/resources-themes","01.csharp/12.desktop-ui/30.resources-themes",{"title":1541,"path":1542,"stem":1543},"Avalonia Themes — Fluent Design та система тематизації","/csharp/desktop-ui/30a.avalonia-themes-fluent","01.csharp/12.desktop-ui/30a.avalonia-themes-fluent",{"title":1545,"path":1546,"stem":1547},"Контроли колекцій — глибоке занурення","/csharp/desktop-ui/collection-controls","01.csharp/12.desktop-ui/31.collection-controls",{"title":1549,"path":1550,"stem":1551},"DataGrid — колонки та базове відображення","/csharp/desktop-ui/datagrid-part1","01.csharp/12.desktop-ui/32.datagrid-part1",{"title":1553,"path":1554,"stem":1555},"DataGrid — сортування, фільтрація, редагування","/csharp/desktop-ui/datagrid-part2","01.csharp/12.desktop-ui/32.datagrid-part2",{"title":1557,"path":1558,"stem":1559},"TreeView та GridView","/csharp/desktop-ui/treeview-listview","01.csharp/12.desktop-ui/33.treeview-listview",{"title":1561,"path":1562,"stem":1563},"Меню, Toolbar, ContextMenu, StatusBar","/csharp/desktop-ui/menus-toolbars","01.csharp/12.desktop-ui/34.menus-toolbars",{"title":1565,"path":1566,"stem":1567},"Навігація та керування вікнами. Частина 1: вікна та сторінки","/csharp/desktop-ui/navigation-windows-part1","01.csharp/12.desktop-ui/35.navigation-windows-part1",{"title":1569,"path":1570,"stem":1571},"Навігація та керування вікнами. Частина 2: MVVM-навігація","/csharp/desktop-ui/navigation-windows-part2","01.csharp/12.desktop-ui/35.navigation-windows-part2",{"title":1573,"path":1574,"stem":1575},"Avalonia — Навігація та діалоги","/csharp/desktop-ui/35a.avalonia-navigation-dialogs","01.csharp/12.desktop-ui/35a.avalonia-navigation-dialogs",{"title":1577,"path":1578,"stem":1579},"Діалоги та File Pickers у WPF","/csharp/desktop-ui/dialogs-file-pickers","01.csharp/12.desktop-ui/36.dialogs-file-pickers",{"title":1581,"path":1582,"stem":1583},"UserControl: компонентний підхід у WPF","/csharp/desktop-ui/user-controls","01.csharp/12.desktop-ui/37.user-controls",{"title":1585,"path":1586,"stem":1587},"Custom Controls: Lookless Controls у WPF","/csharp/desktop-ui/custom-controls","01.csharp/12.desktop-ui/38.custom-controls",{"title":1589,"path":1590,"stem":1591},"Avalonia TemplatedControl — Lookless Controls","/csharp/desktop-ui/38a.avalonia-templated-controls","01.csharp/12.desktop-ui/38a.avalonia-templated-controls",{"title":1593,"path":1594,"stem":1595},"Анімації у WPF: Storyboard та Easing Functions","/csharp/desktop-ui/animations-transitions","01.csharp/12.desktop-ui/39.animations-transitions",{"title":1597,"path":1598,"stem":1599},"Анімації в Avalonia","/csharp/desktop-ui/39a.avalonia-animations","01.csharp/12.desktop-ui/39a.avalonia-animations",{"title":1601,"path":1602,"stem":1603},"2D Графіка та Мультимедіа у WPF","/csharp/desktop-ui/media-graphics","01.csharp/12.desktop-ui/40.media-graphics",{"title":1605,"path":1606,"stem":1607},"Dependency Injection у WPF та Avalonia","/csharp/desktop-ui/di-integration","01.csharp/12.desktop-ui/41.di-integration",{"title":1609,"path":1610,"stem":1611},"SQLite та EF Core у десктопних додатках","/csharp/desktop-ui/data-persistence-part1","01.csharp/12.desktop-ui/42.data-persistence-part1",{"title":1613,"path":1614,"stem":1615},"Repository Pattern та Unit of Work","/csharp/desktop-ui/data-persistence-part2","01.csharp/12.desktop-ui/43.data-persistence-part2",{"title":1617,"path":1618,"stem":1619},"Тестування ViewModels","/csharp/desktop-ui/viewmodel-testing","01.csharp/12.desktop-ui/44.viewmodel-testing",{"title":1621,"path":1622,"stem":1623},"Avalonia Headless Testing — тестування UI без вікон","/csharp/desktop-ui/44a.avalonia-headless-testing","01.csharp/12.desktop-ui/44a.avalonia-headless-testing",{"title":1625,"path":1626,"stem":1627},"Кросплатформна розробка з Avalonia","/csharp/desktop-ui/avalonia-cross-platform","01.csharp/12.desktop-ui/45.avalonia-cross-platform",{"title":1629,"path":1630,"stem":1631},"Пакування та розгортання Avalonia додатків","/csharp/desktop-ui/avalonia-packaging-deployment","01.csharp/12.desktop-ui/46.avalonia-packaging-deployment",{"title":1633,"path":1634,"stem":1635},"Розгортання WPF застосунків","/csharp/desktop-ui/wpf-packaging-deployment","01.csharp/12.desktop-ui/47.wpf-packaging-deployment",{"title":1637,"path":1638,"stem":1639},"C# & .NET: The Ultimate Roadmap","/csharp/roadmap","01.csharp/roadmap",{"title":1641,"icon":1642,"path":1643,"stem":1644,"children":1645,"page":59},"C++","i-devicon-cplusplus","/cpp","02.cpp",[1646,1650,1654,1658,1662,1666,1670,1674,1678,1681,1685,1689,1693,1697,1701,1705,1709,1713,1717,1721,1725,1729,1733,1737,1741,1745,1749,1753,1757,1761],{"title":1647,"path":1648,"stem":1649},"Вступ у програмування та алгоритми","/cpp/intro-algorithms","02.cpp/01.intro-algorithms",{"title":1651,"path":1652,"stem":1653},"Code Style: угоди про оформлення коду","/cpp/code-style","02.cpp/02.code-style",{"title":1655,"path":1656,"stem":1657},"Середовище розробки та перший проєкт","/cpp/ide-setup","02.cpp/03.ide-setup",{"title":1659,"path":1660,"stem":1661},"Вивід даних на екран","/cpp/data-output","02.cpp/04.data-output",{"title":1663,"path":1664,"stem":1665},"Типи даних, змінні та константи","/cpp/data-types-variables","02.cpp/05.data-types-variables",{"title":1667,"path":1668,"stem":1669},"Ввід даних з клавіатури","/cpp/data-input","02.cpp/06.data-input",{"title":1671,"path":1672,"stem":1673},"Оператори, перетворення типів та логічні операції","/cpp/operators-type-conversion","02.cpp/07.operators-type-conversion",{"title":1675,"path":1676,"stem":1677},"Цикли","/cpp/loops","02.cpp/08.loops",{"title":32,"path":1679,"stem":1680},"/cpp/arrays","02.cpp/09.arrays",{"title":1682,"path":1683,"stem":1684},"Алгоритми сортування та аналіз складності","/cpp/sorting","02.cpp/10.sorting",{"title":1686,"path":1687,"stem":1688},"Алгоритми пошуку","/cpp/searching","02.cpp/11.searching",{"title":1690,"path":1691,"stem":1692},"Функції: основи","/cpp/functions-basics","02.cpp/12.functions-basics",{"title":1694,"path":1695,"stem":1696},"Функції: прототипи, область видимості та додаткові можливості","/cpp/functions-scope","02.cpp/13.functions-scope",{"title":1698,"path":1699,"stem":1700},"Функції: перевантаження та шаблони","/cpp/functions-overloading-templates","02.cpp/14.functions-overloading-templates",{"title":1702,"path":1703,"stem":1704},"Вказівники: основи","/cpp/pointers-basics","02.cpp/15.pointers-basics",{"title":1706,"path":1707,"stem":1708},"Посилання (References)","/cpp/references","02.cpp/16.references",{"title":1710,"path":1711,"stem":1712},"Вказівники, const і масиви","/cpp/pointers-const-arrays","02.cpp/17.pointers-const-arrays",{"title":1714,"path":1715,"stem":1716},"Адресна арифметика","/cpp/pointer-arithmetic","02.cpp/18.pointer-arithmetic",{"title":1718,"path":1719,"stem":1720},"Динамічна пам'ять","/cpp/dynamic-memory","02.cpp/19.dynamic-memory",{"title":1722,"path":1723,"stem":1724},"Вказівники типу void","/cpp/void-pointers","02.cpp/20.void-pointers",{"title":1726,"path":1727,"stem":1728},"Вказівники на вказівники","/cpp/pointers-to-pointers","02.cpp/21.pointers-to-pointers",{"title":1730,"path":1731,"stem":1732},"Оператор доступу до членів через вказівник (->)","/cpp/member-access-operator","02.cpp/22.member-access-operator",{"title":1734,"path":1735,"stem":1736},"Цикл for-each (Range-based for)","/cpp/foreach-loop","02.cpp/23.foreach-loop",{"title":1738,"path":1739,"stem":1740},"Вказівники на функції","/cpp/function-pointers","02.cpp/24.function-pointers",{"title":1742,"path":1743,"stem":1744},"Лямбда-вирази","/cpp/lambdas","02.cpp/25.lambdas",{"title":1746,"path":1747,"stem":1748},"Лямбда-захоплення","/cpp/lambda-captures","02.cpp/26.lambda-captures",{"title":1750,"path":1751,"stem":1752},"Еліпсис","/cpp/ellipsis","02.cpp/27.ellipsis",{"title":1754,"path":1755,"stem":1756},"Аргументи командного рядка","/cpp/command-line-arguments","02.cpp/28.command-line-arguments",{"title":1758,"path":1759,"stem":1760},"Перерахування (enum)","/cpp/enum","02.cpp/29.enum",{"title":1762,"path":1763,"stem":1764},"План навчання: Курс C++ — Продовження (Статті 29–60+)","/cpp/curriculum-plan","02.cpp/curriculum-plan",{"title":1766,"icon":1767,"path":1768,"stem":1769,"children":1770,"page":59},"JavaScript","i-devicon-javascript","/javascript","03.javascript",[1771,1797,1851,1873,2177,2215],{"title":1772,"icon":1773,"path":1774,"stem":1775,"children":1776,"page":59},"Events","i-lucide-mouse-pointer-click","/javascript/events","03.javascript/01.events",[1777,1781,1785,1789,1793],{"title":1778,"path":1779,"stem":1780},"Вступ до подій браузера","/javascript/events/intro","03.javascript/01.events/01.intro",{"title":1782,"path":1783,"stem":1784},"Бульбашковий механізм (Bubbling) та занурення (Capturing)","/javascript/events/bubbling-capturing","03.javascript/01.events/02.bubbling-capturing",{"title":1786,"path":1787,"stem":1788},"Делегування подій (Event Delegation)","/javascript/events/delegate-events","03.javascript/01.events/03.delegate-events",{"title":1790,"path":1791,"stem":1792},"Типові дії браузера та preventDefault()","/javascript/events/prevent-default","03.javascript/01.events/04.prevent-default",{"title":1794,"path":1795,"stem":1796},"Запуск користувацьких подій (Custom Events)","/javascript/events/custom-events","03.javascript/01.events/05.custom-events",{"title":1798,"icon":1799,"path":1800,"stem":1801,"children":1802,"page":59},"Network","i-lucide-globe","/javascript/network","03.javascript/02.network",[1803,1807,1811,1815,1819,1823,1827,1831,1835,1839,1843,1847],{"title":1804,"path":1805,"stem":1806},"Fetch API - Сучасний підхід до HTTP-запитів","/javascript/network/01-fetch-api","03.javascript/02.network/01-fetch-api",{"title":1808,"path":1809,"stem":1810},"FormData - Робота з формами та файлами","/javascript/network/02-formdata","03.javascript/02.network/02-formdata",{"title":1812,"path":1813,"stem":1814},"Відстеження прогресу завантаження","/javascript/network/03-download-progress","03.javascript/02.network/03-download-progress",{"title":1816,"path":1817,"stem":1818},"Переривання fetch-запитів","/javascript/network/04-abort-requests","03.javascript/02.network/04-abort-requests",{"title":1820,"path":1821,"stem":1822},"CORS - Запити між різними джерелами","/javascript/network/05-cors","03.javascript/02.network/05-cors",{"title":1824,"path":1825,"stem":1826},"Fetch API - Повний довідник опцій","/javascript/network/06-fetch-options","03.javascript/02.network/06-fetch-options",{"title":1828,"path":1829,"stem":1830},"URL Objects - Робота з посиланнями","/javascript/network/07-url-objects","03.javascript/02.network/07-url-objects",{"title":1832,"path":1833,"stem":1834},"XMLHttpRequest - AJAX та низькорівневі запити","/javascript/network/08-xmlhttprequest","03.javascript/02.network/08-xmlhttprequest",{"title":1836,"path":1837,"stem":1838},"Відновлюване завантаження файлів","/javascript/network/09-resumable-upload","03.javascript/02.network/09-resumable-upload",{"title":1840,"path":1841,"stem":1842},"Cookies, document.cookie та світ після \"Cookiepocalypse\"","/javascript/network/10-cookies","03.javascript/02.network/10-cookies",{"title":1844,"path":1845,"stem":1846},"js-cookie: Керування Cookies без Болю","/javascript/network/11-js-cookie","03.javascript/02.network/11-js-cookie",{"title":1848,"path":1849,"stem":1850},"Axios: Потужний HTTP-клієнт для JavaScript","/javascript/network/12-axios","03.javascript/02.network/12-axios",{"title":1852,"icon":1853,"path":1854,"stem":1855,"children":1856,"page":59},"Bom","i-lucide-monitor","/javascript/bom","03.javascript/03.bom",[1857,1861,1865,1869],{"title":1858,"path":1859,"stem":1860},"LocalStorage, SessionStorage та patterns збереження даних","/javascript/bom/01-localstorage","03.javascript/03.bom/01-localstorage",{"title":1862,"path":1863,"stem":1864},"Location Object - Керування адресою сторінки","/javascript/bom/02-location-object","03.javascript/03.bom/02-location-object",{"title":1866,"path":1867,"stem":1868},"History API - Керування історією браузера","/javascript/bom/03-history-api","03.javascript/03.bom/03-history-api",{"title":1870,"path":1871,"stem":1872},"Navigator Object - Ідентифікація та Можливості Пристрою","/javascript/bom/04-navigator-object","03.javascript/03.bom/04-navigator-object",{"title":1874,"icon":1875,"path":1876,"stem":1877,"children":1878},"React","i-devicon-react","/javascript/react","03.javascript/04.react/index",[1879,1880,1884,1888,1892,1896,1959,1994,2146],{"title":1874,"path":1876,"stem":1877},{"title":1881,"path":1882,"stem":1883},"Робота з Формами в React","/javascript/react/react-forms","03.javascript/04.react/01.react-forms",{"title":1885,"path":1886,"stem":1887},"React Hook Form: Професійна Робота з Формами","/javascript/react/react-hook-form","03.javascript/04.react/02.react-hook-form",{"title":1889,"path":1890,"stem":1891},"React Hook Form: Глибоке Розуміння Архітектури та Оптимізації","/javascript/react/react-hook-form-new","03.javascript/04.react/02.react-hook-form-new",{"title":1893,"path":1894,"stem":1895},"Axios та React: Професійна Архітектура Запитів","/javascript/react/data-fetching-axios","03.javascript/04.react/03.data-fetching-axios",{"title":1897,"icon":132,"path":1898,"stem":1899,"children":1900},"Tanstack Query","/javascript/react/tanstack-query","03.javascript/04.react/04.tanstack-query/index",[1901,1903,1907,1911,1915,1919,1923,1927,1931,1935,1939,1943,1947,1951,1955],{"title":1902,"path":1898,"stem":1899},"TanStack Query: Майстерність Керування Станом Сервера",{"title":1904,"path":1905,"stem":1906},"Парадигма Server State: Чому useEffect недостатньо","/javascript/react/tanstack-query/server-state-paradigm","03.javascript/04.react/04.tanstack-query/01.server-state-paradigm",{"title":1908,"path":1909,"stem":1910},"Встановлення та Налаштування: Фундамент","/javascript/react/tanstack-query/installation-and-devtools","03.javascript/04.react/04.tanstack-query/02.installation-and-devtools",{"title":1912,"path":1913,"stem":1914},"Основи Запитів та Магія Ключів","/javascript/react/tanstack-query/query-basics-and-keys","03.javascript/04.react/04.tanstack-query/03.query-basics-and-keys",{"title":1916,"path":1917,"stem":1918},"Синхронізація Даних: Життєвий Цикл Запиту","/javascript/react/tanstack-query/data-synchronization","03.javascript/04.react/04.tanstack-query/04.data-synchronization",{"title":1920,"path":1921,"stem":1922},"Мутації та Інвалідація: Зміна Даних","/javascript/react/tanstack-query/mutations-and-invalidation","03.javascript/04.react/04.tanstack-query/05.mutations-and-invalidation",{"title":1924,"path":1925,"stem":1926},"Оптимістичні Оновлення: Швидше за Світло","/javascript/react/tanstack-query/optimistic-updates","03.javascript/04.react/04.tanstack-query/06.optimistic-updates",{"title":1928,"path":1929,"stem":1930},"Пагінація та Infinite Scroll","/javascript/react/tanstack-query/pagination-and-load-more","03.javascript/04.react/04.tanstack-query/07.pagination-and-load-more",{"title":1932,"path":1933,"stem":1934},"Просунуті Патерни та Оптимізація","/javascript/react/tanstack-query/advanced-patterns","03.javascript/04.react/04.tanstack-query/08.advanced-patterns",{"title":1936,"path":1937,"stem":1938},"Архітектура та Best Practices","/javascript/react/tanstack-query/architecture-and-best-practices","03.javascript/04.react/04.tanstack-query/09.architecture-and-best-practices",{"title":1940,"path":1941,"stem":1942},"Server-Side Rendering (SSR) та Гідратація","/javascript/react/tanstack-query/server-side-rendering","03.javascript/04.react/04.tanstack-query/10.server-side-rendering",{"title":1944,"path":1945,"stem":1946},"Стратегії Тестування","/javascript/react/tanstack-query/testing-strategies","03.javascript/04.react/04.tanstack-query/11.testing-strategies",{"title":1948,"path":1949,"stem":1950},"Аутентифікація та Обробка Помилок","/javascript/react/tanstack-query/authentication-and-errors","03.javascript/04.react/04.tanstack-query/12.authentication-and-errors",{"title":1952,"path":1953,"stem":1954},"React Suspense та Майбутнє","/javascript/react/tanstack-query/react-suspense","03.javascript/04.react/04.tanstack-query/13.react-suspense",{"title":1956,"path":1957,"stem":1958},"Глибоке Занурення в Продуктивність","/javascript/react/tanstack-query/performance-deep-dive","03.javascript/04.react/04.tanstack-query/14.performance-deep-dive",{"title":1960,"icon":1875,"path":1961,"stem":1962,"children":1963},"React Router","/javascript/react/react-router","03.javascript/04.react/05.react-router/index",[1964,1966,1970,1974,1978,1982,1986,1990],{"title":1965,"path":1961,"stem":1962},"React Router: Навігаційна система сучасного вебу",{"title":1967,"path":1968,"stem":1969},"Налаштування та Базовий Роутинг","/javascript/react/react-router/setup-and-basic-routing","03.javascript/04.react/05.react-router/01.setup-and-basic-routing",{"title":1971,"path":1972,"stem":1973},"Динамічна Навігація","/javascript/react/react-router/navigation-and-links","03.javascript/04.react/05.react-router/02.navigation-and-links",{"title":1975,"path":1976,"stem":1977},"Вкладені Маршрути та Макети","/javascript/react/react-router/nested-routes-and-layouts","03.javascript/04.react/05.react-router/03.nested-routes-and-layouts",{"title":1979,"path":1980,"stem":1981},"Динамічні Маршрути та Параметри","/javascript/react/react-router/dynamic-routing","03.javascript/04.react/05.react-router/04.dynamic-routing",{"title":1983,"path":1984,"stem":1985},"Data APIs: Loaders та Actions","/javascript/react/react-router/data-loading","03.javascript/04.react/05.react-router/05.data-loading",{"title":1987,"path":1988,"stem":1989},"Просунуті Патерни","/javascript/react/react-router/advanced-patterns","03.javascript/04.react/05.react-router/06.advanced-patterns",{"title":1991,"path":1992,"stem":1993},"Legacy Routing: Компонентний підхід","/javascript/react/react-router/legacy-routing","03.javascript/04.react/05.react-router/07.legacy-routing",{"title":1995,"icon":132,"path":1996,"stem":1997,"children":1998},"Redux","/javascript/react/redux","03.javascript/04.react/06.redux/index",[1999,2001,2017,2046,2055,2076,2092,2121],{"title":2000,"path":1996,"stem":1997},"Redux: Еволюція управління станом",{"title":14,"icon":15,"path":2002,"stem":2003,"children":2004,"page":59},"/javascript/react/redux/fundamentals","03.javascript/04.react/06.redux/01.fundamentals",[2005,2009,2013],{"title":2006,"path":2007,"stem":2008},"Вступ до State Management","/javascript/react/redux/fundamentals/intro-state-management","03.javascript/04.react/06.redux/01.fundamentals/01.intro-state-management",{"title":2010,"path":2011,"stem":2012},"Філософія Redux та Три Принципи","/javascript/react/redux/fundamentals/redux-philosophy","03.javascript/04.react/06.redux/01.fundamentals/02.redux-philosophy",{"title":2014,"path":2015,"stem":2016},"Чисті функції та Іммутабельність","/javascript/react/redux/fundamentals/pure-functions-immutability","03.javascript/04.react/06.redux/01.fundamentals/03.pure-functions-immutability",{"title":2018,"icon":132,"path":2019,"stem":2020,"children":2021,"page":59},"Classic Redux","/javascript/react/redux/classic-redux","03.javascript/04.react/06.redux/02.classic-redux",[2022,2026,2030,2034,2038,2042],{"title":2023,"path":2024,"stem":2025},"Створення Store (Classic Redux)","/javascript/react/redux/classic-redux/store-setup","03.javascript/04.react/06.redux/02.classic-redux/01.store-setup",{"title":2027,"path":2028,"stem":2029},"Actions, Constants та Action Creators","/javascript/react/redux/classic-redux/actions-constants","03.javascript/04.react/06.redux/02.classic-redux/02.actions-constants",{"title":2031,"path":2032,"stem":2033},"Логіка Reducers","/javascript/react/redux/classic-redux/reducers","03.javascript/04.react/06.redux/02.classic-redux/03.reducers",{"title":2035,"path":2036,"stem":2037},"Комбінування Reducers (Root Reducer)","/javascript/react/redux/classic-redux/data-flow","03.javascript/04.react/06.redux/02.classic-redux/04.data-flow",{"title":2039,"path":2040,"stem":2041},"Підключення до React (React-Redux)","/javascript/react/redux/classic-redux/react-redux-connection","03.javascript/04.react/06.redux/02.classic-redux/05.react-redux-connection",{"title":2043,"path":2044,"stem":2045},"Middleware та Асинхронність (Redux Thunk)","/javascript/react/redux/classic-redux/middleware-thunk","03.javascript/04.react/06.redux/02.classic-redux/06.middleware-thunk",{"title":2047,"icon":132,"path":2048,"stem":2049,"children":2050,"page":59},"Transition To Rtk","/javascript/react/redux/transition-to-rtk","03.javascript/04.react/06.redux/03.transition-to-rtk",[2051],{"title":2052,"path":2053,"stem":2054},"Проблеми класичного Redux","/javascript/react/redux/transition-to-rtk/problems-with-classic","03.javascript/04.react/06.redux/03.transition-to-rtk/01.problems-with-classic",{"title":2056,"icon":132,"path":2057,"stem":2058,"children":2059,"page":59},"Redux Toolkit","/javascript/react/redux/redux-toolkit","03.javascript/04.react/06.redux/04.redux-toolkit",[2060,2064,2068,2072],{"title":2061,"path":2062,"stem":2063},"Налаштування Store з configureStore","/javascript/react/redux/redux-toolkit/configure-store","03.javascript/04.react/06.redux/04.redux-toolkit/01.configure-store",{"title":2065,"path":2066,"stem":2067},"createSlice: Революція в Redux","/javascript/react/redux/redux-toolkit/create-slice","03.javascript/04.react/06.redux/04.redux-toolkit/02.create-slice",{"title":2069,"path":2070,"stem":2071},"Асинхронність з createAsyncThunk","/javascript/react/redux/redux-toolkit/async-thunks","03.javascript/04.react/06.redux/04.redux-toolkit/03.async-thunks",{"title":2073,"path":2074,"stem":2075},"04. Entity Adapter: Керування нормалізованим станом","/javascript/react/redux/redux-toolkit/entity-adapter","03.javascript/04.react/06.redux/04.redux-toolkit/04.entity-adapter",{"title":2077,"icon":92,"path":2078,"stem":2079,"children":2080,"page":59},"Advanced","/javascript/react/redux/advanced","03.javascript/04.react/06.redux/05.advanced",[2081,2085,2089],{"title":2082,"path":2083,"stem":2084},"Мемоізація та Селектори: Повний Гайд по Reselect","/javascript/react/redux/advanced/selectors-reselect","03.javascript/04.react/06.redux/05.advanced/01.selectors-reselect",{"title":2086,"path":2087,"stem":2088},"RTK Query: Архітектура Серверного Кешу","/javascript/react/redux/advanced/rtk-query-intro","03.javascript/04.react/06.redux/05.advanced/02.rtk-query-intro",{"title":1936,"path":2090,"stem":2091},"/javascript/react/redux/advanced/architecture-best-practices","03.javascript/04.react/06.redux/05.advanced/03.architecture-best-practices",{"title":2093,"icon":132,"path":2094,"stem":2095,"children":2096,"page":59},"Project Kanban","/javascript/react/redux/project-kanban","03.javascript/04.react/06.redux/06.project-kanban",[2097,2101,2105,2109,2113,2117],{"title":2098,"path":2099,"stem":2100},"Проєкт: Kanban Board (Trello Clone)","/javascript/react/redux/project-kanban/project-overview","03.javascript/04.react/06.redux/06.project-kanban/01.project-overview",{"title":2102,"path":2103,"stem":2104},"Налаштування та Типізація","/javascript/react/redux/project-kanban/setup-and-types","03.javascript/04.react/06.redux/06.project-kanban/02.setup-and-types",{"title":2106,"path":2107,"stem":2108},"Board Slice: Серце Дошки","/javascript/react/redux/project-kanban/board-slice","03.javascript/04.react/06.redux/06.project-kanban/03.board-slice",{"title":2110,"path":2111,"stem":2112},"Логіка Drag & Drop","/javascript/react/redux/project-kanban/drag-and-drop-logic","03.javascript/04.react/06.redux/06.project-kanban/04.drag-and-drop-logic",{"title":2114,"path":2115,"stem":2116},"Інтеграція з RTK Query","/javascript/react/redux/project-kanban/rtk-query-integration","03.javascript/04.react/06.redux/06.project-kanban/05.rtk-query-integration",{"title":2118,"path":2119,"stem":2120},"Optimistic Updates","/javascript/react/redux/project-kanban/optimistic-updates","03.javascript/04.react/06.redux/06.project-kanban/06.optimistic-updates",{"title":2122,"icon":132,"path":2123,"stem":2124,"children":2125,"page":59},"Testing","/javascript/react/redux/testing","03.javascript/04.react/06.redux/07.testing",[2126,2130,2134,2138,2142],{"title":2127,"path":2128,"stem":2129},"Тестування Redux","/javascript/react/redux/testing/intro-testing","03.javascript/04.react/06.redux/07.testing/01.intro-testing",{"title":2131,"path":2132,"stem":2133},"Тестування Reducers","/javascript/react/redux/testing/testing-reducers","03.javascript/04.react/06.redux/07.testing/02.testing-reducers",{"title":2135,"path":2136,"stem":2137},"Тестування Селекторів","/javascript/react/redux/testing/testing-selectors","03.javascript/04.react/06.redux/07.testing/03.testing-selectors",{"title":2139,"path":2140,"stem":2141},"Тестування Компонентів (Integration)","/javascript/react/redux/testing/testing-components","03.javascript/04.react/06.redux/07.testing/04.testing-components",{"title":2143,"path":2144,"stem":2145},"Тестування Async Thunks","/javascript/react/redux/testing/testing-thunks","03.javascript/04.react/06.redux/07.testing/05.testing-thunks",{"title":2147,"icon":132,"path":2148,"stem":2149,"children":2150},"Ui Libraries","/javascript/react/ui-libraries","03.javascript/04.react/07.ui-libraries/index",[2151,2153,2157,2161,2165,2169,2173],{"title":2152,"path":2148,"stem":2149},"UI Бібліотеки в React",{"title":2154,"path":2155,"stem":2156},"Вступ до UI Бібліотек: Навіщо Винаходити Велосипед Двічі?","/javascript/react/ui-libraries/introduction-to-ui-libraries","03.javascript/04.react/07.ui-libraries/01.introduction-to-ui-libraries",{"title":2158,"path":2159,"stem":2160},"Філософія shadcn/ui: \"Not a Component Library\"","/javascript/react/ui-libraries/shadcn-philosophy","03.javascript/04.react/07.ui-libraries/02.shadcn-philosophy",{"title":2162,"path":2163,"stem":2164},"Установка та Налаштування shadcn/ui","/javascript/react/ui-libraries/shadcn-installation","03.javascript/04.react/07.ui-libraries/03.shadcn-installation",{"title":2166,"path":2167,"stem":2168},"Базові Компоненти shadcn/ui: Фундамент Інтерфейсу","/javascript/react/ui-libraries/shadcn-components-basics","03.javascript/04.react/07.ui-libraries/04.shadcn-components-basics",{"title":2170,"path":2171,"stem":2172},"Компоненти Форм: Побудова Інтерактивних Form","/javascript/react/ui-libraries/shadcn-components-forms","03.javascript/04.react/07.ui-libraries/05.shadcn-components-forms",{"title":2174,"path":2175,"stem":2176},"Складні Компоненти: Dialog, Dropdown, Table та Command","/javascript/react/ui-libraries/shadcn-components-advanced","03.javascript/04.react/07.ui-libraries/06.shadcn-components-advanced",{"title":2178,"icon":2179,"path":2180,"stem":2181,"children":2182,"page":59},"TypeScript","i-devicon-typescript","/javascript/typescript","03.javascript/05.typescript",[2183,2187,2191,2195,2199,2203,2207,2211],{"title":2184,"path":2185,"stem":2186},"TypeScript: Броня для вашого коду","/javascript/typescript/intro-and-basic-types","03.javascript/05.typescript/01.intro-and-basic-types",{"title":2188,"path":2189,"stem":2190},"Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи","/javascript/typescript/interfaces-and-advanced-types","03.javascript/05.typescript/02.interfaces-and-advanced-types",{"title":2192,"path":2193,"stem":2194},"Алхімія Типів: Generics та Utility Types","/javascript/typescript/generics-and-utilities","03.javascript/05.typescript/03.generics-and-utilities",{"title":2196,"path":2197,"stem":2198},"Архітектура та Шаблони: Класи в TypeScript","/javascript/typescript/classes-and-oop","03.javascript/05.typescript/04.classes-and-oop",{"title":2200,"path":2201,"stem":2202},"Продакшн та Екосистема: Advanced Config & Workflow","/javascript/typescript/advanced-patterns-and-config","03.javascript/05.typescript/05.advanced-patterns-and-config",{"title":2204,"path":2205,"stem":2206},"TypeScript у світі React","/javascript/typescript/react-basics","03.javascript/05.typescript/06.react-basics",{"title":2208,"path":2209,"stem":2210},"React + TypeScript: Продвинуті патерни","/javascript/typescript/react-advanced","03.javascript/05.typescript/07.react-advanced",{"title":2212,"path":2213,"stem":2214},"React + TypeScript: Екосистема та бібліотеки","/javascript/typescript/react-ecosystem","03.javascript/05.typescript/08.react-ecosystem",{"title":2216,"path":2217,"stem":2218},"Atomic Design","/javascript/atomic-design","03.javascript/2.atomic-design",{"title":2220,"icon":2221,"path":2222,"stem":2223,"children":2224,"page":59},"Java","i-devicon-java","/java","04.java",[2225,2228,2231,2235,2239,2243,2247],{"title":162,"path":2226,"stem":2227},"/java/data-mapper-part1","04.java/01.data-mapper-part1",{"title":166,"path":2229,"stem":2230},"/java/data-mapper-part2","04.java/02.data-mapper-part2",{"title":2232,"path":2233,"stem":2234},"Service Layer: Організація бізнес-логіки","/java/service-layer","04.java/03.service-layer",{"title":2236,"path":2237,"stem":2238},"Rich Domain Model та State Pattern","/java/rich-domain-model","04.java/04.rich-domain-model",{"title":2240,"path":2241,"stem":2242},"Патерни для складної бізнес-логіки","/java/business-logic-patterns","04.java/05.business-logic-patterns",{"title":2244,"path":2245,"stem":2246},"Обробка помилок та валідація","/java/error-handling-validation","04.java/06.error-handling-validation",{"title":2248,"path":2249,"stem":2250,"children":2251,"page":59},"Проектування баз даних","/java/pr2","04.java/pr2",[2252,2256,2260,2264,2268,2272,2276,2280,2284,2288,2292,2296,2300,2304,2308,2312,2316,2320,2324,2328,2332,2336,2340,2344,2348],{"title":2253,"path":2254,"stem":2255},"Концептуальне моделювання: Мистецтво розуміння предметної області","/java/pr2/conceptual-modeling","04.java/pr2/01.conceptual-modeling",{"title":2257,"path":2258,"stem":2259},"Логічне моделювання: Від бізнес-ідей до структур даних","/java/pr2/logical-modeling","04.java/pr2/02.logical-modeling",{"title":2261,"path":2262,"stem":2263},"Нормалізація: Гігієна даних та боротьба з аномаліями","/java/pr2/normalization","04.java/pr2/03.normalization",{"title":2265,"path":2266,"stem":2267},"Фізична схема: Від абстракції до DDL","/java/pr2/physical-schema","04.java/pr2/04.physical-schema",{"title":2269,"path":2270,"stem":2271},"Архітектурна класифікація таблиць","/java/pr2/table-classification","04.java/pr2/05.table-classification",{"title":2273,"path":2274,"stem":2275},"Database Migrations: Версіонування схеми з Flyway","/java/pr2/database-migrations","04.java/pr2/06.database-migrations",{"title":2277,"path":2278,"stem":2279},"А що, якби це була не реляційна БД?","/java/pr2/beyond-relational","04.java/pr2/07.beyond-relational",{"title":2281,"path":2282,"stem":2283},"Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити","/java/pr2/impedance-mismatch","04.java/pr2/09.impedance-mismatch",{"title":2285,"path":2286,"stem":2287},"JDBC: Перший контакт із базою даних","/java/pr2/jdbc-fundamentals","04.java/pr2/10.jdbc-fundamentals",{"title":2289,"path":2290,"stem":2291},"Якість коду: Spotless, SpotBugs та SonarQube","/java/pr2/10a.code-quality","04.java/pr2/10a.code-quality",{"title":2293,"path":2294,"stem":2295},"Connection Pool: Патерн Object Pool для JDBC-з'єднань","/java/pr2/connection-pool","04.java/pr2/11.connection-pool",{"title":2297,"path":2298,"stem":2299},"Row Data Gateway: Об'єкт як обгортка рядка таблиці","/java/pr2/row-data-gateway","04.java/pr2/12.row-data-gateway",{"title":2301,"path":2302,"stem":2303},"Table Data Gateway: Фасад таблиці як архітектурний відступ","/java/pr2/table-data-gateway","04.java/pr2/13.table-data-gateway",{"title":2305,"path":2306,"stem":2307},"Repository + Data Mapper: Правильна шарова архітектура з JDBC","/java/pr2/repository-data-mapper","04.java/pr2/14.repository-data-mapper",{"title":2309,"path":2310,"stem":2311},"Identity Map: Кешування сутностей у рамках сесії","/java/pr2/identity-map","04.java/pr2/15.identity-map",{"title":2313,"path":2314,"stem":2315},"Unit of Work: Відстеження змін і координація JDBC-транзакцій","/java/pr2/unit-of-work","04.java/pr2/16.unit-of-work",{"title":2317,"path":2318,"stem":2319},"Strategy: Замінювані SQL-стратегії для підтримки різних СУБД","/java/pr2/strategy-sql","04.java/pr2/17.strategy-sql",{"title":2321,"path":2322,"stem":2323},"Proxy: Lazy Loading для One-To-Many колекцій","/java/pr2/proxy-lazy-loading","04.java/pr2/18.proxy-lazy-loading",{"title":2325,"path":2326,"stem":2327},"Generic Repository через Java Reflection: анотації та динамічний SQL","/java/pr2/generic-repository-reflection","04.java/pr2/19.generic-repository-reflection",{"title":2329,"path":2330,"stem":2331},"Specification Pattern: Композиція бізнес-правил для складних запитів","/java/pr2/specification-pattern","04.java/pr2/20.specification-pattern",{"title":2333,"path":2334,"stem":2335},"Розширені можливості Specification Pattern: підзапити, агрегації та гібридний підхід","/java/pr2/20a.advanced-specifications","04.java/pr2/20a.advanced-specifications",{"title":2337,"path":2338,"stem":2339},"Асинхронність у JDBC: Від блокуючих викликів до CompletableFuture","/java/pr2/asynchronous-jdbc","04.java/pr2/21.asynchronous-jdbc",{"title":2341,"path":2342,"stem":2343},"Інтеграційне тестування JDBC-репозиторіїв: Embedded H2 та патерн AAA","/java/pr2/integration-testing-h2","04.java/pr2/22.integration-testing-h2",{"title":2345,"path":2346,"stem":2347},"Testcontainers: Тестування з реальною PostgreSQL у Docker-контейнерах","/java/pr2/integration-testing-testcontainers","04.java/pr2/23.integration-testing-testcontainers",{"title":2349,"path":2350,"stem":2351},"Модуль \"Проектування реляційних баз даних\" для 04.java/pr2","/java/pr2/implementation_plan","04.java/pr2/implementation_plan",{"title":2353,"icon":2354,"path":2355,"stem":2356,"children":2357,"page":59},"Бази даних","i-lucide-database","/databases","06.databases",[2358,2388,2411,2448,2477,2495,2529,2541,2550],{"title":2359,"icon":2360,"path":2361,"stem":2362,"children":2363,"page":59},"Intro","i-lucide-play","/databases/intro","06.databases/01.intro",[2364,2368,2372,2376,2380,2384],{"title":2365,"path":2366,"stem":2367},"Введення в теорію баз даних","/databases/intro/introduction-to-databases","06.databases/01.intro/01.introduction-to-databases",{"title":2369,"path":2370,"stem":2371},"Реляційна модель даних","/databases/intro/relational-model-theory","06.databases/01.intro/02.relational-model-theory",{"title":2373,"path":2374,"stem":2375},"ER-моделювання","/databases/intro/er-modeling","06.databases/01.intro/03.er-modeling",{"title":2377,"path":2378,"stem":2379},"Логічне проектування БД","/databases/intro/logical-schema","06.databases/01.intro/04.logical-schema",{"title":2381,"path":2382,"stem":2383},"Класифікація таблиць","/databases/intro/table-classification","06.databases/01.intro/05.table-classification",{"title":2385,"path":2386,"stem":2387},"PlantUML для баз даних","/databases/intro/plantuml-diagrams","06.databases/01.intro/06.plantuml-diagrams",{"title":2389,"icon":2354,"path":2390,"stem":2391,"children":2392,"page":59},"MS SQL Server Start","/databases/ms-sql-server-start","06.databases/02.ms-sql-server-start",[2393,2397,2403,2407],{"title":2394,"path":2395,"stem":2396},"Типи даних у MS SQL Server","/databases/ms-sql-server-start/data-types","06.databases/02.ms-sql-server-start/01.data-types",{"title":2398,"path":2399,"stem":2400,"children":2401},"Індекси у MS SQL Server","/databases/ms-sql-server-start/sql-indexes","06.databases/02.ms-sql-server-start/02.sql-indexes",[2402],{"title":2398,"path":2399,"stem":2400},{"title":2404,"path":2405,"stem":2406},"Системні бази даних MS SQL Server","/databases/ms-sql-server-start/system-databases","06.databases/02.ms-sql-server-start/03.system-databases",{"title":2408,"path":2409,"stem":2410},"Огляд мови SQL та запитів","/databases/ms-sql-server-start/sql-queries-overview","06.databases/02.ms-sql-server-start/04.sql-queries-overview",{"title":2412,"icon":2354,"path":2413,"stem":2414,"children":2415,"page":59},"SQL","/databases/sql","06.databases/03.sql",[2416,2420,2424,2428,2432,2436,2440,2444],{"title":2417,"path":2418,"stem":2419},"Налаштування демонстраційної бази даних","/databases/sql/sample-database-setup","06.databases/03.sql/00.sample-database-setup",{"title":2421,"path":2422,"stem":2423},"DDL - Створення таблиць (CREATE TABLE)","/databases/sql/ddl-create-table","06.databases/03.sql/01.ddl-create-table",{"title":2425,"path":2426,"stem":2427},"DDL - Зміна та видалення таблиць (ALTER, DROP)","/databases/sql/ddl-alter-drop-table","06.databases/03.sql/02.ddl-alter-drop-table",{"title":2429,"path":2430,"stem":2431},"SELECT запити - Основи","/databases/sql/select-queries-fundamentals","06.databases/03.sql/03.select-queries-fundamentals",{"title":2433,"path":2434,"stem":2435},"SELECT запити - Розширені можливості","/databases/sql/select-queries-advanced","06.databases/03.sql/04.select-queries-advanced",{"title":2437,"path":2438,"stem":2439},"INSERT запити - Додавання даних","/databases/sql/insert-queries","06.databases/03.sql/05.insert-queries",{"title":2441,"path":2442,"stem":2443},"UPDATE та DELETE запити","/databases/sql/update-delete-queries","06.databases/03.sql/06.update-delete-queries",{"title":2445,"path":2446,"stem":2447},"Транзакції в SQL","/databases/sql/transactions","06.databases/03.sql/07.transactions",{"title":2449,"icon":2354,"path":2450,"stem":2451,"children":2452,"page":59},"Multi Table Databases","/databases/multi-table-databases","06.databases/04.multi-table-databases",[2453,2457,2461,2465,2469,2473],{"title":2454,"path":2455,"stem":2456},"Зв'язки та нормалізація БД","/databases/multi-table-databases/relationships-and-normalization","06.databases/04.multi-table-databases/00.relationships-and-normalization",{"title":2458,"path":2459,"stem":2460},"INNER JOIN - З'єднання таблиць","/databases/multi-table-databases/inner-join","06.databases/04.multi-table-databases/01.inner-join",{"title":2462,"path":2463,"stem":2464},"OUTER JOINs - LEFT, RIGHT, FULL","/databases/multi-table-databases/outer-joins","06.databases/04.multi-table-databases/02.outer-joins",{"title":2466,"path":2467,"stem":2468},"CROSS та SELF JOINs","/databases/multi-table-databases/cross-self-joins","06.databases/04.multi-table-databases/03.cross-self-joins",{"title":2470,"path":2471,"stem":2472},"Підзапити (Subqueries)","/databases/multi-table-databases/subqueries","06.databases/04.multi-table-databases/04.subqueries",{"title":2474,"path":2475,"stem":2476},"Агрегації з JOIN","/databases/multi-table-databases/aggregations-with-joins","06.databases/04.multi-table-databases/05.aggregations-with-joins",{"title":2478,"icon":2479,"path":2480,"stem":2481,"children":2482,"page":59},"Aggregate Functions","i-lucide-calculator","/databases/aggregate-functions","06.databases/05.aggregate-functions",[2483,2487,2491],{"title":2484,"path":2485,"stem":2486},"Функції агрегування в MS SQL Server","/databases/aggregate-functions/introduction-aggregate-functions","06.databases/05.aggregate-functions/01.introduction-aggregate-functions",{"title":2488,"path":2489,"stem":2490},"Групування даних в MS SQL Server","/databases/aggregate-functions/grouping-data","06.databases/05.aggregate-functions/02.grouping-data",{"title":2492,"path":2493,"stem":2494},"Підзапити з агрегатними функціями","/databases/aggregate-functions/subqueries-aggregates","06.databases/05.aggregate-functions/03.subqueries-aggregates",{"title":2496,"icon":2497,"path":2498,"stem":2499,"children":2500,"page":59},"Тригери та зберігаємі процедури","i-lucide-database-zap","/databases/triggers-stored-procedures","06.databases/07.triggers-stored-procedures",[2501,2505,2509,2513,2517,2521,2525],{"title":2502,"path":2503,"stem":2504},"DML-тригери","/databases/triggers-stored-procedures/dml-triggers","06.databases/07.triggers-stored-procedures/01.dml-triggers",{"title":2506,"path":2507,"stem":2508},"DDL-тригери","/databases/triggers-stored-procedures/ddl-triggers","06.databases/07.triggers-stored-procedures/02.ddl-triggers",{"title":2510,"path":2511,"stem":2512},"Transact-SQL розширення","/databases/triggers-stored-procedures/transact-sql-extensions","06.databases/07.triggers-stored-procedures/03.transact-sql-extensions",{"title":2514,"path":2515,"stem":2516},"Транзакції","/databases/triggers-stored-procedures/transactions","06.databases/07.triggers-stored-procedures/04.transactions",{"title":2518,"path":2519,"stem":2520},"Зберігаємі процедури","/databases/triggers-stored-procedures/stored-procedures","06.databases/07.triggers-stored-procedures/05.stored-procedures",{"title":2522,"path":2523,"stem":2524},"Користувацькі функції","/databases/triggers-stored-procedures/user-defined-functions","06.databases/07.triggers-stored-procedures/06.user-defined-functions",{"title":2526,"path":2527,"stem":2528},"Безпека баз даних","/databases/triggers-stored-procedures/security","06.databases/07.triggers-stored-procedures/08.security",{"title":2526,"icon":793,"path":2530,"stem":2531,"children":2532,"page":59},"/databases/security","06.databases/08.security",[2533,2537],{"title":2534,"path":2535,"stem":2536},"Вступ до безпеки баз даних","/databases/security/introduction","06.databases/08.security/01.introduction",{"title":2538,"path":2539,"stem":2540},"Системні представлення та метадані","/databases/security/system-views","06.databases/08.security/02.system-views",{"title":2542,"icon":2543,"path":2544,"stem":2545,"children":2546,"page":59},"Резервне копіювання та відновлення","i-lucide-database-backup","/databases/backup-recovery","06.databases/09.backup-recovery",[2547],{"title":2542,"path":2548,"stem":2549},"/databases/backup-recovery/backup-restore","06.databases/09.backup-recovery/01.backup-restore",{"title":2551,"icon":2552,"path":2553,"stem":2554,"children":2555,"page":59},"Повнотекстовий пошук","i-lucide-search","/databases/full-text-search","06.databases/10.full-text-search",[2556],{"title":2551,"path":2557,"stem":2558},"/databases/full-text-search/full-text-search","06.databases/10.full-text-search/01.full-text-search",{"title":2560,"icon":2561,"path":2562,"stem":2563,"children":2564,"page":59},"Tools","i-lucide-wrench","/tools","07.tools",[2565],{"title":2566,"icon":2567,"path":2568,"stem":2569,"children":2570},"Docker","i-simple-icons-docker","/tools/docker","07.tools/01.docker/index",[2571,2573,2577,2581,2585,2589,2593,2597,2601,2605,2609,2613,2617,2621,2625,2629,2633,2637],{"title":2572,"path":2568,"stem":2569},"Docker: від нуля до production",{"title":2574,"path":2575,"stem":2576},"Контейнеризація — від проблеми до рішення","/tools/docker/containerization-concept","07.tools/01.docker/01.containerization-concept",{"title":2578,"path":2579,"stem":2580},"Docker — що це і навіщо?","/tools/docker/docker-what-and-why","07.tools/01.docker/02.docker-what-and-why",{"title":2582,"path":2583,"stem":2584},"Архітектура Docker Engine","/tools/docker/docker-architecture","07.tools/01.docker/03.docker-architecture",{"title":2586,"path":2587,"stem":2588},"Встановлення Docker","/tools/docker/installation","07.tools/01.docker/04.installation",{"title":2590,"path":2591,"stem":2592},"Перший контейнер — docker run","/tools/docker/first-container","07.tools/01.docker/05.first-container",{"title":2594,"path":2595,"stem":2596},"Життєвий цикл контейнера","/tools/docker/container-lifecycle","07.tools/01.docker/06.container-lifecycle",{"title":2598,"path":2599,"stem":2600},"Docker Images — фундаментальні концепції","/tools/docker/docker-images-fundamentals","07.tools/01.docker/07.docker-images-fundamentals",{"title":2602,"path":2603,"stem":2604},"Dockerfile — основи","/tools/docker/dockerfile-basics","07.tools/01.docker/08.dockerfile-basics",{"title":2606,"path":2607,"stem":2608},"Dockerfile — просунуті техніки","/tools/docker/dockerfile-advanced","07.tools/01.docker/09.dockerfile-advanced",{"title":2610,"path":2611,"stem":2612},"Build Context та кешування шарів","/tools/docker/build-context-and-cache","07.tools/01.docker/10.build-context-and-cache",{"title":2614,"path":2615,"stem":2616},"Реєстри Docker-образів","/tools/docker/image-registries","07.tools/01.docker/11.image-registries",{"title":2618,"path":2619,"stem":2620},"Контейнеризація .NET додатків","/tools/docker/dotnet-containerization","07.tools/01.docker/12.dotnet-containerization",{"title":2622,"path":2623,"stem":2624},"Томи та збереження даних","/tools/docker/volumes-and-data","07.tools/01.docker/13.volumes-and-data",{"title":2626,"path":2627,"stem":2628},"Основи мережі в Docker","/tools/docker/networking-basics","07.tools/01.docker/14.networking-basics",{"title":2630,"path":2631,"stem":2632},"Змінні оточення та конфігурація","/tools/docker/environment-and-configuration","07.tools/01.docker/15.environment-and-configuration",{"title":2634,"path":2635,"stem":2636},"Docker Compose — оркестрація контейнерів","/tools/docker/docker-compose-basics","07.tools/01.docker/16.docker-compose-basics",{"title":2638,"path":2639,"stem":2640},"Docker Compose — Multi-Service застосунки","/tools/docker/compose-multi-service","07.tools/01.docker/17.compose-multi-service",{"title":2642,"icon":2643,"path":2644,"stem":2645,"children":2646,"page":59},"Software Engineering","i-lucide-code-2","/software-engineering","09.software-engineering",[2647,2651,2655,2659,2663,2667,2671,2675,2679,2683,2687],{"title":2648,"path":2649,"stem":2650},"1. Аналіз предметної області. Експертні знання та складність","/software-engineering/intro.subdomains","09.software-engineering/01.intro.subdomains",{"title":2652,"path":2653,"stem":2654},"2. Обмежені контексти. Інтеграція обмежених контекстів","/software-engineering/integrating-limited-contexts","09.software-engineering/02.integrating-limited-contexts",{"title":2656,"path":2657,"stem":2658},"3. Реалізація простої бізнес-логіки","/software-engineering/simple","09.software-engineering/03.simple",{"title":2660,"path":2661,"stem":2662},"4. Опрацювання складної бізнес-логіки","/software-engineering/complex-business-logic","09.software-engineering/04.complex-business-logic",{"title":2664,"path":2665,"stem":2666},"5. Моделювання фактора часу. Подієво-орієнтована архітектура.","/software-engineering/modelling-the-time-factor","09.software-engineering/05.modelling-the-time-factor",{"title":2668,"path":2669,"stem":2670},"6. Архітектурні патерни","/software-engineering/architectural-patterns","09.software-engineering/06.architectural-patterns",{"title":2672,"path":2673,"stem":2674},"Паттерни взаємодії","/software-engineering/patterns-of-interaction","09.software-engineering/07.patterns-of-interaction",{"title":2676,"path":2677,"stem":2678},"Евристика проєктування","/software-engineering/design-heuristics","09.software-engineering/08.design-heuristics",{"title":2680,"path":2681,"stem":2682},"Еволюція проєктних рішень","/software-engineering/evolution-of-design-solutions","09.software-engineering/09.evolution-of-design-solutions",{"title":2684,"path":2685,"stem":2686},"EventStorming","/software-engineering/eventstorming","09.software-engineering/10.eventstorming",{"title":2688,"path":2689,"stem":2690},"DDD на практиці","/software-engineering/ddd-in-practice","09.software-engineering/11.ddd-in-practice",{"title":2692,"icon":943,"path":2693,"stem":2694,"children":2695,"page":59},"DDD","/ddd","10.ddd",[2696,2700,2704,2708,2712,2716,2720,2724,2728,2732,2736,2740,2744],{"title":2697,"path":2698,"stem":2699},"Аналіз предметної області","/ddd/domain-analysis","10.ddd/01.domain-analysis",{"title":2701,"path":2702,"stem":2703},"Експертні знання про предметну область","/ddd/domain-expert-knowledge","10.ddd/02.domain-expert-knowledge",{"title":2705,"path":2706,"stem":2707},"Як осмислити складність предметної області","/ddd/managing-domain-complexity","10.ddd/03.managing-domain-complexity",{"title":2709,"path":2710,"stem":2711},"Інтеграція обмежених контекстів","/ddd/bounded-context-integration","10.ddd/04.bounded-context-integration",{"title":2713,"path":2714,"stem":2715},"Реалізація простої бізнес-логіки","/ddd/simple-business-logic","10.ddd/05.simple-business-logic",{"title":2717,"path":2718,"stem":2719},"Обробка складної бізнес-логіки","/ddd/complex-business-logic","10.ddd/06.complex-business-logic",{"title":2721,"path":2722,"stem":2723},"Моделювання фактора часу","/ddd/time-modeling","10.ddd/07.time-modeling",{"title":2725,"path":2726,"stem":2727},"Глава 8. Архітектурні Патерни","/ddd/architectural-patterns","10.ddd/08.architectural-patterns",{"title":2729,"path":2730,"stem":2731},"Глава 9. Патерни Взаємодії","/ddd/interaction-patterns","10.ddd/09.interaction-patterns",{"title":2733,"path":2734,"stem":2735},"Глава 10. Проектні Евристики","/ddd/design-heuristics","10.ddd/10.design-heuristics",{"title":2737,"path":2738,"stem":2739},"Глава 11. Еволюція Проектних Рішень","/ddd/evolution-of-design-decisions","10.ddd/11.evolution-of-design-decisions",{"title":2741,"path":2742,"stem":2743},"Глава 12. EventStorming","/ddd/event-storming","10.ddd/12.event-storming",{"title":2745,"path":2746,"stem":2747},"Глава 13. DDD на Практиці","/ddd/ddd-in-practice","10.ddd/13.ddd-in-practice",{"title":2749,"icon":2750,"path":2751,"stem":2752,"children":2753,"page":59},"Media Streaming","i-lucide-video","/media-streaming","11.media-streaming",[2754,2758,2762,2766,2770,2774,2778],{"title":2755,"path":2756,"stem":2757},"01. Магія Стрімінгу: Що відбувається, коли ви натискаєте \"Play\"","/media-streaming/introduction","11.media-streaming/01.introduction",{"title":2759,"path":2760,"stem":2761},"02. Анатомія Медіа: Кодеки, Контейнери та Стиснення","/media-streaming/audio-video-anatomy","11.media-streaming/02.audio-video-anatomy",{"title":2763,"path":2764,"stem":2765},"03. The Gym: FFmpeg Deep Dive","/media-streaming/ffmpeg-gym","11.media-streaming/03.ffmpeg-gym",{"title":2767,"path":2768,"stem":2769},"04. HLS Protocol: HTTP Live Streaming у Деталях","/media-streaming/hls-protocol","11.media-streaming/04.hls-protocol",{"title":2771,"path":2772,"stem":2773},"05. DASH Protocol: Відкритий Стандарт","/media-streaming/dash-protocol","11.media-streaming/05.dash-protocol",{"title":2775,"path":2776,"stem":2777},"06. Масштабування: CDN та Adaptive Bitrate","/media-streaming/cdn-and-adaptive-bitrate","11.media-streaming/06.cdn-and-adaptive-bitrate",{"title":2779,"path":2780,"stem":2781},"07. Війна із Затримкою (Latency)","/media-streaming/realtime-latency","11.media-streaming/07.realtime-latency",{"title":2783,"icon":2784,"path":2785,"stem":2786,"children":2787,"page":59},"HTML & CSS","i-devicon-html5","/html-css","12.html-css",[2788,2792,2796,2800,2804,2808,2812,2816,2820,2824,2828,2832,2836,2840,2844,2848,2852,2856,2860,2864,2868,2872,2876,2880,2884,2888,2892,2896,2900,2904],{"title":2789,"path":2790,"stem":2791},"Вступ до HTML. Структура документа","/html-css/intro-html-structure","12.html-css/01.intro-html-structure",{"title":2793,"path":2794,"stem":2795},"Форматування тексту в HTML","/html-css/html-text-formatting","12.html-css/02.html-text-formatting",{"title":2797,"path":2798,"stem":2799},"Посилання та зображення в HTML","/html-css/html-links-images","12.html-css/03.html-links-images",{"title":2801,"path":2802,"stem":2803},"Списки та таблиці в HTML","/html-css/html-lists-tables","12.html-css/04.html-lists-tables",{"title":2805,"path":2806,"stem":2807},"Форми в HTML","/html-css/html-forms","12.html-css/05.html-forms",{"title":2809,"path":2810,"stem":2811},"Семантичні елементи HTML5","/html-css/html-semantic-elements","12.html-css/06.html-semantic-elements",{"title":2813,"path":2814,"stem":2815},"Мультимедіа та розширені елементи HTML","/html-css/html-multimedia-advanced","12.html-css/07.html-multimedia-advanced",{"title":2817,"path":2818,"stem":2819},"Мікророзмітка та SEO в HTML","/html-css/html-microdata-seo","12.html-css/08.html-microdata-seo",{"title":2821,"path":2822,"stem":2823},"Вступ до CSS. Селектори та специфічність","/html-css/css-intro-selectors","12.html-css/09.css-intro-selectors",{"title":2825,"path":2826,"stem":2827},"Блокова модель CSS. Відступи. Box Sizing","/html-css/css-box-model","12.html-css/10.css-box-model",{"title":2829,"path":2830,"stem":2831},"Розміри у CSS: повний довідник одиниць і ключових слів","/html-css/10a.css-sizing","12.html-css/10a.css-sizing",{"title":2833,"path":2834,"stem":2835},"Типографіка в CSS. Шрифти та текст","/html-css/css-typography","12.html-css/11.css-typography",{"title":2837,"path":2838,"stem":2839},"Кольори та фони в CSS","/html-css/css-colors-backgrounds","12.html-css/12.css-colors-backgrounds",{"title":2841,"path":2842,"stem":2843},"Тіні та фільтри в CSS","/html-css/12b.css-shadows-filters","12.html-css/12b.css-shadows-filters",{"title":2845,"path":2846,"stem":2847},"CSS Flexbox: Фундамент гнучких макетів","/html-css/css-flexbox-fundamentals","12.html-css/13.css-flexbox-fundamentals",{"title":2849,"path":2850,"stem":2851},"CSS Flexbox: Вирівнювання та Позиціонування","/html-css/css-flexbox-alignment-sizing-and-patterns","12.html-css/14.css-flexbox-alignment-sizing-and-patterns",{"title":2853,"path":2854,"stem":2855},"CSS Grid. Двовимірний макет. Частина 1","/html-css/css-layout-grid","12.html-css/15.css-layout-grid",{"title":2857,"path":2858,"stem":2859},"CSS Grid. Двовимірний макет. Частина 2","/html-css/css-layout-grid-advanced","12.html-css/16.css-layout-grid-advanced",{"title":2861,"path":2862,"stem":2863},"Позиціонування в CSS. Z-index. Stacking Context","/html-css/css-positioning","12.html-css/17.css-positioning",{"title":2865,"path":2866,"stem":2867},"CSS Анімації та Переходи","/html-css/css-animations-transitions","12.html-css/18.css-animations-transitions",{"title":2869,"path":2870,"stem":2871},"Адаптивний дизайн. Media Queries. Частина 1","/html-css/css-responsive-media-queries","12.html-css/19.css-responsive-media-queries",{"title":2873,"path":2874,"stem":2875},"Адаптивний дизайн. Частина 2: clamp(), Container Queries, @layer","/html-css/css-responsive-advanced","12.html-css/20.css-responsive-advanced",{"title":2877,"path":2878,"stem":2879},"CSS Custom Properties. Методології. Сучасний CSS","/html-css/css-variables-methodologies","12.html-css/21.css-variables-methodologies",{"title":2881,"path":2882,"stem":2883},"Сучасний CSS 2023–2025: Нові можливості","/html-css/css-modern-features","12.html-css/22.css-modern-features",{"title":2885,"path":2886,"stem":2887},"CSS Nesting, @layer, @scope та @property: нативний препроцесор","/html-css/22a.css-nesting-modern-syntax","12.html-css/22a.css-nesting-modern-syntax",{"title":2889,"path":2890,"stem":2891},"CSS для форм та інтерактивних станів","/html-css/css-forms-interactive-states","12.html-css/23.css-forms-interactive-states",{"title":2893,"path":2894,"stem":2895},"Доступність у CSS (CSS Accessibility)","/html-css/css-accessibility","12.html-css/24.css-accessibility",{"title":2897,"path":2898,"stem":2899},"CSS-функції та сучасні sizing primitives","/html-css/css-functions-sizing","12.html-css/25.css-functions-sizing",{"title":2901,"path":2902,"stem":2903},"Rendering Pipeline і CSS Performance","/html-css/css-rendering-performance","12.html-css/26.css-rendering-performance",{"title":2905,"path":2906,"stem":2907},"CSS Best Practices: типові ситуації та правильні рішення","/html-css/css-best-practices","12.html-css/27.css-best-practices",{"title":2909,"path":2910,"stem":2911,"children":2912,"page":59},"Tailwind","/tailwind","21.tailwind",[2913,2917,2921,2925,2929,2933,2937,2941],{"title":2914,"path":2915,"stem":2916},"Що таке Tailwind CSS і навіщо він потрібен","/tailwind/tailwind-intro-philosophy","21.tailwind/01.tailwind-intro-philosophy",{"title":2918,"path":2919,"stem":2920},"Встановлення та налаштування Tailwind CSS v4","/tailwind/tailwind-installation-setup","21.tailwind/02.tailwind-installation-setup",{"title":2922,"path":2923,"stem":2924},"Utility-класи: основи та система Tailwind","/tailwind/tailwind-utility-classes-core","21.tailwind/03.tailwind-utility-classes-core",{"title":2926,"path":2927,"stem":2928},"Layout: Flexbox та Grid через Tailwind","/tailwind/tailwind-flexbox-grid","21.tailwind/04.tailwind-flexbox-grid",{"title":2930,"path":2931,"stem":2932},"Кастомізація теми через @theme у Tailwind v4","/tailwind/tailwind-theme-customization","21.tailwind/05.tailwind-theme-customization",{"title":2934,"path":2935,"stem":2936},"Варіанти: hover, focus, responsive, dark mode та нові v4","/tailwind/tailwind-variants-states","21.tailwind/06.tailwind-variants-states",{"title":2938,"path":2939,"stem":2940},"Типографіка та система кольорів у Tailwind v4","/tailwind/tailwind-typography-colors","21.tailwind/07.tailwind-typography-colors",{"title":2942,"path":2943,"stem":2944},"Компоненти та повторюваність: @apply, @utility та патерни","/tailwind/tailwind-components-patterns","21.tailwind/08.tailwind-components-patterns",{"title":2946,"path":2947,"stem":2948},"Showcase Компонентів kostyl.dev","/test-new-components","98.test-new-components",{"id":2950,"title":2345,"body":2951,"description":15282,"extension":15283,"links":15284,"meta":15285,"navigation":3061,"path":2346,"seo":15286,"stem":2347,"__hash__":15287},"docs/04.java/pr2/23.integration-testing-testcontainers.md",{"type":2952,"value":2953,"toc":15240},"minimark",[2954,2958,2963,2967,2975,2980,2983,3151,3165,3171,3242,3245,3297,3306,3309,3313,3316,3469,3474,3533,3547,3549,3553,3564,3616,3628,3634,3639,3641,3645,3655,3661,3739,3744,3753,3758,3760,3764,3781,3785,3953,3958,3992,3994,3998,4001,4006,4009,4184,4189,4197,4202,4214,4219,4224,4226,4230,4237,4431,4435,4443,4447,4455,4457,4461,4464,4565,4569,4575,4581,4583,4587,4590,4761,4786,4788,4792,4795,4798,4803,4843,4848,4938,4943,4960,4984,4986,4990,4996,5580,5585,5627,5649,5651,5655,5662,7532,7537,7633,7658,7660,7664,7671,8802,8807,8852,8854,8858,8865,8869,11005,11010,11044,11065,11067,11071,11078,11082,11085,12594,12599,12650,12769,12771,12775,12778,12784,12787,12798,13842,13847,13875,13910,13912,13916,13919,14074,14079,14114,14153,14155,14158,14161,14165,14453,14457,14488,14490,14494,14708,14712,14737,14739,14743,14860,14864,14875,14917,14919,14923,14926,14974,14976,14980,15151,15156,15162,15164,15168,15222,15224,15230,15236],[2955,2956,2345],"h1",{"id":2957},"testcontainers-тестування-з-реальною-postgresql-у-docker-контейнерах",[2959,2960,2962],"h2",{"id":2961},"вступ-обмеження-embedded-h2","Вступ: Обмеження Embedded H2",[2964,2965,2966],"p",{},"У попередній статті ми побудували інтеграційні тести на основі Embedded H2 Database — легковагової Java-БД, що працює у пам'яті JVM-процесу. Цей підхід має значні переваги: швидкість виконання (мілісекунди на тест), простота налаштування (жодних зовнішніх залежностей), автоматична ізоляція (кожен тест отримує нову БД).",[2964,2968,2969,2970,2974],{},"Але H2 є ",[2971,2972,2973],"strong",{},"емуляцією"," реляційної БД, а не повноцінною СУБД. Розглянемо конкретний приклад, що демонструє фундаментальну проблему емуляції.",[2976,2977,2979],"h3",{"id":2978},"проблема-1-діалектні-відмінності-sql","Проблема 1: Діалектні відмінності SQL",[2964,2981,2982],{},"Наша production-система використовує PostgreSQL 15. У схемі БД визначено ENUM-тип для форматів аудіофайлів:",[2984,2985,2990],"pre",{"className":2986,"code":2987,"language":2988,"meta":2989,"style":2989},"language-sql shiki shiki-themes light-plus dark-plus dark-plus","-- PostgreSQL DDL (production)\nCREATE TYPE file_format_enum AS ENUM ('mp3', 'ogg', 'wav', 'm4b', 'aac', 'flac');\n\nCREATE TABLE audiobook_files (\n    id           UUID PRIMARY KEY,\n    audiobook_id UUID NOT NULL,\n    file_path    VARCHAR(2048) NOT NULL,\n    format       file_format_enum NOT NULL,  -- ← PostgreSQL ENUM\n    size         INTEGER\n);\n","sql","",[2991,2992,2993,3002,3056,3063,3077,3089,3100,3123,3137,3146],"code",{"__ignoreMap":2989},[2994,2995,2998],"span",{"class":2996,"line":2997},"line",1,[2994,2999,3001],{"class":3000},"spJ8K","-- PostgreSQL DDL (production)\n",[2994,3003,3005,3009,3012,3016,3019,3023,3027,3030,3033,3035,3038,3040,3043,3045,3048,3050,3053],{"class":2996,"line":3004},2,[2994,3006,3008],{"class":3007},"su1O8","CREATE",[2994,3010,3011],{"class":3007}," TYPE",[2994,3013,3015],{"class":3014},"s8Opu"," file_format_enum",[2994,3017,3018],{"class":3007}," AS",[2994,3020,3022],{"class":3021},"sHH4Y"," ENUM (",[2994,3024,3026],{"class":3025},"sbdoH","'mp3'",[2994,3028,3029],{"class":3021},", ",[2994,3031,3032],{"class":3025},"'ogg'",[2994,3034,3029],{"class":3021},[2994,3036,3037],{"class":3025},"'wav'",[2994,3039,3029],{"class":3021},[2994,3041,3042],{"class":3025},"'m4b'",[2994,3044,3029],{"class":3021},[2994,3046,3047],{"class":3025},"'aac'",[2994,3049,3029],{"class":3021},[2994,3051,3052],{"class":3025},"'flac'",[2994,3054,3055],{"class":3021},");\n",[2994,3057,3059],{"class":2996,"line":3058},3,[2994,3060,3062],{"emptyLinePlaceholder":3061},true,"\n",[2994,3064,3066,3068,3071,3074],{"class":2996,"line":3065},4,[2994,3067,3008],{"class":3007},[2994,3069,3070],{"class":3007}," TABLE",[2994,3072,3073],{"class":3014}," audiobook_files",[2994,3075,3076],{"class":3021}," (\n",[2994,3078,3080,3083,3086],{"class":2996,"line":3079},5,[2994,3081,3082],{"class":3021},"    id           UUID ",[2994,3084,3085],{"class":3007},"PRIMARY KEY",[2994,3087,3088],{"class":3021},",\n",[2994,3090,3092,3095,3098],{"class":2996,"line":3091},6,[2994,3093,3094],{"class":3021},"    audiobook_id UUID ",[2994,3096,3097],{"class":3007},"NOT NULL",[2994,3099,3088],{"class":3021},[2994,3101,3103,3106,3109,3112,3116,3119,3121],{"class":2996,"line":3102},7,[2994,3104,3105],{"class":3021},"    file_path    ",[2994,3107,3108],{"class":3007},"VARCHAR",[2994,3110,3111],{"class":3021},"(",[2994,3113,3115],{"class":3114},"sJj4R","2048",[2994,3117,3118],{"class":3021},") ",[2994,3120,3097],{"class":3007},[2994,3122,3088],{"class":3021},[2994,3124,3126,3129,3131,3134],{"class":2996,"line":3125},8,[2994,3127,3128],{"class":3021},"    format       file_format_enum ",[2994,3130,3097],{"class":3007},[2994,3132,3133],{"class":3021},",  ",[2994,3135,3136],{"class":3000},"-- ← PostgreSQL ENUM\n",[2994,3138,3140,3143],{"class":2996,"line":3139},9,[2994,3141,3142],{"class":3007},"    size",[2994,3144,3145],{"class":3007},"         INTEGER\n",[2994,3147,3149],{"class":2996,"line":3148},10,[2994,3150,3055],{"class":3021},[2964,3152,3153,3154,3157,3158,3160,3161,3164],{},"H2 до версії 2.0 ",[2971,3155,3156],{},"не підтримував ENUM взагалі",". У версії 2.x з'явилася підтримка, але з обмеженнями: ENUM у H2 є синтаксичним цукром над ",[2991,3159,3108],{}," з ",[2991,3162,3163],{},"CHECK"," constraint, а не окремим типом даних, як у PostgreSQL.",[2964,3166,3167,3170],{},[2971,3168,3169],{},"Наслідок:"," Тести на H2 можуть проходити успішно, але код провалиться у production:",[2984,3172,3176],{"className":3173,"code":3174,"language":3175,"meta":2989,"style":2989},"language-java shiki shiki-themes light-plus dark-plus dark-plus","// Цей код працює на H2, але провалюється на PostgreSQL\nString sql = \"INSERT INTO audiobook_files (id, audiobook_id, file_path, format, size) \" +\n             \"VALUES (?, ?, ?, ?, ?)\";\nstmt.setString(4, \"mp3\"); // H2: OK (VARCHAR)\n                          // PostgreSQL: ERROR — потрібно CAST('mp3' AS file_format_enum)\n","java",[2991,3177,3178,3183,3202,3210,3237],{"__ignoreMap":2989},[2994,3179,3180],{"class":2996,"line":2997},[2994,3181,3182],{"class":3000},"// Цей код працює на H2, але провалюється на PostgreSQL\n",[2994,3184,3185,3189,3193,3196,3199],{"class":2996,"line":3004},[2994,3186,3188],{"class":3187},"sN1BT","String",[2994,3190,3192],{"class":3191},"siwwj"," sql",[2994,3194,3195],{"class":3021}," = ",[2994,3197,3198],{"class":3025},"\"INSERT INTO audiobook_files (id, audiobook_id, file_path, format, size) \"",[2994,3200,3201],{"class":3021}," +\n",[2994,3203,3204,3207],{"class":2996,"line":3058},[2994,3205,3206],{"class":3025},"             \"VALUES (?, ?, ?, ?, ?)\"",[2994,3208,3209],{"class":3021},";\n",[2994,3211,3212,3215,3218,3221,3223,3226,3228,3231,3234],{"class":2996,"line":3065},[2994,3213,3214],{"class":3191},"stmt",[2994,3216,3217],{"class":3021},".",[2994,3219,3220],{"class":3014},"setString",[2994,3222,3111],{"class":3021},[2994,3224,3225],{"class":3114},"4",[2994,3227,3029],{"class":3021},[2994,3229,3230],{"class":3025},"\"mp3\"",[2994,3232,3233],{"class":3021},"); ",[2994,3235,3236],{"class":3000},"// H2: OK (VARCHAR)\n",[2994,3238,3239],{"class":2996,"line":3079},[2994,3240,3241],{"class":3000},"                          // PostgreSQL: ERROR — потрібно CAST('mp3' AS file_format_enum)\n",[2964,3243,3244],{},"Правильний код для PostgreSQL:",[2984,3246,3248],{"className":3173,"code":3247,"language":3175,"meta":2989,"style":2989},"// PostgreSQL вимагає явного приведення типу або використання setObject\nstmt.setObject(4, \"mp3\", java.sql.Types.OTHER);\n// або через PGobject (PostgreSQL JDBC driver)\n",[2991,3249,3250,3255,3292],{"__ignoreMap":2989},[2994,3251,3252],{"class":2996,"line":2997},[2994,3253,3254],{"class":3000},"// PostgreSQL вимагає явного приведення типу або використання setObject\n",[2994,3256,3257,3259,3261,3264,3266,3268,3270,3272,3274,3276,3278,3280,3282,3285,3287,3290],{"class":2996,"line":3004},[2994,3258,3214],{"class":3191},[2994,3260,3217],{"class":3021},[2994,3262,3263],{"class":3014},"setObject",[2994,3265,3111],{"class":3021},[2994,3267,3225],{"class":3114},[2994,3269,3029],{"class":3021},[2994,3271,3230],{"class":3025},[2994,3273,3029],{"class":3021},[2994,3275,3175],{"class":3191},[2994,3277,3217],{"class":3021},[2994,3279,2988],{"class":3191},[2994,3281,3217],{"class":3021},[2994,3283,3284],{"class":3191},"Types",[2994,3286,3217],{"class":3021},[2994,3288,3289],{"class":3191},"OTHER",[2994,3291,3055],{"class":3021},[2994,3293,3294],{"class":2996,"line":3058},[2994,3295,3296],{"class":3000},"// або через PGobject (PostgreSQL JDBC driver)\n",[2964,3298,3299,3302,3303,3217],{},[2971,3300,3301],{},"Проблема поглиблюється:"," Якщо розробник тестує лише на H2, він не дізнається про цю помилку до deployment у production. Це порушує фундаментальний принцип тестування: ",[2971,3304,3305],{},"тестове оточення має максимально відповідати production",[3307,3308],"hr",{},[2976,3310,3312],{"id":3311},"проблема-2-відмінності-у-типах-даних","Проблема 2: Відмінності у типах даних",[2964,3314,3315],{},"PostgreSQL підтримує багаті типи даних, що не мають аналогів у H2:",[3317,3318,3319,3338],"table",{},[3320,3321,3322],"thead",{},[3323,3324,3325,3329,3332,3335],"tr",{},[3326,3327,3328],"th",{},"Тип PostgreSQL",[3326,3330,3331],{},"Призначення",[3326,3333,3334],{},"Аналог у H2",[3326,3336,3337],{},"Проблема",[3339,3340,3341,3362,3393,3411,3432,3448],"tbody",{},[3323,3342,3343,3349,3352,3359],{},[3344,3345,3346],"td",{},[2991,3347,3348],{},"ENUM",[3344,3350,3351],{},"Перелічення значень",[3344,3353,3354,3356,3357],{},[2991,3355,3108],{}," + ",[2991,3358,3163],{},[3344,3360,3361],{},"Немає type safety на рівні БД",[3323,3363,3364,3373,3376,3380],{},[3344,3365,3366,3369,3370],{},[2991,3367,3368],{},"JSON"," / ",[2991,3371,3372],{},"JSONB",[3344,3374,3375],{},"Структуровані дані",[3344,3377,3378],{},[2991,3379,3108],{},[3344,3381,3382,3383,3029,3386,3029,3389,3392],{},"Немає JSON-операторів (",[2991,3384,3385],{},"->",[2991,3387,3388],{},"->>",[2991,3390,3391],{},"@>",")",[3323,3394,3395,3400,3402,3405],{},[3344,3396,3397],{},[2991,3398,3399],{},"ARRAY",[3344,3401,32],{},[3344,3403,3404],{},"Немає",[3344,3406,3407,3408],{},"Неможливо протестувати ",[2991,3409,3410],{},"ANY(array)",[3323,3412,3413,3421,3424,3426],{},[3344,3414,3415,3369,3418],{},[2991,3416,3417],{},"TSQUERY",[2991,3419,3420],{},"TSVECTOR",[3344,3422,3423],{},"Full-text search",[3344,3425,3404],{},[3344,3427,3407,3428,3431],{},[2991,3429,3430],{},"@@"," оператор",[3323,3433,3434,3439,3442,3445],{},[3344,3435,3436],{},[2991,3437,3438],{},"INTERVAL",[3344,3440,3441],{},"Часові інтервали",[3344,3443,3444],{},"Обмежена підтримка",[3344,3446,3447],{},"Різна семантика арифметики",[3323,3449,3450,3458,3461,3466],{},[3344,3451,3452,3369,3455],{},[2991,3453,3454],{},"SERIAL",[2991,3456,3457],{},"BIGSERIAL",[3344,3459,3460],{},"Автоінкремент",[3344,3462,3463],{},[2991,3464,3465],{},"IDENTITY",[3344,3467,3468],{},"Різний синтаксис",[2964,3470,3471],{},[2971,3472,3473],{},"Приклад з JSON:",[2984,3475,3477],{"className":2986,"code":3476,"language":2988,"meta":2989,"style":2989},"-- PostgreSQL: пошук у JSON-полі\nSELECT * FROM audiobooks \nWHERE metadata->>'language' = 'ukrainian'\n  AND metadata->'tags' @> '[\"classic\"]';\n",[2991,3478,3479,3484,3498,3514],{"__ignoreMap":2989},[2994,3480,3481],{"class":2996,"line":2997},[2994,3482,3483],{"class":3000},"-- PostgreSQL: пошук у JSON-полі\n",[2994,3485,3486,3489,3492,3495],{"class":2996,"line":3004},[2994,3487,3488],{"class":3007},"SELECT",[2994,3490,3491],{"class":3021}," * ",[2994,3493,3494],{"class":3007},"FROM",[2994,3496,3497],{"class":3021}," audiobooks \n",[2994,3499,3500,3503,3506,3509,3511],{"class":2996,"line":3058},[2994,3501,3502],{"class":3007},"WHERE",[2994,3504,3505],{"class":3021}," metadata->>",[2994,3507,3508],{"class":3025},"'language'",[2994,3510,3195],{"class":3021},[2994,3512,3513],{"class":3025},"'ukrainian'\n",[2994,3515,3516,3519,3522,3525,3528,3531],{"class":2996,"line":3065},[2994,3517,3518],{"class":3007},"  AND",[2994,3520,3521],{"class":3021}," metadata->",[2994,3523,3524],{"class":3025},"'tags'",[2994,3526,3527],{"class":3021}," @> ",[2994,3529,3530],{"class":3025},"'[\"classic\"]'",[2994,3532,3209],{"class":3021},[2964,3534,3535,3536,3539,3540,3029,3542,3029,3544,3546],{},"Цей запит ",[2971,3537,3538],{},"синтаксично некоректний"," для H2 — оператори ",[2991,3541,3385],{},[2991,3543,3388],{},[2991,3545,3391],{}," не існують. Тест на H2 провалиться на етапі парсингу SQL, але це не означає, що код некоректний — він просто не може бути протестований на H2.",[3307,3548],{},[2976,3550,3552],{"id":3551},"проблема-3-відмінності-у-поведінці-оптимізатора","Проблема 3: Відмінності у поведінці оптимізатора",[2964,3554,3555,3556,3559,3560,3563],{},"Навіть коли SQL синтаксично сумісний, ",[2971,3557,3558],{},"оптимізатор запитів"," H2 і PostgreSQL працює по-різному. Розглянемо запит з ",[2991,3561,3562],{},"LEFT JOIN",":",[2984,3565,3567],{"className":2986,"code":3566,"language":2988,"meta":2989,"style":2989},"SELECT a.*, COUNT(ab.id) AS book_count\nFROM authors a\nLEFT JOIN audiobooks ab ON a.id = ab.author_id\nGROUP BY a.id;\n",[2991,3568,3569,3588,3595,3608],{"__ignoreMap":2989},[2994,3570,3571,3573,3576,3579,3582,3585],{"class":2996,"line":2997},[2994,3572,3488],{"class":3007},[2994,3574,3575],{"class":3021}," a.*, ",[2994,3577,3578],{"class":3014},"COUNT",[2994,3580,3581],{"class":3021},"(ab.id) ",[2994,3583,3584],{"class":3007},"AS",[2994,3586,3587],{"class":3021}," book_count\n",[2994,3589,3590,3592],{"class":2996,"line":3004},[2994,3591,3494],{"class":3007},[2994,3593,3594],{"class":3021}," authors a\n",[2994,3596,3597,3599,3602,3605],{"class":2996,"line":3058},[2994,3598,3562],{"class":3007},[2994,3600,3601],{"class":3021}," audiobooks ab ",[2994,3603,3604],{"class":3007},"ON",[2994,3606,3607],{"class":3021}," a.id = ab.author_id\n",[2994,3609,3610,3613],{"class":2996,"line":3065},[2994,3611,3612],{"class":3007},"GROUP BY",[2994,3614,3615],{"class":3021}," a.id;\n",[2964,3617,3618,3621,3622,3624,3625,3627],{},[2971,3619,3620],{},"H2:"," Виконує ",[2991,3623,3562],{}," → ",[2991,3626,3612],{}," → повертає результат. Час виконання: ~5ms для 1000 авторів.",[2964,3629,3630,3633],{},[2971,3631,3632],{},"PostgreSQL:"," Може обрати інший план виконання залежно від статистики таблиць, наявності індексів, версії PostgreSQL. Час виконання: може варіюватися від 3ms до 50ms.",[2964,3635,3636,3638],{},[2971,3637,3169],{}," Тест продуктивності на H2 не відображає реальну продуктивність у production. Запит, що виконується за 5ms на H2, може виконуватися 200ms на PostgreSQL через відсутність індексу, що не був виявлений у тестах.",[3307,3640],{},[2976,3642,3644],{"id":3643},"проблема-4-транзакційна-ізоляція","Проблема 4: Транзакційна ізоляція",[2964,3646,3647,3648,3160,3651,3654],{},"PostgreSQL за замовчуванням використовує рівень ізоляції ",[2991,3649,3650],{},"READ COMMITTED",[2971,3652,3653],{},"MVCC"," (Multi-Version Concurrency Control). H2 використовує інший механізм блокувань.",[2964,3656,3657,3660],{},[2971,3658,3659],{},"Сценарій:"," Два паралельних транзакції оновлюють один рядок.",[2984,3662,3664],{"className":3173,"code":3663,"language":3175,"meta":2989,"style":2989},"// Транзакція 1\nUPDATE authors SET bio = 'Нова біографія' WHERE id = ?;\n// Транзакція 2 (паралельно)\nUPDATE authors SET image_path = '/new.jpg' WHERE id = ?; // той самий id\n",[2991,3665,3666,3671,3704,3709],{"__ignoreMap":2989},[2994,3667,3668],{"class":2996,"line":2997},[2994,3669,3670],{"class":3000},"// Транзакція 1\n",[2994,3672,3673,3676,3679,3682,3685,3687,3690,3693,3696,3698,3702],{"class":2996,"line":3004},[2994,3674,3675],{"class":3187},"UPDATE",[2994,3677,3678],{"class":3021}," authors ",[2994,3680,3681],{"class":3187},"SET",[2994,3683,3684],{"class":3191}," bio",[2994,3686,3195],{"class":3021},[2994,3688,3689],{"class":3025},"'Нова біографія'",[2994,3691,3692],{"class":3187}," WHERE",[2994,3694,3695],{"class":3191}," id",[2994,3697,3195],{"class":3021},[2994,3699,3701],{"class":3700},"sCDza","?",[2994,3703,3209],{"class":3021},[2994,3705,3706],{"class":2996,"line":3058},[2994,3707,3708],{"class":3000},"// Транзакція 2 (паралельно)\n",[2994,3710,3711,3713,3715,3717,3720,3722,3725,3727,3729,3731,3733,3736],{"class":2996,"line":3065},[2994,3712,3675],{"class":3187},[2994,3714,3678],{"class":3021},[2994,3716,3681],{"class":3187},[2994,3718,3719],{"class":3191}," image_path",[2994,3721,3195],{"class":3021},[2994,3723,3724],{"class":3025},"'/new.jpg'",[2994,3726,3692],{"class":3187},[2994,3728,3695],{"class":3191},[2994,3730,3195],{"class":3021},[2994,3732,3701],{"class":3700},[2994,3734,3735],{"class":3021},"; ",[2994,3737,3738],{"class":3000},"// той самий id\n",[2964,3740,3741,3743],{},[2971,3742,3632],{}," Транзакція 2 блокується до завершення транзакції 1 (row-level lock). Після commit транзакції 1 — транзакція 2 продовжує виконання.",[2964,3745,3746,3748,3749,3752],{},[2971,3747,3620],{}," Може використовувати table-level lock або інший механізм. Поведінка може відрізнятися, особливо при ",[2991,3750,3751],{},"SERIALIZABLE"," ізоляції.",[2964,3754,3755,3757],{},[2971,3756,3169],{}," Тести на H2 не виявлять deadlock або race condition, що виникнуть у production при високому навантаженні.",[3307,3759],{},[2959,3761,3763],{"id":3762},"концепція-testcontainers","Концепція: Testcontainers",[2964,3765,3766,3769,3770,3776,3777,3780],{},[2971,3767,3768],{},"Testcontainers"," (",[3771,3772,3773],"a",{"href":3773,"rel":3774},"https://testcontainers.com",[3775],"nofollow",") — це Java-бібліотека, що дозволяє запускати ",[2971,3778,3779],{},"реальні Docker-контейнери"," як частину інтеграційних тестів. Замість емуляції БД через H2, Testcontainers запускає справжню PostgreSQL у Docker-контейнері, виконує тести, і автоматично видаляє контейнер після завершення.",[2976,3782,3784],{"id":3783},"архітектура-testcontainers","Архітектура Testcontainers",[3786,3787,3788],"mermaid",{},[2984,3789,3792],{"className":3790,"code":3791,"language":3786,"meta":2989,"style":2989},"language-mermaid shiki shiki-themes light-plus dark-plus dark-plus","graph TB\n    subgraph \"JVM Process (Тести)\"\n        JUnit[\"JUnit 5 Test Runner\"]\n        TC[\"Testcontainers API\"]\n        JDBC[\"JDBC Driver\"]\n        Repo[\"Repository під тестом\"]\n    end\n\n    subgraph \"Docker Engine\"\n        PG[\"PostgreSQL Container\u003Cbr/>(postgres:15-alpine)\"]\n        Net[\"Docker Network\u003Cbr/>(bridge)\"]\n    end\n\n    JUnit -->|\"@BeforeAll\"| TC\n    TC -->|\"docker run postgres:15\"| Docker[\"Docker CLI/API\"]\n    Docker -->|\"створює\"| PG\n    TC -->|\"отримує\"| Port[\"Динамічний порт\u003Cbr/>(напр. 32768)\"]\n    TC -->|\"передає\"| JDBC\n    JDBC -->|\"TCP 32768\"| PG\n    Repo -->|\"SQL\"| JDBC\n\n    JUnit -->|\"@AfterAll\"| TC\n    TC -->|\"docker stop + rm\"| Docker\n    Docker -->|\"видаляє\"| PG\n\n    style JUnit fill:#3b82f6,stroke:#1d4ed8,color:#ffffff\n    style TC fill:#f59e0b,stroke:#b45309,color:#000000\n    style PG fill:#22c55e,stroke:#15803d,color:#ffffff\n    style Docker fill:#64748b,stroke:#334155,color:#ffffff\n",[2991,3793,3794,3799,3804,3809,3814,3819,3824,3829,3833,3838,3843,3849,3854,3859,3865,3871,3877,3883,3889,3895,3901,3906,3912,3918,3924,3929,3935,3941,3947],{"__ignoreMap":2989},[2994,3795,3796],{"class":2996,"line":2997},[2994,3797,3798],{},"graph TB\n",[2994,3800,3801],{"class":2996,"line":3004},[2994,3802,3803],{},"    subgraph \"JVM Process (Тести)\"\n",[2994,3805,3806],{"class":2996,"line":3058},[2994,3807,3808],{},"        JUnit[\"JUnit 5 Test Runner\"]\n",[2994,3810,3811],{"class":2996,"line":3065},[2994,3812,3813],{},"        TC[\"Testcontainers API\"]\n",[2994,3815,3816],{"class":2996,"line":3079},[2994,3817,3818],{},"        JDBC[\"JDBC Driver\"]\n",[2994,3820,3821],{"class":2996,"line":3091},[2994,3822,3823],{},"        Repo[\"Repository під тестом\"]\n",[2994,3825,3826],{"class":2996,"line":3102},[2994,3827,3828],{},"    end\n",[2994,3830,3831],{"class":2996,"line":3125},[2994,3832,3062],{"emptyLinePlaceholder":3061},[2994,3834,3835],{"class":2996,"line":3139},[2994,3836,3837],{},"    subgraph \"Docker Engine\"\n",[2994,3839,3840],{"class":2996,"line":3148},[2994,3841,3842],{},"        PG[\"PostgreSQL Container\u003Cbr/>(postgres:15-alpine)\"]\n",[2994,3844,3846],{"class":2996,"line":3845},11,[2994,3847,3848],{},"        Net[\"Docker Network\u003Cbr/>(bridge)\"]\n",[2994,3850,3852],{"class":2996,"line":3851},12,[2994,3853,3828],{},[2994,3855,3857],{"class":2996,"line":3856},13,[2994,3858,3062],{"emptyLinePlaceholder":3061},[2994,3860,3862],{"class":2996,"line":3861},14,[2994,3863,3864],{},"    JUnit -->|\"@BeforeAll\"| TC\n",[2994,3866,3868],{"class":2996,"line":3867},15,[2994,3869,3870],{},"    TC -->|\"docker run postgres:15\"| Docker[\"Docker CLI/API\"]\n",[2994,3872,3874],{"class":2996,"line":3873},16,[2994,3875,3876],{},"    Docker -->|\"створює\"| PG\n",[2994,3878,3880],{"class":2996,"line":3879},17,[2994,3881,3882],{},"    TC -->|\"отримує\"| Port[\"Динамічний порт\u003Cbr/>(напр. 32768)\"]\n",[2994,3884,3886],{"class":2996,"line":3885},18,[2994,3887,3888],{},"    TC -->|\"передає\"| JDBC\n",[2994,3890,3892],{"class":2996,"line":3891},19,[2994,3893,3894],{},"    JDBC -->|\"TCP 32768\"| PG\n",[2994,3896,3898],{"class":2996,"line":3897},20,[2994,3899,3900],{},"    Repo -->|\"SQL\"| JDBC\n",[2994,3902,3904],{"class":2996,"line":3903},21,[2994,3905,3062],{"emptyLinePlaceholder":3061},[2994,3907,3909],{"class":2996,"line":3908},22,[2994,3910,3911],{},"    JUnit -->|\"@AfterAll\"| TC\n",[2994,3913,3915],{"class":2996,"line":3914},23,[2994,3916,3917],{},"    TC -->|\"docker stop + rm\"| Docker\n",[2994,3919,3921],{"class":2996,"line":3920},24,[2994,3922,3923],{},"    Docker -->|\"видаляє\"| PG\n",[2994,3925,3927],{"class":2996,"line":3926},25,[2994,3928,3062],{"emptyLinePlaceholder":3061},[2994,3930,3932],{"class":2996,"line":3931},26,[2994,3933,3934],{},"    style JUnit fill:#3b82f6,stroke:#1d4ed8,color:#ffffff\n",[2994,3936,3938],{"class":2996,"line":3937},27,[2994,3939,3940],{},"    style TC fill:#f59e0b,stroke:#b45309,color:#000000\n",[2994,3942,3944],{"class":2996,"line":3943},28,[2994,3945,3946],{},"    style PG fill:#22c55e,stroke:#15803d,color:#ffffff\n",[2994,3948,3950],{"class":2996,"line":3949},29,[2994,3951,3952],{},"    style Docker fill:#64748b,stroke:#334155,color:#ffffff\n",[2964,3954,3955],{},[2971,3956,3957],{},"Ключові компоненти:",[3959,3960,3961,3968,3974,3980,3986],"ol",{},[3962,3963,3964,3967],"li",{},[2971,3965,3966],{},"Testcontainers API:"," Java-бібліотека, що керує lifecycle Docker-контейнерів через Docker Engine API.",[3962,3969,3970,3973],{},[2971,3971,3972],{},"Docker Engine:"," Демон Docker, що запускає контейнери. Має бути встановлений на машині розробника або CI-сервері.",[3962,3975,3976,3979],{},[2971,3977,3978],{},"PostgreSQL Container:"," Офіційний Docker-образ PostgreSQL (з Docker Hub), що запускається у ізольованому контейнері.",[3962,3981,3982,3985],{},[2971,3983,3984],{},"Динамічний порт:"," Testcontainers автоматично призначає вільний порт на host-машині (наприклад, 32768) і проксує його до порту 5432 всередині контейнера.",[3962,3987,3988,3991],{},[2971,3989,3990],{},"JDBC Connection:"," Тести підключаються до PostgreSQL через звичайний JDBC, використовуючи динамічний порт.",[3307,3993],{},[2976,3995,3997],{"id":3996},"lifecycle-контейнера-у-тестах","Lifecycle контейнера у тестах",[2964,3999,4000],{},"Testcontainers підтримує кілька стратегій управління lifecycle контейнерів:",[4002,4003,4005],"h4",{"id":4004},"стратегія-1-per-test-class-рекомендовано","Стратегія 1: Per-Test-Class (рекомендовано)",[2964,4007,4008],{},"Один контейнер на весь тестовий клас — запускається перед першим тестом, видаляється після останнього.",[2984,4010,4012],{"className":3173,"code":4011,"language":3175,"meta":2989,"style":2989},"@Testcontainers // JUnit 5 extension\nclass AuthorRepositoryTest {\n\n    @Container // керується Testcontainers\n    static PostgreSQLContainer\u003C?> postgres = new PostgreSQLContainer\u003C>(\"postgres:15-alpine\")\n        .withDatabaseName(\"test_db\")\n        .withUsername(\"test_user\")\n        .withPassword(\"test_pass\");\n\n    @Test\n    void test1() { /* використовує той самий контейнер */ }\n\n    @Test\n    void test2() { /* використовує той самий контейнер */ }\n}\n",[2991,4013,4014,4024,4035,4039,4050,4085,4100,4114,4128,4132,4139,4156,4160,4166,4179],{"__ignoreMap":2989},[2994,4015,4016,4019,4021],{"class":2996,"line":2997},[2994,4017,4018],{"class":3021},"@",[2994,4020,3768],{"class":3187},[2994,4022,4023],{"class":3000}," // JUnit 5 extension\n",[2994,4025,4026,4029,4032],{"class":2996,"line":3004},[2994,4027,4028],{"class":3007},"class",[2994,4030,4031],{"class":3187}," AuthorRepositoryTest",[2994,4033,4034],{"class":3021}," {\n",[2994,4036,4037],{"class":2996,"line":3058},[2994,4038,3062],{"emptyLinePlaceholder":3061},[2994,4040,4041,4044,4047],{"class":2996,"line":3065},[2994,4042,4043],{"class":3021},"    @",[2994,4045,4046],{"class":3187},"Container",[2994,4048,4049],{"class":3000}," // керується Testcontainers\n",[2994,4051,4052,4055,4058,4061,4063,4066,4069,4071,4074,4076,4079,4082],{"class":2996,"line":3079},[2994,4053,4054],{"class":3007},"    static",[2994,4056,4057],{"class":3187}," PostgreSQLContainer",[2994,4059,4060],{"class":3021},"\u003C",[2994,4062,3701],{"class":3007},[2994,4064,4065],{"class":3021},"> ",[2994,4067,4068],{"class":3191},"postgres",[2994,4070,3195],{"class":3021},[2994,4072,4073],{"class":3700},"new",[2994,4075,4057],{"class":3187},[2994,4077,4078],{"class":3021},"\u003C>(",[2994,4080,4081],{"class":3025},"\"postgres:15-alpine\"",[2994,4083,4084],{"class":3021},")\n",[2994,4086,4087,4090,4093,4095,4098],{"class":2996,"line":3091},[2994,4088,4089],{"class":3021},"        .",[2994,4091,4092],{"class":3014},"withDatabaseName",[2994,4094,3111],{"class":3021},[2994,4096,4097],{"class":3025},"\"test_db\"",[2994,4099,4084],{"class":3021},[2994,4101,4102,4104,4107,4109,4112],{"class":2996,"line":3102},[2994,4103,4089],{"class":3021},[2994,4105,4106],{"class":3014},"withUsername",[2994,4108,3111],{"class":3021},[2994,4110,4111],{"class":3025},"\"test_user\"",[2994,4113,4084],{"class":3021},[2994,4115,4116,4118,4121,4123,4126],{"class":2996,"line":3125},[2994,4117,4089],{"class":3021},[2994,4119,4120],{"class":3014},"withPassword",[2994,4122,3111],{"class":3021},[2994,4124,4125],{"class":3025},"\"test_pass\"",[2994,4127,3055],{"class":3021},[2994,4129,4130],{"class":2996,"line":3139},[2994,4131,3062],{"emptyLinePlaceholder":3061},[2994,4133,4134,4136],{"class":2996,"line":3148},[2994,4135,4043],{"class":3021},[2994,4137,4138],{"class":3187},"Test\n",[2994,4140,4141,4144,4147,4150,4153],{"class":2996,"line":3845},[2994,4142,4143],{"class":3187},"    void",[2994,4145,4146],{"class":3014}," test1",[2994,4148,4149],{"class":3021},"() { ",[2994,4151,4152],{"class":3000},"/* використовує той самий контейнер */",[2994,4154,4155],{"class":3021}," }\n",[2994,4157,4158],{"class":2996,"line":3851},[2994,4159,3062],{"emptyLinePlaceholder":3061},[2994,4161,4162,4164],{"class":2996,"line":3856},[2994,4163,4043],{"class":3021},[2994,4165,4138],{"class":3187},[2994,4167,4168,4170,4173,4175,4177],{"class":2996,"line":3861},[2994,4169,4143],{"class":3187},[2994,4171,4172],{"class":3014}," test2",[2994,4174,4149],{"class":3021},[2994,4176,4152],{"class":3000},[2994,4178,4155],{"class":3021},[2994,4180,4181],{"class":2996,"line":3867},[2994,4182,4183],{"class":3021},"}\n",[2964,4185,4186],{},[2971,4187,4188],{},"Lifecycle:",[2984,4190,4195],{"className":4191,"code":4193,"language":4194},[4192],"language-text","@BeforeAll (JUnit)\n  → Testcontainers запускає контейнер\n  → Чекає на готовність PostgreSQL (health check)\n  → Повертає JDBC URL з динамічним портом\n\ntest1() виконується\ntest2() виконується\n...\n\n@AfterAll (JUnit)\n  → Testcontainers зупиняє контейнер\n  → Видаляє контейнер (docker rm)\n","text",[2991,4196,4193],{"__ignoreMap":2989},[2964,4198,4199],{},[2971,4200,4201],{},"Переваги:",[4203,4204,4205,4208],"ul",{},[3962,4206,4207],{},"Швидше за per-test (контейнер запускається один раз)",[3962,4209,4210,4211,3392],{},"Тести все одно ізольовані (кожен тест очищає дані через ",[2991,4212,4213],{},"@BeforeEach",[2964,4215,4216],{},[2971,4217,4218],{},"Недоліки:",[4203,4220,4221],{},[3962,4222,4223],{},"Якщо один тест «забруднює» БД і не очищає — наступний тест може провалитися",[3307,4225],{},[4002,4227,4229],{"id":4228},"стратегія-2-singleton-container-найшвидша","Стратегія 2: Singleton Container (найшвидша)",[2964,4231,4232,4233,4236],{},"Один контейнер для ",[2971,4234,4235],{},"всіх"," тестових класів у проєкті. Запускається один раз при першому тесті, живе до завершення JVM.",[2984,4238,4240],{"className":3173,"code":4239,"language":3175,"meta":2989,"style":2989},"public abstract class AbstractIntegrationTest {\n\n    private static final PostgreSQLContainer\u003C?> POSTGRES;\n\n    static {\n        POSTGRES = new PostgreSQLContainer\u003C>(\"postgres:15-alpine\")\n            .withDatabaseName(\"test_db\")\n            .withUsername(\"test\")\n            .withPassword(\"test\")\n            .withReuse(true); // ← дозволити повторне використання\n\n        POSTGRES.start();\n    }\n\n    protected static String getJdbcUrl() {\n        return POSTGRES.getJdbcUrl();\n    }\n}\n",[2991,4241,4242,4258,4262,4286,4290,4296,4311,4324,4337,4349,4366,4370,4383,4388,4392,4408,4423,4427],{"__ignoreMap":2989},[2994,4243,4244,4247,4250,4253,4256],{"class":2996,"line":2997},[2994,4245,4246],{"class":3007},"public",[2994,4248,4249],{"class":3007}," abstract",[2994,4251,4252],{"class":3007}," class",[2994,4254,4255],{"class":3187}," AbstractIntegrationTest",[2994,4257,4034],{"class":3021},[2994,4259,4260],{"class":2996,"line":3004},[2994,4261,3062],{"emptyLinePlaceholder":3061},[2994,4263,4264,4267,4270,4273,4275,4277,4279,4281,4284],{"class":2996,"line":3058},[2994,4265,4266],{"class":3007},"    private",[2994,4268,4269],{"class":3007}," static",[2994,4271,4272],{"class":3007}," final",[2994,4274,4057],{"class":3187},[2994,4276,4060],{"class":3021},[2994,4278,3701],{"class":3007},[2994,4280,4065],{"class":3021},[2994,4282,4283],{"class":3191},"POSTGRES",[2994,4285,3209],{"class":3021},[2994,4287,4288],{"class":2996,"line":3065},[2994,4289,3062],{"emptyLinePlaceholder":3061},[2994,4291,4292,4294],{"class":2996,"line":3079},[2994,4293,4054],{"class":3007},[2994,4295,4034],{"class":3021},[2994,4297,4298,4301,4303,4305,4307,4309],{"class":2996,"line":3091},[2994,4299,4300],{"class":3021},"        POSTGRES = ",[2994,4302,4073],{"class":3700},[2994,4304,4057],{"class":3187},[2994,4306,4078],{"class":3021},[2994,4308,4081],{"class":3025},[2994,4310,4084],{"class":3021},[2994,4312,4313,4316,4318,4320,4322],{"class":2996,"line":3102},[2994,4314,4315],{"class":3021},"            .",[2994,4317,4092],{"class":3014},[2994,4319,3111],{"class":3021},[2994,4321,4097],{"class":3025},[2994,4323,4084],{"class":3021},[2994,4325,4326,4328,4330,4332,4335],{"class":2996,"line":3125},[2994,4327,4315],{"class":3021},[2994,4329,4106],{"class":3014},[2994,4331,3111],{"class":3021},[2994,4333,4334],{"class":3025},"\"test\"",[2994,4336,4084],{"class":3021},[2994,4338,4339,4341,4343,4345,4347],{"class":2996,"line":3139},[2994,4340,4315],{"class":3021},[2994,4342,4120],{"class":3014},[2994,4344,3111],{"class":3021},[2994,4346,4334],{"class":3025},[2994,4348,4084],{"class":3021},[2994,4350,4351,4353,4356,4358,4361,4363],{"class":2996,"line":3148},[2994,4352,4315],{"class":3021},[2994,4354,4355],{"class":3014},"withReuse",[2994,4357,3111],{"class":3021},[2994,4359,4360],{"class":3007},"true",[2994,4362,3233],{"class":3021},[2994,4364,4365],{"class":3000},"// ← дозволити повторне використання\n",[2994,4367,4368],{"class":2996,"line":3845},[2994,4369,3062],{"emptyLinePlaceholder":3061},[2994,4371,4372,4375,4377,4380],{"class":2996,"line":3851},[2994,4373,4374],{"class":3191},"        POSTGRES",[2994,4376,3217],{"class":3021},[2994,4378,4379],{"class":3014},"start",[2994,4381,4382],{"class":3021},"();\n",[2994,4384,4385],{"class":2996,"line":3856},[2994,4386,4387],{"class":3021},"    }\n",[2994,4389,4390],{"class":2996,"line":3861},[2994,4391,3062],{"emptyLinePlaceholder":3061},[2994,4393,4394,4397,4399,4402,4405],{"class":2996,"line":3867},[2994,4395,4396],{"class":3007},"    protected",[2994,4398,4269],{"class":3007},[2994,4400,4401],{"class":3187}," String",[2994,4403,4404],{"class":3014}," getJdbcUrl",[2994,4406,4407],{"class":3021},"() {\n",[2994,4409,4410,4413,4416,4418,4421],{"class":2996,"line":3873},[2994,4411,4412],{"class":3700},"        return",[2994,4414,4415],{"class":3191}," POSTGRES",[2994,4417,3217],{"class":3021},[2994,4419,4420],{"class":3014},"getJdbcUrl",[2994,4422,4382],{"class":3021},[2994,4424,4425],{"class":2996,"line":3879},[2994,4426,4387],{"class":3021},[2994,4428,4429],{"class":2996,"line":3885},[2994,4430,4183],{"class":3021},[2964,4432,4433],{},[2971,4434,4201],{},[4203,4436,4437,4440],{},[3962,4438,4439],{},"Максимальна швидкість (контейнер запускається один раз)",[3962,4441,4442],{},"Підходить для великих тестових наборів (сотні тестів)",[2964,4444,4445],{},[2971,4446,4218],{},[4203,4448,4449,4452],{},[3962,4450,4451],{},"Потребує ретельного очищення БД між тестами",[3962,4453,4454],{},"Складніше налагоджувати (контейнер живе після завершення тестів)",[3307,4456],{},[4002,4458,4460],{"id":4459},"стратегія-3-per-test-найповільніша-найізольованіша","Стратегія 3: Per-Test (найповільніша, найізольованіша)",[2964,4462,4463],{},"Новий контейнер для кожного тесту. Максимальна ізоляція, але дуже повільно.",[2984,4465,4467],{"className":3173,"code":4466,"language":3175,"meta":2989,"style":2989},"class AuthorRepositoryTest {\n\n    @Container // не static — новий контейнер для кожного тесту\n    PostgreSQLContainer\u003C?> postgres = new PostgreSQLContainer\u003C>(\"postgres:15-alpine\");\n\n    @Test\n    void test1() { /* власний контейнер */ }\n\n    @Test\n    void test2() { /* новий контейнер */ }\n}\n",[2991,4468,4469,4477,4481,4490,4515,4519,4525,4538,4542,4548,4561],{"__ignoreMap":2989},[2994,4470,4471,4473,4475],{"class":2996,"line":2997},[2994,4472,4028],{"class":3007},[2994,4474,4031],{"class":3187},[2994,4476,4034],{"class":3021},[2994,4478,4479],{"class":2996,"line":3004},[2994,4480,3062],{"emptyLinePlaceholder":3061},[2994,4482,4483,4485,4487],{"class":2996,"line":3058},[2994,4484,4043],{"class":3021},[2994,4486,4046],{"class":3187},[2994,4488,4489],{"class":3000}," // не static — новий контейнер для кожного тесту\n",[2994,4491,4492,4495,4497,4499,4501,4503,4505,4507,4509,4511,4513],{"class":2996,"line":3065},[2994,4493,4494],{"class":3187},"    PostgreSQLContainer",[2994,4496,4060],{"class":3021},[2994,4498,3701],{"class":3007},[2994,4500,4065],{"class":3021},[2994,4502,4068],{"class":3191},[2994,4504,3195],{"class":3021},[2994,4506,4073],{"class":3700},[2994,4508,4057],{"class":3187},[2994,4510,4078],{"class":3021},[2994,4512,4081],{"class":3025},[2994,4514,3055],{"class":3021},[2994,4516,4517],{"class":2996,"line":3079},[2994,4518,3062],{"emptyLinePlaceholder":3061},[2994,4520,4521,4523],{"class":2996,"line":3091},[2994,4522,4043],{"class":3021},[2994,4524,4138],{"class":3187},[2994,4526,4527,4529,4531,4533,4536],{"class":2996,"line":3102},[2994,4528,4143],{"class":3187},[2994,4530,4146],{"class":3014},[2994,4532,4149],{"class":3021},[2994,4534,4535],{"class":3000},"/* власний контейнер */",[2994,4537,4155],{"class":3021},[2994,4539,4540],{"class":2996,"line":3125},[2994,4541,3062],{"emptyLinePlaceholder":3061},[2994,4543,4544,4546],{"class":2996,"line":3139},[2994,4545,4043],{"class":3021},[2994,4547,4138],{"class":3187},[2994,4549,4550,4552,4554,4556,4559],{"class":2996,"line":3148},[2994,4551,4143],{"class":3187},[2994,4553,4172],{"class":3014},[2994,4555,4149],{"class":3021},[2994,4557,4558],{"class":3000},"/* новий контейнер */",[2994,4560,4155],{"class":3021},[2994,4562,4563],{"class":2996,"line":3845},[2994,4564,4183],{"class":3021},[2964,4566,4567],{},[2971,4568,4188],{},[2984,4570,4573],{"className":4571,"code":4572,"language":4194},[4192],"@BeforeEach\n  → Запустити новий контейнер\n  → Виконати DDL\n\ntest1()\n\n@AfterEach\n  → Зупинити контейнер\n\n@BeforeEach\n  → Запустити НОВИЙ контейнер\n  → Виконати DDL\n\ntest2()\n\n@AfterEach\n  → Зупинити контейнер\n",[2991,4574,4572],{"__ignoreMap":2989},[2964,4576,4577,4580],{},[2971,4578,4579],{},"Використання:"," Рідко. Лише коли тести модифікують схему БД (DDL) і не можуть бути ізольовані інакше.",[3307,4582],{},[2959,4584,4586],{"id":4585},"порівняння-підходів-h2-vs-testcontainers","Порівняння підходів: H2 vs Testcontainers",[2964,4588,4589],{},"Перш ніж перейти до реалізації, підсумуємо переваги та недоліки обох підходів:",[3317,4591,4592,4605],{},[3320,4593,4594],{},[3323,4595,4596,4599,4602],{},[3326,4597,4598],{},"Критерій",[3326,4600,4601],{},"Embedded H2",[3326,4603,4604],{},"Testcontainers + PostgreSQL",[3339,4606,4607,4620,4633,4646,4659,4672,4693,4709,4722,4735,4748],{},[3323,4608,4609,4614,4617],{},[3344,4610,4611],{},[2971,4612,4613],{},"Швидкість запуску",[3344,4615,4616],{},"⚡⚡⚡ Миттєво (in-memory)",[3344,4618,4619],{},"🐢 2–5 секунд (запуск контейнера)",[3323,4621,4622,4627,4630],{},[3344,4623,4624],{},[2971,4625,4626],{},"Швидкість виконання тестів",[3344,4628,4629],{},"⚡⚡⚡ Мілісекунди",[3344,4631,4632],{},"⚡⚡ Десятки мілісекунд",[3323,4634,4635,4640,4643],{},[3344,4636,4637],{},[2971,4638,4639],{},"Точність емуляції",[3344,4641,4642],{},"⚠️ 70–80% сумісність",[3344,4644,4645],{},"✅ 100% — реальна PostgreSQL",[3323,4647,4648,4653,4656],{},[3344,4649,4650],{},[2971,4651,4652],{},"Підтримка ENUM",[3344,4654,4655],{},"⚠️ Обмежена (H2 2.x)",[3344,4657,4658],{},"✅ Повна",[3323,4660,4661,4666,4669],{},[3344,4662,4663],{},[2971,4664,4665],{},"Підтримка JSON",[3344,4667,4668],{},"❌ Немає операторів",[3344,4670,4671],{},"✅ JSONB + всі оператори",[3323,4673,4674,4679,4682],{},[3344,4675,4676],{},[2971,4677,4678],{},"Підтримка full-text search",[3344,4680,4681],{},"❌ Немає",[3344,4683,4684,4685,3029,4688,3029,4691],{},"✅ ",[2991,4686,4687],{},"tsvector",[2991,4689,4690],{},"tsquery",[2991,4692,3430],{},[3323,4694,4695,4700,4702],{},[3344,4696,4697],{},[2971,4698,4699],{},"Підтримка масивів",[3344,4701,4681],{},[3344,4703,4684,4704,3029,4706],{},[2991,4705,3399],{},[2991,4707,4708],{},"ANY()",[3323,4710,4711,4716,4719],{},[3344,4712,4713],{},[2971,4714,4715],{},"Транзакційна ізоляція",[3344,4717,4718],{},"⚠️ Інша реалізація",[3344,4720,4721],{},"✅ Ідентична production",[3323,4723,4724,4729,4732],{},[3344,4725,4726],{},[2971,4727,4728],{},"Вимоги до оточення",[3344,4730,4731],{},"✅ Жодних (JAR-файл)",[3344,4733,4734],{},"⚠️ Потрібен Docker",[3323,4736,4737,4742,4745],{},[3344,4738,4739],{},[2971,4740,4741],{},"CI/CD інтеграція",[3344,4743,4744],{},"✅ Проста",[3344,4746,4747],{},"⚠️ Потрібен Docker-in-Docker",[3323,4749,4750,4755,4758],{},[3344,4751,4752],{},[2971,4753,4754],{},"Налагодження",[3344,4756,4757],{},"✅ Просте (in-process)",[3344,4759,4760],{},"⚠️ Складніше (окремий процес)",[4762,4763,4764,4769,4783],"tip",{},[2964,4765,4766],{},[2971,4767,4768],{},"Гібридна стратегія (рекомендовано для production):",[3959,4770,4771,4777],{},[3962,4772,4773,4776],{},[2971,4774,4775],{},"Швидкі тести на H2:"," Базові CRUD-операції, валідація constraints, прості запити. Виконуються при кожному збереженні файлу (IDE watch mode).",[3962,4778,4779,4782],{},[2971,4780,4781],{},"Повні тести на Testcontainers:"," PostgreSQL-специфічні функції, складні JOIN, транзакційна логіка, тести продуктивності. Виконуються перед commit та у CI/CD.",[2964,4784,4785],{},"Це дає баланс між швидкістю розробки та впевненістю у коректності коду.",[3307,4787],{},[2959,4789,4791],{"id":4790},"налаштування-testcontainers","Налаштування Testcontainers",[2976,4793,2586],{"id":4794},"встановлення-docker",[2964,4796,4797],{},"Testcontainers вимагає наявності Docker Engine на машині розробника. Процес встановлення залежить від операційної системи:",[2964,4799,4800],{},[2971,4801,4802],{},"macOS:",[2984,4804,4808],{"className":4805,"code":4806,"language":4807,"meta":2989,"style":2989},"language-bash shiki shiki-themes light-plus dark-plus dark-plus","# Через Homebrew\nbrew install --cask docker\n\n# Або завантажити Docker Desktop з docker.com\n# Docker Desktop включає Docker Engine + GUI\n","bash",[2991,4809,4810,4815,4829,4833,4838],{"__ignoreMap":2989},[2994,4811,4812],{"class":2996,"line":2997},[2994,4813,4814],{"class":3000},"# Через Homebrew\n",[2994,4816,4817,4820,4823,4826],{"class":2996,"line":3004},[2994,4818,4819],{"class":3014},"brew",[2994,4821,4822],{"class":3025}," install",[2994,4824,4825],{"class":3007}," --cask",[2994,4827,4828],{"class":3025}," docker\n",[2994,4830,4831],{"class":2996,"line":3058},[2994,4832,3062],{"emptyLinePlaceholder":3061},[2994,4834,4835],{"class":2996,"line":3065},[2994,4836,4837],{"class":3000},"# Або завантажити Docker Desktop з docker.com\n",[2994,4839,4840],{"class":2996,"line":3079},[2994,4841,4842],{"class":3000},"# Docker Desktop включає Docker Engine + GUI\n",[2964,4844,4845],{},[2971,4846,4847],{},"Linux (Ubuntu/Debian):",[2984,4849,4851],{"className":4805,"code":4850,"language":4807,"meta":2989,"style":2989},"# Встановити Docker Engine\nsudo apt-get update\nsudo apt-get install docker-ce docker-ce-cli containerd.io\n\n# Додати поточного користувача до групи docker (щоб не потрібен sudo)\nsudo usermod -aG docker $USER\nnewgrp docker\n\n# Перевірити встановлення\ndocker run hello-world\n",[2991,4852,4853,4858,4869,4886,4890,4895,4911,4918,4922,4927],{"__ignoreMap":2989},[2994,4854,4855],{"class":2996,"line":2997},[2994,4856,4857],{"class":3000},"# Встановити Docker Engine\n",[2994,4859,4860,4863,4866],{"class":2996,"line":3004},[2994,4861,4862],{"class":3014},"sudo",[2994,4864,4865],{"class":3025}," apt-get",[2994,4867,4868],{"class":3025}," update\n",[2994,4870,4871,4873,4875,4877,4880,4883],{"class":2996,"line":3058},[2994,4872,4862],{"class":3014},[2994,4874,4865],{"class":3025},[2994,4876,4822],{"class":3025},[2994,4878,4879],{"class":3025}," docker-ce",[2994,4881,4882],{"class":3025}," docker-ce-cli",[2994,4884,4885],{"class":3025}," containerd.io\n",[2994,4887,4888],{"class":2996,"line":3065},[2994,4889,3062],{"emptyLinePlaceholder":3061},[2994,4891,4892],{"class":2996,"line":3079},[2994,4893,4894],{"class":3000},"# Додати поточного користувача до групи docker (щоб не потрібен sudo)\n",[2994,4896,4897,4899,4902,4905,4908],{"class":2996,"line":3091},[2994,4898,4862],{"class":3014},[2994,4900,4901],{"class":3025}," usermod",[2994,4903,4904],{"class":3007}," -aG",[2994,4906,4907],{"class":3025}," docker",[2994,4909,4910],{"class":3191}," $USER\n",[2994,4912,4913,4916],{"class":2996,"line":3102},[2994,4914,4915],{"class":3014},"newgrp",[2994,4917,4828],{"class":3025},[2994,4919,4920],{"class":2996,"line":3125},[2994,4921,3062],{"emptyLinePlaceholder":3061},[2994,4923,4924],{"class":2996,"line":3139},[2994,4925,4926],{"class":3000},"# Перевірити встановлення\n",[2994,4928,4929,4932,4935],{"class":2996,"line":3148},[2994,4930,4931],{"class":3014},"docker",[2994,4933,4934],{"class":3025}," run",[2994,4936,4937],{"class":3025}," hello-world\n",[2964,4939,4940],{},[2971,4941,4942],{},"Windows:",[2984,4944,4948],{"className":4945,"code":4946,"language":4947,"meta":2989,"style":2989},"language-powershell shiki shiki-themes light-plus dark-plus dark-plus","# Завантажити Docker Desktop для Windows з docker.com\n# Вимагає WSL 2 (Windows Subsystem for Linux)\n","powershell",[2991,4949,4950,4955],{"__ignoreMap":2989},[2994,4951,4952],{"class":2996,"line":2997},[2994,4953,4954],{"class":3000},"# Завантажити Docker Desktop для Windows з docker.com\n",[2994,4956,4957],{"class":2996,"line":3004},[2994,4958,4959],{"class":3000},"# Вимагає WSL 2 (Windows Subsystem for Linux)\n",[4961,4962,4963,4968,4981],"note",{},[2964,4964,4965],{},[2971,4966,4967],{},"Docker Desktop vs Docker Engine:",[4203,4969,4970,4976],{},[3962,4971,4972,4975],{},[2971,4973,4974],{},"Docker Desktop:"," GUI-додаток для macOS/Windows, що включає Docker Engine, Docker Compose, Kubernetes. Рекомендовано для розробників.",[3962,4977,4978,4980],{},[2971,4979,3972],{}," Лише демон Docker без GUI. Використовується на Linux-серверах та у CI/CD.",[2964,4982,4983],{},"Testcontainers працює з обома варіантами — йому потрібен лише Docker Engine API.",[3307,4985],{},[2976,4987,4989],{"id":4988},"maven-залежності","Maven залежності",[2964,4991,4992,4993,3563],{},"Додайте Testcontainers до ",[2991,4994,4995],{},"pom.xml",[2984,4997,5002],{"className":4998,"code":4999,"language":5000,"meta":5001,"style":2989},"language-xml shiki shiki-themes light-plus dark-plus dark-plus","\u003Cdependencies>\n    \u003C!-- JUnit 5 (вже є з попередньої статті) -->\n    \u003Cdependency>\n        \u003CgroupId>org.junit.jupiter\u003C/groupId>\n        \u003CartifactId>junit-jupiter\u003C/artifactId>\n        \u003Cversion>5.10.2\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- Testcontainers Core -->\n    \u003Cdependency>\n        \u003CgroupId>org.testcontainers\u003C/groupId>\n        \u003CartifactId>testcontainers\u003C/artifactId>\n        \u003Cversion>1.19.7\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- Testcontainers JUnit 5 Integration -->\n    \u003Cdependency>\n        \u003CgroupId>org.testcontainers\u003C/groupId>\n        \u003CartifactId>junit-jupiter\u003C/artifactId>\n        \u003Cversion>1.19.7\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- Testcontainers PostgreSQL Module -->\n    \u003Cdependency>\n        \u003CgroupId>org.testcontainers\u003C/groupId>\n        \u003CartifactId>postgresql\u003C/artifactId>\n        \u003Cversion>1.19.7\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- PostgreSQL JDBC Driver (для production і тестів) -->\n    \u003Cdependency>\n        \u003CgroupId>org.postgresql\u003C/groupId>\n        \u003CartifactId>postgresql\u003C/artifactId>\n        \u003Cversion>42.7.3\u003C/version>\n    \u003C/dependency>\n\n    \u003C!-- AssertJ (вже є з попередньої статті) -->\n    \u003Cdependency>\n        \u003CgroupId>org.assertj\u003C/groupId>\n        \u003CartifactId>assertj-core\u003C/artifactId>\n        \u003Cversion>3.25.3\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\u003C/dependencies>\n","xml","showLineNumbers",[2991,5003,5004,5016,5021,5031,5052,5070,5088,5106,5115,5119,5124,5132,5149,5166,5183,5199,5207,5211,5216,5224,5240,5256,5272,5288,5296,5300,5305,5313,5329,5346,5363,5380,5389,5394,5400,5409,5427,5444,5462,5471,5476,5482,5491,5509,5527,5545,5562,5571],{"__ignoreMap":2989},[2994,5005,5006,5009,5013],{"class":2996,"line":2997},[2994,5007,4060],{"class":5008},"s0P7L",[2994,5010,5012],{"class":5011},"sKtos","dependencies",[2994,5014,5015],{"class":5008},">\n",[2994,5017,5018],{"class":2996,"line":3004},[2994,5019,5020],{"class":3000},"    \u003C!-- JUnit 5 (вже є з попередньої статті) -->\n",[2994,5022,5023,5026,5029],{"class":2996,"line":3058},[2994,5024,5025],{"class":5008},"    \u003C",[2994,5027,5028],{"class":5011},"dependency",[2994,5030,5015],{"class":5008},[2994,5032,5033,5036,5039,5042,5045,5048,5050],{"class":2996,"line":3065},[2994,5034,5035],{"class":5008},"        \u003C",[2994,5037,5038],{"class":5011},"groupId",[2994,5040,5041],{"class":5008},">",[2994,5043,5044],{"class":3021},"org.junit.jupiter",[2994,5046,5047],{"class":5008},"\u003C/",[2994,5049,5038],{"class":5011},[2994,5051,5015],{"class":5008},[2994,5053,5054,5056,5059,5061,5064,5066,5068],{"class":2996,"line":3079},[2994,5055,5035],{"class":5008},[2994,5057,5058],{"class":5011},"artifactId",[2994,5060,5041],{"class":5008},[2994,5062,5063],{"class":3021},"junit-jupiter",[2994,5065,5047],{"class":5008},[2994,5067,5058],{"class":5011},[2994,5069,5015],{"class":5008},[2994,5071,5072,5074,5077,5079,5082,5084,5086],{"class":2996,"line":3091},[2994,5073,5035],{"class":5008},[2994,5075,5076],{"class":5011},"version",[2994,5078,5041],{"class":5008},[2994,5080,5081],{"class":3021},"5.10.2",[2994,5083,5047],{"class":5008},[2994,5085,5076],{"class":5011},[2994,5087,5015],{"class":5008},[2994,5089,5090,5092,5095,5097,5100,5102,5104],{"class":2996,"line":3102},[2994,5091,5035],{"class":5008},[2994,5093,5094],{"class":5011},"scope",[2994,5096,5041],{"class":5008},[2994,5098,5099],{"class":3021},"test",[2994,5101,5047],{"class":5008},[2994,5103,5094],{"class":5011},[2994,5105,5015],{"class":5008},[2994,5107,5108,5111,5113],{"class":2996,"line":3125},[2994,5109,5110],{"class":5008},"    \u003C/",[2994,5112,5028],{"class":5011},[2994,5114,5015],{"class":5008},[2994,5116,5117],{"class":2996,"line":3139},[2994,5118,3062],{"emptyLinePlaceholder":3061},[2994,5120,5121],{"class":2996,"line":3148},[2994,5122,5123],{"class":3000},"    \u003C!-- Testcontainers Core -->\n",[2994,5125,5126,5128,5130],{"class":2996,"line":3845},[2994,5127,5025],{"class":5008},[2994,5129,5028],{"class":5011},[2994,5131,5015],{"class":5008},[2994,5133,5134,5136,5138,5140,5143,5145,5147],{"class":2996,"line":3851},[2994,5135,5035],{"class":5008},[2994,5137,5038],{"class":5011},[2994,5139,5041],{"class":5008},[2994,5141,5142],{"class":3021},"org.testcontainers",[2994,5144,5047],{"class":5008},[2994,5146,5038],{"class":5011},[2994,5148,5015],{"class":5008},[2994,5150,5151,5153,5155,5157,5160,5162,5164],{"class":2996,"line":3856},[2994,5152,5035],{"class":5008},[2994,5154,5058],{"class":5011},[2994,5156,5041],{"class":5008},[2994,5158,5159],{"class":3021},"testcontainers",[2994,5161,5047],{"class":5008},[2994,5163,5058],{"class":5011},[2994,5165,5015],{"class":5008},[2994,5167,5168,5170,5172,5174,5177,5179,5181],{"class":2996,"line":3861},[2994,5169,5035],{"class":5008},[2994,5171,5076],{"class":5011},[2994,5173,5041],{"class":5008},[2994,5175,5176],{"class":3021},"1.19.7",[2994,5178,5047],{"class":5008},[2994,5180,5076],{"class":5011},[2994,5182,5015],{"class":5008},[2994,5184,5185,5187,5189,5191,5193,5195,5197],{"class":2996,"line":3867},[2994,5186,5035],{"class":5008},[2994,5188,5094],{"class":5011},[2994,5190,5041],{"class":5008},[2994,5192,5099],{"class":3021},[2994,5194,5047],{"class":5008},[2994,5196,5094],{"class":5011},[2994,5198,5015],{"class":5008},[2994,5200,5201,5203,5205],{"class":2996,"line":3873},[2994,5202,5110],{"class":5008},[2994,5204,5028],{"class":5011},[2994,5206,5015],{"class":5008},[2994,5208,5209],{"class":2996,"line":3879},[2994,5210,3062],{"emptyLinePlaceholder":3061},[2994,5212,5213],{"class":2996,"line":3885},[2994,5214,5215],{"class":3000},"    \u003C!-- Testcontainers JUnit 5 Integration -->\n",[2994,5217,5218,5220,5222],{"class":2996,"line":3891},[2994,5219,5025],{"class":5008},[2994,5221,5028],{"class":5011},[2994,5223,5015],{"class":5008},[2994,5225,5226,5228,5230,5232,5234,5236,5238],{"class":2996,"line":3897},[2994,5227,5035],{"class":5008},[2994,5229,5038],{"class":5011},[2994,5231,5041],{"class":5008},[2994,5233,5142],{"class":3021},[2994,5235,5047],{"class":5008},[2994,5237,5038],{"class":5011},[2994,5239,5015],{"class":5008},[2994,5241,5242,5244,5246,5248,5250,5252,5254],{"class":2996,"line":3903},[2994,5243,5035],{"class":5008},[2994,5245,5058],{"class":5011},[2994,5247,5041],{"class":5008},[2994,5249,5063],{"class":3021},[2994,5251,5047],{"class":5008},[2994,5253,5058],{"class":5011},[2994,5255,5015],{"class":5008},[2994,5257,5258,5260,5262,5264,5266,5268,5270],{"class":2996,"line":3908},[2994,5259,5035],{"class":5008},[2994,5261,5076],{"class":5011},[2994,5263,5041],{"class":5008},[2994,5265,5176],{"class":3021},[2994,5267,5047],{"class":5008},[2994,5269,5076],{"class":5011},[2994,5271,5015],{"class":5008},[2994,5273,5274,5276,5278,5280,5282,5284,5286],{"class":2996,"line":3914},[2994,5275,5035],{"class":5008},[2994,5277,5094],{"class":5011},[2994,5279,5041],{"class":5008},[2994,5281,5099],{"class":3021},[2994,5283,5047],{"class":5008},[2994,5285,5094],{"class":5011},[2994,5287,5015],{"class":5008},[2994,5289,5290,5292,5294],{"class":2996,"line":3920},[2994,5291,5110],{"class":5008},[2994,5293,5028],{"class":5011},[2994,5295,5015],{"class":5008},[2994,5297,5298],{"class":2996,"line":3926},[2994,5299,3062],{"emptyLinePlaceholder":3061},[2994,5301,5302],{"class":2996,"line":3931},[2994,5303,5304],{"class":3000},"    \u003C!-- Testcontainers PostgreSQL Module -->\n",[2994,5306,5307,5309,5311],{"class":2996,"line":3937},[2994,5308,5025],{"class":5008},[2994,5310,5028],{"class":5011},[2994,5312,5015],{"class":5008},[2994,5314,5315,5317,5319,5321,5323,5325,5327],{"class":2996,"line":3943},[2994,5316,5035],{"class":5008},[2994,5318,5038],{"class":5011},[2994,5320,5041],{"class":5008},[2994,5322,5142],{"class":3021},[2994,5324,5047],{"class":5008},[2994,5326,5038],{"class":5011},[2994,5328,5015],{"class":5008},[2994,5330,5331,5333,5335,5337,5340,5342,5344],{"class":2996,"line":3949},[2994,5332,5035],{"class":5008},[2994,5334,5058],{"class":5011},[2994,5336,5041],{"class":5008},[2994,5338,5339],{"class":3021},"postgresql",[2994,5341,5047],{"class":5008},[2994,5343,5058],{"class":5011},[2994,5345,5015],{"class":5008},[2994,5347,5349,5351,5353,5355,5357,5359,5361],{"class":2996,"line":5348},30,[2994,5350,5035],{"class":5008},[2994,5352,5076],{"class":5011},[2994,5354,5041],{"class":5008},[2994,5356,5176],{"class":3021},[2994,5358,5047],{"class":5008},[2994,5360,5076],{"class":5011},[2994,5362,5015],{"class":5008},[2994,5364,5366,5368,5370,5372,5374,5376,5378],{"class":2996,"line":5365},31,[2994,5367,5035],{"class":5008},[2994,5369,5094],{"class":5011},[2994,5371,5041],{"class":5008},[2994,5373,5099],{"class":3021},[2994,5375,5047],{"class":5008},[2994,5377,5094],{"class":5011},[2994,5379,5015],{"class":5008},[2994,5381,5383,5385,5387],{"class":2996,"line":5382},32,[2994,5384,5110],{"class":5008},[2994,5386,5028],{"class":5011},[2994,5388,5015],{"class":5008},[2994,5390,5392],{"class":2996,"line":5391},33,[2994,5393,3062],{"emptyLinePlaceholder":3061},[2994,5395,5397],{"class":2996,"line":5396},34,[2994,5398,5399],{"class":3000},"    \u003C!-- PostgreSQL JDBC Driver (для production і тестів) -->\n",[2994,5401,5403,5405,5407],{"class":2996,"line":5402},35,[2994,5404,5025],{"class":5008},[2994,5406,5028],{"class":5011},[2994,5408,5015],{"class":5008},[2994,5410,5412,5414,5416,5418,5421,5423,5425],{"class":2996,"line":5411},36,[2994,5413,5035],{"class":5008},[2994,5415,5038],{"class":5011},[2994,5417,5041],{"class":5008},[2994,5419,5420],{"class":3021},"org.postgresql",[2994,5422,5047],{"class":5008},[2994,5424,5038],{"class":5011},[2994,5426,5015],{"class":5008},[2994,5428,5430,5432,5434,5436,5438,5440,5442],{"class":2996,"line":5429},37,[2994,5431,5035],{"class":5008},[2994,5433,5058],{"class":5011},[2994,5435,5041],{"class":5008},[2994,5437,5339],{"class":3021},[2994,5439,5047],{"class":5008},[2994,5441,5058],{"class":5011},[2994,5443,5015],{"class":5008},[2994,5445,5447,5449,5451,5453,5456,5458,5460],{"class":2996,"line":5446},38,[2994,5448,5035],{"class":5008},[2994,5450,5076],{"class":5011},[2994,5452,5041],{"class":5008},[2994,5454,5455],{"class":3021},"42.7.3",[2994,5457,5047],{"class":5008},[2994,5459,5076],{"class":5011},[2994,5461,5015],{"class":5008},[2994,5463,5465,5467,5469],{"class":2996,"line":5464},39,[2994,5466,5110],{"class":5008},[2994,5468,5028],{"class":5011},[2994,5470,5015],{"class":5008},[2994,5472,5474],{"class":2996,"line":5473},40,[2994,5475,3062],{"emptyLinePlaceholder":3061},[2994,5477,5479],{"class":2996,"line":5478},41,[2994,5480,5481],{"class":3000},"    \u003C!-- AssertJ (вже є з попередньої статті) -->\n",[2994,5483,5485,5487,5489],{"class":2996,"line":5484},42,[2994,5486,5025],{"class":5008},[2994,5488,5028],{"class":5011},[2994,5490,5015],{"class":5008},[2994,5492,5494,5496,5498,5500,5503,5505,5507],{"class":2996,"line":5493},43,[2994,5495,5035],{"class":5008},[2994,5497,5038],{"class":5011},[2994,5499,5041],{"class":5008},[2994,5501,5502],{"class":3021},"org.assertj",[2994,5504,5047],{"class":5008},[2994,5506,5038],{"class":5011},[2994,5508,5015],{"class":5008},[2994,5510,5512,5514,5516,5518,5521,5523,5525],{"class":2996,"line":5511},44,[2994,5513,5035],{"class":5008},[2994,5515,5058],{"class":5011},[2994,5517,5041],{"class":5008},[2994,5519,5520],{"class":3021},"assertj-core",[2994,5522,5047],{"class":5008},[2994,5524,5058],{"class":5011},[2994,5526,5015],{"class":5008},[2994,5528,5530,5532,5534,5536,5539,5541,5543],{"class":2996,"line":5529},45,[2994,5531,5035],{"class":5008},[2994,5533,5076],{"class":5011},[2994,5535,5041],{"class":5008},[2994,5537,5538],{"class":3021},"3.25.3",[2994,5540,5047],{"class":5008},[2994,5542,5076],{"class":5011},[2994,5544,5015],{"class":5008},[2994,5546,5548,5550,5552,5554,5556,5558,5560],{"class":2996,"line":5547},46,[2994,5549,5035],{"class":5008},[2994,5551,5094],{"class":5011},[2994,5553,5041],{"class":5008},[2994,5555,5099],{"class":3021},[2994,5557,5047],{"class":5008},[2994,5559,5094],{"class":5011},[2994,5561,5015],{"class":5008},[2994,5563,5565,5567,5569],{"class":2996,"line":5564},47,[2994,5566,5110],{"class":5008},[2994,5568,5028],{"class":5011},[2994,5570,5015],{"class":5008},[2994,5572,5574,5576,5578],{"class":2996,"line":5573},48,[2994,5575,5047],{"class":5008},[2994,5577,5012],{"class":5011},[2994,5579,5015],{"class":5008},[2964,5581,5582],{},[2971,5583,5584],{},"Пояснення залежностей:",[4203,5586,5587,5594,5608,5619],{},[3962,5588,5589,5593],{},[2971,5590,5591],{},[2991,5592,5159],{}," (рядок 12): Core-бібліотека Testcontainers. Надає базовий API для роботи з Docker.",[3962,5595,5596,5600,5601,5604,5605,3217],{},[2971,5597,5598],{},[2991,5599,5063],{}," (рядок 18): Інтеграція з JUnit 5. Надає анотації ",[2991,5602,5603],{},"@Testcontainers"," та ",[2991,5606,5607],{},"@Container",[3962,5609,5610,5614,5615,5618],{},[2971,5611,5612],{},[2991,5613,5339],{}," (рядок 24): Модуль для PostgreSQL. Надає клас ",[2991,5616,5617],{},"PostgreSQLContainer"," з преконфігурованими налаштуваннями.",[3962,5620,5621,5626],{},[2971,5622,5623,5625],{},[2991,5624,5339],{}," JDBC driver"," (рядок 30): Драйвер для підключення до PostgreSQL. Потрібен і для production, і для тестів.",[5628,5629,5630,5635,5638,5646],"warning",{},[2964,5631,5632],{},[2971,5633,5634],{},"Версії Testcontainers та Docker:",[2964,5636,5637],{},"Testcontainers 1.19.x вимагає:",[4203,5639,5640,5643],{},[3962,5641,5642],{},"Docker Engine 20.10+ (випущений у 2020)",[3962,5644,5645],{},"Java 8+ (але рекомендовано Java 17+)",[2964,5647,5648],{},"Якщо у вас старіша версія Docker — оновіть її. Testcontainers не працює з Docker версій 1.x.",[3307,5650],{},[2976,5652,5654],{"id":5653},"базовий-клас-abstractpostgresintegrationtest","Базовий клас AbstractPostgresIntegrationTest",[2964,5656,5657,5658,5661],{},"Створимо абстрактний базовий клас, аналогічний ",[2991,5659,5660],{},"AbstractRepositoryTest"," з попередньої статті, але для Testcontainers:",[2984,5663,5665],{"className":3173,"code":5664,"language":3175,"meta":5001,"style":2989},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.db.ConnectionManager;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.testcontainers.containers.PostgreSQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.junit.jupiter.Testcontainers;\nimport org.testcontainers.utility.DockerImageName;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.Statement;\nimport java.util.stream.Collectors;\n\n/**\n * Базовий клас для інтеграційних тестів з Testcontainers + PostgreSQL.\n * \u003Cp>\n * \u003Cb>Архітектурні рішення:\u003C/b>\n * \u003Cul>\n *   \u003Cli>\u003Cb>Singleton Container Pattern:\u003C/b> Один контейнер PostgreSQL для всього\n *       тестового класу (static @Container). Це швидше за створення нового\n *       контейнера для кожного тесту.\u003C/li>\n *   \u003Cli>\u003Cb>Ізоляція через TRUNCATE:\u003C/b> Кожен тест очищує всі таблиці у\n *       {@code @BeforeEach}, гарантуючи незалежність тестів.\u003C/li>\n *   \u003Cli>\u003Cb>DDL виконується один раз:\u003C/b> Схема БД створюється у {@code @BeforeAll}\n *       і використовується всіма тестами.\u003C/li>\n * \u003C/ul>\n * \u003Cp>\n * \u003Cb>Lifecycle:\u003C/b>\n * \u003Cpre>\n * @BeforeAll (один раз для класу)\n *   → Testcontainers запускає PostgreSQL контейнер\n *   → Виконується DDL-скрипт (CREATE TABLE)\n *   → Створюється ConnectionManager\n *\n * @BeforeEach (перед кожним тестом)\n *   → TRUNCATE всіх таблиць (очищення даних)\n *\n * test1() → test2() → test3() ...\n *\n * @AfterEach (після кожного тесту)\n *   → Закрити з'єднання (опціонально)\n *\n * @AfterAll (один раз після всіх тестів)\n *   → Testcontainers зупиняє і видаляє контейнер\n * \u003C/pre>\n */\n@Testcontainers // JUnit 5 extension для автоматичного управління контейнерами\npublic abstract class AbstractPostgresIntegrationTest {\n\n    /**\n     * PostgreSQL контейнер — спільний для всіх тестів у класі.\n     * \u003Cp>\n     * \u003Cb>static:\u003C/b> Контейнер запускається один раз для класу, а не для кожного тесту.\n     * Це значно швидше (запуск контейнера займає 2–5 секунд).\n     * \u003Cp>\n     * \u003Cb>@Container:\u003C/b> Testcontainers автоматично керує lifecycle:\n     * запускає перед {@code @BeforeAll}, зупиняє після {@code @AfterAll}.\n     * \u003Cp>\n     * \u003Cb>DockerImageName.parse():\u003C/b> Явно вказуємо Docker-образ з версією.\n     * Це гарантує, що тести використовують ту саму версію PostgreSQL, що й production.\n     */\n    @Container\n    protected static final PostgreSQLContainer\u003C?> POSTGRES = \n        new PostgreSQLContainer\u003C>(DockerImageName.parse(\"postgres:15-alpine\"))\n            .withDatabaseName(\"audiobook_test_db\")\n            .withUsername(\"test_user\")\n            .withPassword(\"test_password\")\n            .withReuse(false); // false = новий контейнер для кожного запуску тестів\n\n    /**\n     * ConnectionManager для тестової БД.\n     * Створюється після запуску контейнера у {@code @BeforeAll}.\n     */\n    protected static ConnectionManager connectionManager;\n\n    /**\n     * Ініціалізує ConnectionManager та виконує DDL-скрипт.\n     * \u003Cp>\n     * Викликається один раз перед усіма тестами класу.\n     * На цьому етапі PostgreSQL контейнер вже запущений (завдяки @Container).\n     */\n    @BeforeAll\n    static void initializeDatabase() throws IOException, SQLException {\n        // Отримати JDBC URL з динамічним портом\n        // Приклад: jdbc:postgresql://localhost:32768/audiobook_test_db\n        String jdbcUrl = POSTGRES.getJdbcUrl();\n        String username = POSTGRES.getUsername();\n        String password = POSTGRES.getPassword();\n\n        // Створити ConnectionManager для тестової БД\n        connectionManager = new ConnectionManager(jdbcUrl, username, password);\n\n        // Виконати DDL-скрипт (створити таблиці)\n        executeDdlScript(\"ddl_postgres.sql\");\n    }\n\n    /**\n     * Очищує всі таблиці перед кожним тестом.\n     * \u003Cp>\n     * Це гарантує ізоляцію тестів: кожен тест починає з порожньої БД.\n     * Використовуємо {@code TRUNCATE} замість {@code DELETE}, оскільки:\n     * \u003Cul>\n     *   \u003Cli>TRUNCATE швидший (не генерує WAL-записи для кожного рядка)\u003C/li>\n     *   \u003Cli>TRUNCATE скидає AUTO_INCREMENT лічильники\u003C/li>\n     *   \u003Cli>TRUNCATE ... CASCADE автоматично очищає пов'язані таблиці\u003C/li>\n     * \u003C/ul>\n     */\n    @BeforeEach\n    void cleanDatabase() throws SQLException {\n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement()) {\n            \n            // TRUNCATE всі таблиці у правильному порядку (від дочірніх до батьківських)\n            // CASCADE автоматично очистить пов'язані таблиці через FK\n            stmt.execute(\"TRUNCATE TABLE audiobook_files CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE listening_progresses CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE audiobook_collection CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE collections CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE audiobooks CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE authors CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE genres CASCADE\");\n            stmt.execute(\"TRUNCATE TABLE users CASCADE\");\n        }\n    }\n\n    /**\n     * Закриває ConnectionManager після всіх тестів.\n     * Контейнер PostgreSQL автоматично зупиняється Testcontainers.\n     */\n    @AfterEach\n    void tearDown() {\n        // У цій реалізації нічого не робимо — ConnectionManager залишається відкритим\n        // для наступних тестів. Закриття відбувається у @AfterAll (якщо потрібно).\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // Утилітні методи (аналогічні AbstractRepositoryTest з H2)\n    // ═══════════════════════════════════════════════════════════════════════\n\n    /**\n     * Виконує DDL-скрипт з ресурсів.\n     * \u003Cp>\n     * Скрипт має бути PostgreSQL-сумісним (не H2!).\n     * Розташування: {@code src/test/resources/ddl_postgres.sql}\n     */\n    protected static void executeDdlScript(String scriptPath) throws IOException, SQLException {\n        String sql = loadResourceAsString(scriptPath);\n        \n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement()) {\n            \n            // PostgreSQL підтримує багаторядкові команди через ;\n            String[] commands = sql.split(\";\");\n            for (String command : commands) {\n                String trimmed = command.trim();\n                if (!trimmed.isEmpty() && !trimmed.startsWith(\"--\")) {\n                    stmt.execute(trimmed);\n                }\n            }\n        }\n    }\n\n    /**\n     * Завантажує файл з ресурсів як рядок.\n     */\n    protected static String loadResourceAsString(String resourcePath) throws IOException {\n        try (BufferedReader reader = new BufferedReader(\n                new InputStreamReader(\n                    AbstractPostgresIntegrationTest.class\n                        .getClassLoader()\n                        .getResourceAsStream(resourcePath),\n                    StandardCharsets.UTF_8))) {\n            return reader.lines().collect(Collectors.joining(\"\\n\"));\n        }\n    }\n\n    /**\n     * Підраховує кількість рядків у таблиці.\n     */\n    protected long countRowsInTable(String tableName) throws SQLException {\n        String sql = \"SELECT COUNT(*) FROM \" + tableName;\n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement();\n             var rs = stmt.executeQuery(sql)) {\n            rs.next();\n            return rs.getLong(1);\n        }\n    }\n\n    /**\n     * Виконує довільний SQL-запит (для підготовки тестових даних).\n     */\n    protected void executeSql(String sql) throws SQLException {\n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement()) {\n            stmt.execute(sql);\n        }\n    }\n}\n",[2991,5666,5667,5675,5679,5687,5694,5701,5708,5715,5722,5729,5736,5740,5747,5754,5761,5768,5775,5782,5789,5796,5800,5805,5810,5815,5820,5825,5830,5835,5840,5845,5850,5855,5860,5865,5869,5874,5879,5884,5889,5894,5899,5904,5909,5914,5918,5923,5927,5932,5937,5942,5948,5954,5960,5966,5976,5990,5995,6001,6007,6013,6019,6025,6030,6036,6042,6047,6053,6059,6065,6073,6095,6120,6134,6147,6161,6178,6183,6188,6194,6200,6205,6220,6225,6230,6236,6241,6247,6253,6258,6266,6293,6299,6305,6324,6343,6362,6367,6373,6386,6391,6397,6410,6415,6420,6425,6431,6436,6442,6448,6454,6460,6466,6472,6478,6483,6491,6508,6534,6556,6562,6568,6574,6592,6608,6624,6640,6656,6672,6688,6704,6710,6715,6720,6725,6731,6737,6742,6750,6760,6766,6772,6777,6782,6788,6794,6799,6804,6809,6815,6820,6826,6832,6837,6868,6883,6889,6910,6927,6932,6938,6966,6985,7006,7041,7054,7060,7066,7071,7076,7081,7086,7092,7097,7124,7147,7158,7169,7181,7192,7206,7249,7254,7259,7264,7269,7275,7280,7306,7321,7342,7359,7380,7393,7412,7417,7422,7427,7432,7438,7443,7467,7488,7505,7517,7522,7527],{"__ignoreMap":2989},[2994,5668,5669,5672],{"class":2996,"line":2997},[2994,5670,5671],{"class":3007},"package",[2994,5673,5674],{"class":3021}," com.example.audiobook.repository;\n",[2994,5676,5677],{"class":2996,"line":3004},[2994,5678,3062],{"emptyLinePlaceholder":3061},[2994,5680,5681,5684],{"class":2996,"line":3058},[2994,5682,5683],{"class":3007},"import",[2994,5685,5686],{"class":3021}," com.example.audiobook.db.ConnectionManager;\n",[2994,5688,5689,5691],{"class":2996,"line":3065},[2994,5690,5683],{"class":3007},[2994,5692,5693],{"class":3021}," org.junit.jupiter.api.AfterEach;\n",[2994,5695,5696,5698],{"class":2996,"line":3079},[2994,5697,5683],{"class":3007},[2994,5699,5700],{"class":3021}," org.junit.jupiter.api.BeforeAll;\n",[2994,5702,5703,5705],{"class":2996,"line":3091},[2994,5704,5683],{"class":3007},[2994,5706,5707],{"class":3021}," org.junit.jupiter.api.BeforeEach;\n",[2994,5709,5710,5712],{"class":2996,"line":3102},[2994,5711,5683],{"class":3007},[2994,5713,5714],{"class":3021}," org.testcontainers.containers.PostgreSQLContainer;\n",[2994,5716,5717,5719],{"class":2996,"line":3125},[2994,5718,5683],{"class":3007},[2994,5720,5721],{"class":3021}," org.testcontainers.junit.jupiter.Container;\n",[2994,5723,5724,5726],{"class":2996,"line":3139},[2994,5725,5683],{"class":3007},[2994,5727,5728],{"class":3021}," org.testcontainers.junit.jupiter.Testcontainers;\n",[2994,5730,5731,5733],{"class":2996,"line":3148},[2994,5732,5683],{"class":3007},[2994,5734,5735],{"class":3021}," org.testcontainers.utility.DockerImageName;\n",[2994,5737,5738],{"class":2996,"line":3845},[2994,5739,3062],{"emptyLinePlaceholder":3061},[2994,5741,5742,5744],{"class":2996,"line":3851},[2994,5743,5683],{"class":3007},[2994,5745,5746],{"class":3021}," java.io.BufferedReader;\n",[2994,5748,5749,5751],{"class":2996,"line":3856},[2994,5750,5683],{"class":3007},[2994,5752,5753],{"class":3021}," java.io.IOException;\n",[2994,5755,5756,5758],{"class":2996,"line":3861},[2994,5757,5683],{"class":3007},[2994,5759,5760],{"class":3021}," java.io.InputStreamReader;\n",[2994,5762,5763,5765],{"class":2996,"line":3867},[2994,5764,5683],{"class":3007},[2994,5766,5767],{"class":3021}," java.nio.charset.StandardCharsets;\n",[2994,5769,5770,5772],{"class":2996,"line":3873},[2994,5771,5683],{"class":3007},[2994,5773,5774],{"class":3021}," java.sql.Connection;\n",[2994,5776,5777,5779],{"class":2996,"line":3879},[2994,5778,5683],{"class":3007},[2994,5780,5781],{"class":3021}," java.sql.SQLException;\n",[2994,5783,5784,5786],{"class":2996,"line":3885},[2994,5785,5683],{"class":3007},[2994,5787,5788],{"class":3021}," java.sql.Statement;\n",[2994,5790,5791,5793],{"class":2996,"line":3891},[2994,5792,5683],{"class":3007},[2994,5794,5795],{"class":3021}," java.util.stream.Collectors;\n",[2994,5797,5798],{"class":2996,"line":3897},[2994,5799,3062],{"emptyLinePlaceholder":3061},[2994,5801,5802],{"class":2996,"line":3903},[2994,5803,5804],{"class":3000},"/**\n",[2994,5806,5807],{"class":2996,"line":3908},[2994,5808,5809],{"class":3000}," * Базовий клас для інтеграційних тестів з Testcontainers + PostgreSQL.\n",[2994,5811,5812],{"class":2996,"line":3914},[2994,5813,5814],{"class":3000}," * \u003Cp>\n",[2994,5816,5817],{"class":2996,"line":3920},[2994,5818,5819],{"class":3000}," * \u003Cb>Архітектурні рішення:\u003C/b>\n",[2994,5821,5822],{"class":2996,"line":3926},[2994,5823,5824],{"class":3000}," * \u003Cul>\n",[2994,5826,5827],{"class":2996,"line":3931},[2994,5828,5829],{"class":3000}," *   \u003Cli>\u003Cb>Singleton Container Pattern:\u003C/b> Один контейнер PostgreSQL для всього\n",[2994,5831,5832],{"class":2996,"line":3937},[2994,5833,5834],{"class":3000}," *       тестового класу (static @Container). Це швидше за створення нового\n",[2994,5836,5837],{"class":2996,"line":3943},[2994,5838,5839],{"class":3000}," *       контейнера для кожного тесту.\u003C/li>\n",[2994,5841,5842],{"class":2996,"line":3949},[2994,5843,5844],{"class":3000}," *   \u003Cli>\u003Cb>Ізоляція через TRUNCATE:\u003C/b> Кожен тест очищує всі таблиці у\n",[2994,5846,5847],{"class":2996,"line":5348},[2994,5848,5849],{"class":3000}," *       {@code @BeforeEach}, гарантуючи незалежність тестів.\u003C/li>\n",[2994,5851,5852],{"class":2996,"line":5365},[2994,5853,5854],{"class":3000}," *   \u003Cli>\u003Cb>DDL виконується один раз:\u003C/b> Схема БД створюється у {@code @BeforeAll}\n",[2994,5856,5857],{"class":2996,"line":5382},[2994,5858,5859],{"class":3000}," *       і використовується всіма тестами.\u003C/li>\n",[2994,5861,5862],{"class":2996,"line":5391},[2994,5863,5864],{"class":3000}," * \u003C/ul>\n",[2994,5866,5867],{"class":2996,"line":5396},[2994,5868,5814],{"class":3000},[2994,5870,5871],{"class":2996,"line":5402},[2994,5872,5873],{"class":3000}," * \u003Cb>Lifecycle:\u003C/b>\n",[2994,5875,5876],{"class":2996,"line":5411},[2994,5877,5878],{"class":3000}," * \u003Cpre>\n",[2994,5880,5881],{"class":2996,"line":5429},[2994,5882,5883],{"class":3000}," * @BeforeAll (один раз для класу)\n",[2994,5885,5886],{"class":2996,"line":5446},[2994,5887,5888],{"class":3000}," *   → Testcontainers запускає PostgreSQL контейнер\n",[2994,5890,5891],{"class":2996,"line":5464},[2994,5892,5893],{"class":3000}," *   → Виконується DDL-скрипт (CREATE TABLE)\n",[2994,5895,5896],{"class":2996,"line":5473},[2994,5897,5898],{"class":3000}," *   → Створюється ConnectionManager\n",[2994,5900,5901],{"class":2996,"line":5478},[2994,5902,5903],{"class":3000}," *\n",[2994,5905,5906],{"class":2996,"line":5484},[2994,5907,5908],{"class":3000}," * @BeforeEach (перед кожним тестом)\n",[2994,5910,5911],{"class":2996,"line":5493},[2994,5912,5913],{"class":3000}," *   → TRUNCATE всіх таблиць (очищення даних)\n",[2994,5915,5916],{"class":2996,"line":5511},[2994,5917,5903],{"class":3000},[2994,5919,5920],{"class":2996,"line":5529},[2994,5921,5922],{"class":3000}," * test1() → test2() → test3() ...\n",[2994,5924,5925],{"class":2996,"line":5547},[2994,5926,5903],{"class":3000},[2994,5928,5929],{"class":2996,"line":5564},[2994,5930,5931],{"class":3000}," * @AfterEach (після кожного тесту)\n",[2994,5933,5934],{"class":2996,"line":5573},[2994,5935,5936],{"class":3000}," *   → Закрити з'єднання (опціонально)\n",[2994,5938,5940],{"class":2996,"line":5939},49,[2994,5941,5903],{"class":3000},[2994,5943,5945],{"class":2996,"line":5944},50,[2994,5946,5947],{"class":3000}," * @AfterAll (один раз після всіх тестів)\n",[2994,5949,5951],{"class":2996,"line":5950},51,[2994,5952,5953],{"class":3000}," *   → Testcontainers зупиняє і видаляє контейнер\n",[2994,5955,5957],{"class":2996,"line":5956},52,[2994,5958,5959],{"class":3000}," * \u003C/pre>\n",[2994,5961,5963],{"class":2996,"line":5962},53,[2994,5964,5965],{"class":3000}," */\n",[2994,5967,5969,5971,5973],{"class":2996,"line":5968},54,[2994,5970,4018],{"class":3021},[2994,5972,3768],{"class":3187},[2994,5974,5975],{"class":3000}," // JUnit 5 extension для автоматичного управління контейнерами\n",[2994,5977,5979,5981,5983,5985,5988],{"class":2996,"line":5978},55,[2994,5980,4246],{"class":3007},[2994,5982,4249],{"class":3007},[2994,5984,4252],{"class":3007},[2994,5986,5987],{"class":3187}," AbstractPostgresIntegrationTest",[2994,5989,4034],{"class":3021},[2994,5991,5993],{"class":2996,"line":5992},56,[2994,5994,3062],{"emptyLinePlaceholder":3061},[2994,5996,5998],{"class":2996,"line":5997},57,[2994,5999,6000],{"class":3000},"    /**\n",[2994,6002,6004],{"class":2996,"line":6003},58,[2994,6005,6006],{"class":3000},"     * PostgreSQL контейнер — спільний для всіх тестів у класі.\n",[2994,6008,6010],{"class":2996,"line":6009},59,[2994,6011,6012],{"class":3000},"     * \u003Cp>\n",[2994,6014,6016],{"class":2996,"line":6015},60,[2994,6017,6018],{"class":3000},"     * \u003Cb>static:\u003C/b> Контейнер запускається один раз для класу, а не для кожного тесту.\n",[2994,6020,6022],{"class":2996,"line":6021},61,[2994,6023,6024],{"class":3000},"     * Це значно швидше (запуск контейнера займає 2–5 секунд).\n",[2994,6026,6028],{"class":2996,"line":6027},62,[2994,6029,6012],{"class":3000},[2994,6031,6033],{"class":2996,"line":6032},63,[2994,6034,6035],{"class":3000},"     * \u003Cb>@Container:\u003C/b> Testcontainers автоматично керує lifecycle:\n",[2994,6037,6039],{"class":2996,"line":6038},64,[2994,6040,6041],{"class":3000},"     * запускає перед {@code @BeforeAll}, зупиняє після {@code @AfterAll}.\n",[2994,6043,6045],{"class":2996,"line":6044},65,[2994,6046,6012],{"class":3000},[2994,6048,6050],{"class":2996,"line":6049},66,[2994,6051,6052],{"class":3000},"     * \u003Cb>DockerImageName.parse():\u003C/b> Явно вказуємо Docker-образ з версією.\n",[2994,6054,6056],{"class":2996,"line":6055},67,[2994,6057,6058],{"class":3000},"     * Це гарантує, що тести використовують ту саму версію PostgreSQL, що й production.\n",[2994,6060,6062],{"class":2996,"line":6061},68,[2994,6063,6064],{"class":3000},"     */\n",[2994,6066,6068,6070],{"class":2996,"line":6067},69,[2994,6069,4043],{"class":3021},[2994,6071,6072],{"class":3187},"Container\n",[2994,6074,6076,6078,6080,6082,6084,6086,6088,6090,6092],{"class":2996,"line":6075},70,[2994,6077,4396],{"class":3007},[2994,6079,4269],{"class":3007},[2994,6081,4272],{"class":3007},[2994,6083,4057],{"class":3187},[2994,6085,4060],{"class":3021},[2994,6087,3701],{"class":3007},[2994,6089,4065],{"class":3021},[2994,6091,4283],{"class":3191},[2994,6093,6094],{"class":3021}," = \n",[2994,6096,6098,6101,6103,6105,6108,6110,6113,6115,6117],{"class":2996,"line":6097},71,[2994,6099,6100],{"class":3700},"        new",[2994,6102,4057],{"class":3187},[2994,6104,4078],{"class":3021},[2994,6106,6107],{"class":3191},"DockerImageName",[2994,6109,3217],{"class":3021},[2994,6111,6112],{"class":3014},"parse",[2994,6114,3111],{"class":3021},[2994,6116,4081],{"class":3025},[2994,6118,6119],{"class":3021},"))\n",[2994,6121,6123,6125,6127,6129,6132],{"class":2996,"line":6122},72,[2994,6124,4315],{"class":3021},[2994,6126,4092],{"class":3014},[2994,6128,3111],{"class":3021},[2994,6130,6131],{"class":3025},"\"audiobook_test_db\"",[2994,6133,4084],{"class":3021},[2994,6135,6137,6139,6141,6143,6145],{"class":2996,"line":6136},73,[2994,6138,4315],{"class":3021},[2994,6140,4106],{"class":3014},[2994,6142,3111],{"class":3021},[2994,6144,4111],{"class":3025},[2994,6146,4084],{"class":3021},[2994,6148,6150,6152,6154,6156,6159],{"class":2996,"line":6149},74,[2994,6151,4315],{"class":3021},[2994,6153,4120],{"class":3014},[2994,6155,3111],{"class":3021},[2994,6157,6158],{"class":3025},"\"test_password\"",[2994,6160,4084],{"class":3021},[2994,6162,6164,6166,6168,6170,6173,6175],{"class":2996,"line":6163},75,[2994,6165,4315],{"class":3021},[2994,6167,4355],{"class":3014},[2994,6169,3111],{"class":3021},[2994,6171,6172],{"class":3007},"false",[2994,6174,3233],{"class":3021},[2994,6176,6177],{"class":3000},"// false = новий контейнер для кожного запуску тестів\n",[2994,6179,6181],{"class":2996,"line":6180},76,[2994,6182,3062],{"emptyLinePlaceholder":3061},[2994,6184,6186],{"class":2996,"line":6185},77,[2994,6187,6000],{"class":3000},[2994,6189,6191],{"class":2996,"line":6190},78,[2994,6192,6193],{"class":3000},"     * ConnectionManager для тестової БД.\n",[2994,6195,6197],{"class":2996,"line":6196},79,[2994,6198,6199],{"class":3000},"     * Створюється після запуску контейнера у {@code @BeforeAll}.\n",[2994,6201,6203],{"class":2996,"line":6202},80,[2994,6204,6064],{"class":3000},[2994,6206,6208,6210,6212,6215,6218],{"class":2996,"line":6207},81,[2994,6209,4396],{"class":3007},[2994,6211,4269],{"class":3007},[2994,6213,6214],{"class":3187}," ConnectionManager",[2994,6216,6217],{"class":3191}," connectionManager",[2994,6219,3209],{"class":3021},[2994,6221,6223],{"class":2996,"line":6222},82,[2994,6224,3062],{"emptyLinePlaceholder":3061},[2994,6226,6228],{"class":2996,"line":6227},83,[2994,6229,6000],{"class":3000},[2994,6231,6233],{"class":2996,"line":6232},84,[2994,6234,6235],{"class":3000},"     * Ініціалізує ConnectionManager та виконує DDL-скрипт.\n",[2994,6237,6239],{"class":2996,"line":6238},85,[2994,6240,6012],{"class":3000},[2994,6242,6244],{"class":2996,"line":6243},86,[2994,6245,6246],{"class":3000},"     * Викликається один раз перед усіма тестами класу.\n",[2994,6248,6250],{"class":2996,"line":6249},87,[2994,6251,6252],{"class":3000},"     * На цьому етапі PostgreSQL контейнер вже запущений (завдяки @Container).\n",[2994,6254,6256],{"class":2996,"line":6255},88,[2994,6257,6064],{"class":3000},[2994,6259,6261,6263],{"class":2996,"line":6260},89,[2994,6262,4043],{"class":3021},[2994,6264,6265],{"class":3187},"BeforeAll\n",[2994,6267,6269,6271,6274,6277,6280,6283,6286,6288,6291],{"class":2996,"line":6268},90,[2994,6270,4054],{"class":3007},[2994,6272,6273],{"class":3187}," void",[2994,6275,6276],{"class":3014}," initializeDatabase",[2994,6278,6279],{"class":3021},"() ",[2994,6281,6282],{"class":3007},"throws",[2994,6284,6285],{"class":3187}," IOException",[2994,6287,3029],{"class":3021},[2994,6289,6290],{"class":3187},"SQLException",[2994,6292,4034],{"class":3021},[2994,6294,6296],{"class":2996,"line":6295},91,[2994,6297,6298],{"class":3000},"        // Отримати JDBC URL з динамічним портом\n",[2994,6300,6302],{"class":2996,"line":6301},92,[2994,6303,6304],{"class":3000},"        // Приклад: jdbc:postgresql://localhost:32768/audiobook_test_db\n",[2994,6306,6308,6311,6314,6316,6318,6320,6322],{"class":2996,"line":6307},93,[2994,6309,6310],{"class":3187},"        String",[2994,6312,6313],{"class":3191}," jdbcUrl",[2994,6315,3195],{"class":3021},[2994,6317,4283],{"class":3191},[2994,6319,3217],{"class":3021},[2994,6321,4420],{"class":3014},[2994,6323,4382],{"class":3021},[2994,6325,6327,6329,6332,6334,6336,6338,6341],{"class":2996,"line":6326},94,[2994,6328,6310],{"class":3187},[2994,6330,6331],{"class":3191}," username",[2994,6333,3195],{"class":3021},[2994,6335,4283],{"class":3191},[2994,6337,3217],{"class":3021},[2994,6339,6340],{"class":3014},"getUsername",[2994,6342,4382],{"class":3021},[2994,6344,6346,6348,6351,6353,6355,6357,6360],{"class":2996,"line":6345},95,[2994,6347,6310],{"class":3187},[2994,6349,6350],{"class":3191}," password",[2994,6352,3195],{"class":3021},[2994,6354,4283],{"class":3191},[2994,6356,3217],{"class":3021},[2994,6358,6359],{"class":3014},"getPassword",[2994,6361,4382],{"class":3021},[2994,6363,6365],{"class":2996,"line":6364},96,[2994,6366,3062],{"emptyLinePlaceholder":3061},[2994,6368,6370],{"class":2996,"line":6369},97,[2994,6371,6372],{"class":3000},"        // Створити ConnectionManager для тестової БД\n",[2994,6374,6376,6379,6381,6383],{"class":2996,"line":6375},98,[2994,6377,6378],{"class":3021},"        connectionManager = ",[2994,6380,4073],{"class":3700},[2994,6382,6214],{"class":3014},[2994,6384,6385],{"class":3021},"(jdbcUrl, username, password);\n",[2994,6387,6389],{"class":2996,"line":6388},99,[2994,6390,3062],{"emptyLinePlaceholder":3061},[2994,6392,6394],{"class":2996,"line":6393},100,[2994,6395,6396],{"class":3000},"        // Виконати DDL-скрипт (створити таблиці)\n",[2994,6398,6400,6403,6405,6408],{"class":2996,"line":6399},101,[2994,6401,6402],{"class":3014},"        executeDdlScript",[2994,6404,3111],{"class":3021},[2994,6406,6407],{"class":3025},"\"ddl_postgres.sql\"",[2994,6409,3055],{"class":3021},[2994,6411,6413],{"class":2996,"line":6412},102,[2994,6414,4387],{"class":3021},[2994,6416,6418],{"class":2996,"line":6417},103,[2994,6419,3062],{"emptyLinePlaceholder":3061},[2994,6421,6423],{"class":2996,"line":6422},104,[2994,6424,6000],{"class":3000},[2994,6426,6428],{"class":2996,"line":6427},105,[2994,6429,6430],{"class":3000},"     * Очищує всі таблиці перед кожним тестом.\n",[2994,6432,6434],{"class":2996,"line":6433},106,[2994,6435,6012],{"class":3000},[2994,6437,6439],{"class":2996,"line":6438},107,[2994,6440,6441],{"class":3000},"     * Це гарантує ізоляцію тестів: кожен тест починає з порожньої БД.\n",[2994,6443,6445],{"class":2996,"line":6444},108,[2994,6446,6447],{"class":3000},"     * Використовуємо {@code TRUNCATE} замість {@code DELETE}, оскільки:\n",[2994,6449,6451],{"class":2996,"line":6450},109,[2994,6452,6453],{"class":3000},"     * \u003Cul>\n",[2994,6455,6457],{"class":2996,"line":6456},110,[2994,6458,6459],{"class":3000},"     *   \u003Cli>TRUNCATE швидший (не генерує WAL-записи для кожного рядка)\u003C/li>\n",[2994,6461,6463],{"class":2996,"line":6462},111,[2994,6464,6465],{"class":3000},"     *   \u003Cli>TRUNCATE скидає AUTO_INCREMENT лічильники\u003C/li>\n",[2994,6467,6469],{"class":2996,"line":6468},112,[2994,6470,6471],{"class":3000},"     *   \u003Cli>TRUNCATE ... CASCADE автоматично очищає пов'язані таблиці\u003C/li>\n",[2994,6473,6475],{"class":2996,"line":6474},113,[2994,6476,6477],{"class":3000},"     * \u003C/ul>\n",[2994,6479,6481],{"class":2996,"line":6480},114,[2994,6482,6064],{"class":3000},[2994,6484,6486,6488],{"class":2996,"line":6485},115,[2994,6487,4043],{"class":3021},[2994,6489,6490],{"class":3187},"BeforeEach\n",[2994,6492,6494,6496,6499,6501,6503,6506],{"class":2996,"line":6493},116,[2994,6495,4143],{"class":3187},[2994,6497,6498],{"class":3014}," cleanDatabase",[2994,6500,6279],{"class":3021},[2994,6502,6282],{"class":3007},[2994,6504,6505],{"class":3187}," SQLException",[2994,6507,4034],{"class":3021},[2994,6509,6511,6514,6516,6519,6522,6524,6527,6529,6532],{"class":2996,"line":6510},117,[2994,6512,6513],{"class":3700},"        try",[2994,6515,3769],{"class":3021},[2994,6517,6518],{"class":3187},"Connection",[2994,6520,6521],{"class":3191}," conn",[2994,6523,3195],{"class":3021},[2994,6525,6526],{"class":3191},"connectionManager",[2994,6528,3217],{"class":3021},[2994,6530,6531],{"class":3014},"getConnection",[2994,6533,4382],{"class":3021},[2994,6535,6537,6540,6543,6545,6548,6550,6553],{"class":2996,"line":6536},118,[2994,6538,6539],{"class":3187},"             Statement",[2994,6541,6542],{"class":3191}," stmt",[2994,6544,3195],{"class":3021},[2994,6546,6547],{"class":3191},"conn",[2994,6549,3217],{"class":3021},[2994,6551,6552],{"class":3014},"createStatement",[2994,6554,6555],{"class":3021},"()) {\n",[2994,6557,6559],{"class":2996,"line":6558},119,[2994,6560,6561],{"class":3021},"            \n",[2994,6563,6565],{"class":2996,"line":6564},120,[2994,6566,6567],{"class":3000},"            // TRUNCATE всі таблиці у правильному порядку (від дочірніх до батьківських)\n",[2994,6569,6571],{"class":2996,"line":6570},121,[2994,6572,6573],{"class":3000},"            // CASCADE автоматично очистить пов'язані таблиці через FK\n",[2994,6575,6577,6580,6582,6585,6587,6590],{"class":2996,"line":6576},122,[2994,6578,6579],{"class":3191},"            stmt",[2994,6581,3217],{"class":3021},[2994,6583,6584],{"class":3014},"execute",[2994,6586,3111],{"class":3021},[2994,6588,6589],{"class":3025},"\"TRUNCATE TABLE audiobook_files CASCADE\"",[2994,6591,3055],{"class":3021},[2994,6593,6595,6597,6599,6601,6603,6606],{"class":2996,"line":6594},123,[2994,6596,6579],{"class":3191},[2994,6598,3217],{"class":3021},[2994,6600,6584],{"class":3014},[2994,6602,3111],{"class":3021},[2994,6604,6605],{"class":3025},"\"TRUNCATE TABLE listening_progresses CASCADE\"",[2994,6607,3055],{"class":3021},[2994,6609,6611,6613,6615,6617,6619,6622],{"class":2996,"line":6610},124,[2994,6612,6579],{"class":3191},[2994,6614,3217],{"class":3021},[2994,6616,6584],{"class":3014},[2994,6618,3111],{"class":3021},[2994,6620,6621],{"class":3025},"\"TRUNCATE TABLE audiobook_collection CASCADE\"",[2994,6623,3055],{"class":3021},[2994,6625,6627,6629,6631,6633,6635,6638],{"class":2996,"line":6626},125,[2994,6628,6579],{"class":3191},[2994,6630,3217],{"class":3021},[2994,6632,6584],{"class":3014},[2994,6634,3111],{"class":3021},[2994,6636,6637],{"class":3025},"\"TRUNCATE TABLE collections CASCADE\"",[2994,6639,3055],{"class":3021},[2994,6641,6643,6645,6647,6649,6651,6654],{"class":2996,"line":6642},126,[2994,6644,6579],{"class":3191},[2994,6646,3217],{"class":3021},[2994,6648,6584],{"class":3014},[2994,6650,3111],{"class":3021},[2994,6652,6653],{"class":3025},"\"TRUNCATE TABLE audiobooks CASCADE\"",[2994,6655,3055],{"class":3021},[2994,6657,6659,6661,6663,6665,6667,6670],{"class":2996,"line":6658},127,[2994,6660,6579],{"class":3191},[2994,6662,3217],{"class":3021},[2994,6664,6584],{"class":3014},[2994,6666,3111],{"class":3021},[2994,6668,6669],{"class":3025},"\"TRUNCATE TABLE authors CASCADE\"",[2994,6671,3055],{"class":3021},[2994,6673,6675,6677,6679,6681,6683,6686],{"class":2996,"line":6674},128,[2994,6676,6579],{"class":3191},[2994,6678,3217],{"class":3021},[2994,6680,6584],{"class":3014},[2994,6682,3111],{"class":3021},[2994,6684,6685],{"class":3025},"\"TRUNCATE TABLE genres CASCADE\"",[2994,6687,3055],{"class":3021},[2994,6689,6691,6693,6695,6697,6699,6702],{"class":2996,"line":6690},129,[2994,6692,6579],{"class":3191},[2994,6694,3217],{"class":3021},[2994,6696,6584],{"class":3014},[2994,6698,3111],{"class":3021},[2994,6700,6701],{"class":3025},"\"TRUNCATE TABLE users CASCADE\"",[2994,6703,3055],{"class":3021},[2994,6705,6707],{"class":2996,"line":6706},130,[2994,6708,6709],{"class":3021},"        }\n",[2994,6711,6713],{"class":2996,"line":6712},131,[2994,6714,4387],{"class":3021},[2994,6716,6718],{"class":2996,"line":6717},132,[2994,6719,3062],{"emptyLinePlaceholder":3061},[2994,6721,6723],{"class":2996,"line":6722},133,[2994,6724,6000],{"class":3000},[2994,6726,6728],{"class":2996,"line":6727},134,[2994,6729,6730],{"class":3000},"     * Закриває ConnectionManager після всіх тестів.\n",[2994,6732,6734],{"class":2996,"line":6733},135,[2994,6735,6736],{"class":3000},"     * Контейнер PostgreSQL автоматично зупиняється Testcontainers.\n",[2994,6738,6740],{"class":2996,"line":6739},136,[2994,6741,6064],{"class":3000},[2994,6743,6745,6747],{"class":2996,"line":6744},137,[2994,6746,4043],{"class":3021},[2994,6748,6749],{"class":3187},"AfterEach\n",[2994,6751,6753,6755,6758],{"class":2996,"line":6752},138,[2994,6754,4143],{"class":3187},[2994,6756,6757],{"class":3014}," tearDown",[2994,6759,4407],{"class":3021},[2994,6761,6763],{"class":2996,"line":6762},139,[2994,6764,6765],{"class":3000},"        // У цій реалізації нічого не робимо — ConnectionManager залишається відкритим\n",[2994,6767,6769],{"class":2996,"line":6768},140,[2994,6770,6771],{"class":3000},"        // для наступних тестів. Закриття відбувається у @AfterAll (якщо потрібно).\n",[2994,6773,6775],{"class":2996,"line":6774},141,[2994,6776,4387],{"class":3021},[2994,6778,6780],{"class":2996,"line":6779},142,[2994,6781,3062],{"emptyLinePlaceholder":3061},[2994,6783,6785],{"class":2996,"line":6784},143,[2994,6786,6787],{"class":3000},"    // ═══════════════════════════════════════════════════════════════════════\n",[2994,6789,6791],{"class":2996,"line":6790},144,[2994,6792,6793],{"class":3000},"    // Утилітні методи (аналогічні AbstractRepositoryTest з H2)\n",[2994,6795,6797],{"class":2996,"line":6796},145,[2994,6798,6787],{"class":3000},[2994,6800,6802],{"class":2996,"line":6801},146,[2994,6803,3062],{"emptyLinePlaceholder":3061},[2994,6805,6807],{"class":2996,"line":6806},147,[2994,6808,6000],{"class":3000},[2994,6810,6812],{"class":2996,"line":6811},148,[2994,6813,6814],{"class":3000},"     * Виконує DDL-скрипт з ресурсів.\n",[2994,6816,6818],{"class":2996,"line":6817},149,[2994,6819,6012],{"class":3000},[2994,6821,6823],{"class":2996,"line":6822},150,[2994,6824,6825],{"class":3000},"     * Скрипт має бути PostgreSQL-сумісним (не H2!).\n",[2994,6827,6829],{"class":2996,"line":6828},151,[2994,6830,6831],{"class":3000},"     * Розташування: {@code src/test/resources/ddl_postgres.sql}\n",[2994,6833,6835],{"class":2996,"line":6834},152,[2994,6836,6064],{"class":3000},[2994,6838,6840,6842,6844,6846,6849,6851,6853,6856,6858,6860,6862,6864,6866],{"class":2996,"line":6839},153,[2994,6841,4396],{"class":3007},[2994,6843,4269],{"class":3007},[2994,6845,6273],{"class":3187},[2994,6847,6848],{"class":3014}," executeDdlScript",[2994,6850,3111],{"class":3021},[2994,6852,3188],{"class":3187},[2994,6854,6855],{"class":3191}," scriptPath",[2994,6857,3118],{"class":3021},[2994,6859,6282],{"class":3007},[2994,6861,6285],{"class":3187},[2994,6863,3029],{"class":3021},[2994,6865,6290],{"class":3187},[2994,6867,4034],{"class":3021},[2994,6869,6871,6873,6875,6877,6880],{"class":2996,"line":6870},154,[2994,6872,6310],{"class":3187},[2994,6874,3192],{"class":3191},[2994,6876,3195],{"class":3021},[2994,6878,6879],{"class":3014},"loadResourceAsString",[2994,6881,6882],{"class":3021},"(scriptPath);\n",[2994,6884,6886],{"class":2996,"line":6885},155,[2994,6887,6888],{"class":3021},"        \n",[2994,6890,6892,6894,6896,6898,6900,6902,6904,6906,6908],{"class":2996,"line":6891},156,[2994,6893,6513],{"class":3700},[2994,6895,3769],{"class":3021},[2994,6897,6518],{"class":3187},[2994,6899,6521],{"class":3191},[2994,6901,3195],{"class":3021},[2994,6903,6526],{"class":3191},[2994,6905,3217],{"class":3021},[2994,6907,6531],{"class":3014},[2994,6909,4382],{"class":3021},[2994,6911,6913,6915,6917,6919,6921,6923,6925],{"class":2996,"line":6912},157,[2994,6914,6539],{"class":3187},[2994,6916,6542],{"class":3191},[2994,6918,3195],{"class":3021},[2994,6920,6547],{"class":3191},[2994,6922,3217],{"class":3021},[2994,6924,6552],{"class":3014},[2994,6926,6555],{"class":3021},[2994,6928,6930],{"class":2996,"line":6929},158,[2994,6931,6561],{"class":3021},[2994,6933,6935],{"class":2996,"line":6934},159,[2994,6936,6937],{"class":3000},"            // PostgreSQL підтримує багаторядкові команди через ;\n",[2994,6939,6941,6944,6947,6950,6952,6954,6956,6959,6961,6964],{"class":2996,"line":6940},160,[2994,6942,6943],{"class":3187},"            String",[2994,6945,6946],{"class":3021},"[] ",[2994,6948,6949],{"class":3191},"commands",[2994,6951,3195],{"class":3021},[2994,6953,2988],{"class":3191},[2994,6955,3217],{"class":3021},[2994,6957,6958],{"class":3014},"split",[2994,6960,3111],{"class":3021},[2994,6962,6963],{"class":3025},"\";\"",[2994,6965,3055],{"class":3021},[2994,6967,6969,6972,6974,6976,6979,6982],{"class":2996,"line":6968},161,[2994,6970,6971],{"class":3700},"            for",[2994,6973,3769],{"class":3021},[2994,6975,3188],{"class":3187},[2994,6977,6978],{"class":3191}," command",[2994,6980,6981],{"class":3700}," :",[2994,6983,6984],{"class":3021}," commands) {\n",[2994,6986,6988,6991,6994,6996,6999,7001,7004],{"class":2996,"line":6987},162,[2994,6989,6990],{"class":3187},"                String",[2994,6992,6993],{"class":3191}," trimmed",[2994,6995,3195],{"class":3021},[2994,6997,6998],{"class":3191},"command",[2994,7000,3217],{"class":3021},[2994,7002,7003],{"class":3014},"trim",[2994,7005,4382],{"class":3021},[2994,7007,7009,7012,7015,7018,7020,7023,7026,7028,7030,7033,7035,7038],{"class":2996,"line":7008},163,[2994,7010,7011],{"class":3700},"                if",[2994,7013,7014],{"class":3021}," (!",[2994,7016,7017],{"class":3191},"trimmed",[2994,7019,3217],{"class":3021},[2994,7021,7022],{"class":3014},"isEmpty",[2994,7024,7025],{"class":3021},"() && !",[2994,7027,7017],{"class":3191},[2994,7029,3217],{"class":3021},[2994,7031,7032],{"class":3014},"startsWith",[2994,7034,3111],{"class":3021},[2994,7036,7037],{"class":3025},"\"--\"",[2994,7039,7040],{"class":3021},")) {\n",[2994,7042,7044,7047,7049,7051],{"class":2996,"line":7043},164,[2994,7045,7046],{"class":3191},"                    stmt",[2994,7048,3217],{"class":3021},[2994,7050,6584],{"class":3014},[2994,7052,7053],{"class":3021},"(trimmed);\n",[2994,7055,7057],{"class":2996,"line":7056},165,[2994,7058,7059],{"class":3021},"                }\n",[2994,7061,7063],{"class":2996,"line":7062},166,[2994,7064,7065],{"class":3021},"            }\n",[2994,7067,7069],{"class":2996,"line":7068},167,[2994,7070,6709],{"class":3021},[2994,7072,7074],{"class":2996,"line":7073},168,[2994,7075,4387],{"class":3021},[2994,7077,7079],{"class":2996,"line":7078},169,[2994,7080,3062],{"emptyLinePlaceholder":3061},[2994,7082,7084],{"class":2996,"line":7083},170,[2994,7085,6000],{"class":3000},[2994,7087,7089],{"class":2996,"line":7088},171,[2994,7090,7091],{"class":3000},"     * Завантажує файл з ресурсів як рядок.\n",[2994,7093,7095],{"class":2996,"line":7094},172,[2994,7096,6064],{"class":3000},[2994,7098,7100,7102,7104,7106,7109,7111,7113,7116,7118,7120,7122],{"class":2996,"line":7099},173,[2994,7101,4396],{"class":3007},[2994,7103,4269],{"class":3007},[2994,7105,4401],{"class":3187},[2994,7107,7108],{"class":3014}," loadResourceAsString",[2994,7110,3111],{"class":3021},[2994,7112,3188],{"class":3187},[2994,7114,7115],{"class":3191}," resourcePath",[2994,7117,3118],{"class":3021},[2994,7119,6282],{"class":3007},[2994,7121,6285],{"class":3187},[2994,7123,4034],{"class":3021},[2994,7125,7127,7129,7131,7134,7137,7139,7141,7144],{"class":2996,"line":7126},174,[2994,7128,6513],{"class":3700},[2994,7130,3769],{"class":3021},[2994,7132,7133],{"class":3187},"BufferedReader",[2994,7135,7136],{"class":3191}," reader",[2994,7138,3195],{"class":3021},[2994,7140,4073],{"class":3700},[2994,7142,7143],{"class":3014}," BufferedReader",[2994,7145,7146],{"class":3021},"(\n",[2994,7148,7150,7153,7156],{"class":2996,"line":7149},175,[2994,7151,7152],{"class":3700},"                new",[2994,7154,7155],{"class":3014}," InputStreamReader",[2994,7157,7146],{"class":3021},[2994,7159,7161,7164,7166],{"class":2996,"line":7160},176,[2994,7162,7163],{"class":3191},"                    AbstractPostgresIntegrationTest",[2994,7165,3217],{"class":3021},[2994,7167,7168],{"class":3191},"class\n",[2994,7170,7172,7175,7178],{"class":2996,"line":7171},177,[2994,7173,7174],{"class":3021},"                        .",[2994,7176,7177],{"class":3014},"getClassLoader",[2994,7179,7180],{"class":3021},"()\n",[2994,7182,7184,7186,7189],{"class":2996,"line":7183},178,[2994,7185,7174],{"class":3021},[2994,7187,7188],{"class":3014},"getResourceAsStream",[2994,7190,7191],{"class":3021},"(resourcePath),\n",[2994,7193,7195,7198,7200,7203],{"class":2996,"line":7194},179,[2994,7196,7197],{"class":3191},"                    StandardCharsets",[2994,7199,3217],{"class":3021},[2994,7201,7202],{"class":3191},"UTF_8",[2994,7204,7205],{"class":3021},"))) {\n",[2994,7207,7209,7212,7214,7216,7219,7222,7225,7227,7230,7232,7235,7237,7240,7244,7246],{"class":2996,"line":7208},180,[2994,7210,7211],{"class":3700},"            return",[2994,7213,7136],{"class":3191},[2994,7215,3217],{"class":3021},[2994,7217,7218],{"class":3014},"lines",[2994,7220,7221],{"class":3021},"().",[2994,7223,7224],{"class":3014},"collect",[2994,7226,3111],{"class":3021},[2994,7228,7229],{"class":3191},"Collectors",[2994,7231,3217],{"class":3021},[2994,7233,7234],{"class":3014},"joining",[2994,7236,3111],{"class":3021},[2994,7238,7239],{"class":3025},"\"",[2994,7241,7243],{"class":7242},"sjcCO","\\n",[2994,7245,7239],{"class":3025},[2994,7247,7248],{"class":3021},"));\n",[2994,7250,7252],{"class":2996,"line":7251},181,[2994,7253,6709],{"class":3021},[2994,7255,7257],{"class":2996,"line":7256},182,[2994,7258,4387],{"class":3021},[2994,7260,7262],{"class":2996,"line":7261},183,[2994,7263,3062],{"emptyLinePlaceholder":3061},[2994,7265,7267],{"class":2996,"line":7266},184,[2994,7268,6000],{"class":3000},[2994,7270,7272],{"class":2996,"line":7271},185,[2994,7273,7274],{"class":3000},"     * Підраховує кількість рядків у таблиці.\n",[2994,7276,7278],{"class":2996,"line":7277},186,[2994,7279,6064],{"class":3000},[2994,7281,7283,7285,7288,7291,7293,7295,7298,7300,7302,7304],{"class":2996,"line":7282},187,[2994,7284,4396],{"class":3007},[2994,7286,7287],{"class":3187}," long",[2994,7289,7290],{"class":3014}," countRowsInTable",[2994,7292,3111],{"class":3021},[2994,7294,3188],{"class":3187},[2994,7296,7297],{"class":3191}," tableName",[2994,7299,3118],{"class":3021},[2994,7301,6282],{"class":3007},[2994,7303,6505],{"class":3187},[2994,7305,4034],{"class":3021},[2994,7307,7309,7311,7313,7315,7318],{"class":2996,"line":7308},188,[2994,7310,6310],{"class":3187},[2994,7312,3192],{"class":3191},[2994,7314,3195],{"class":3021},[2994,7316,7317],{"class":3025},"\"SELECT COUNT(*) FROM \"",[2994,7319,7320],{"class":3021}," + tableName;\n",[2994,7322,7324,7326,7328,7330,7332,7334,7336,7338,7340],{"class":2996,"line":7323},189,[2994,7325,6513],{"class":3700},[2994,7327,3769],{"class":3021},[2994,7329,6518],{"class":3187},[2994,7331,6521],{"class":3191},[2994,7333,3195],{"class":3021},[2994,7335,6526],{"class":3191},[2994,7337,3217],{"class":3021},[2994,7339,6531],{"class":3014},[2994,7341,4382],{"class":3021},[2994,7343,7345,7347,7349,7351,7353,7355,7357],{"class":2996,"line":7344},190,[2994,7346,6539],{"class":3187},[2994,7348,6542],{"class":3191},[2994,7350,3195],{"class":3021},[2994,7352,6547],{"class":3191},[2994,7354,3217],{"class":3021},[2994,7356,6552],{"class":3014},[2994,7358,4382],{"class":3021},[2994,7360,7362,7365,7368,7370,7372,7374,7377],{"class":2996,"line":7361},191,[2994,7363,7364],{"class":3007},"             var",[2994,7366,7367],{"class":3191}," rs",[2994,7369,3195],{"class":3021},[2994,7371,3214],{"class":3191},[2994,7373,3217],{"class":3021},[2994,7375,7376],{"class":3014},"executeQuery",[2994,7378,7379],{"class":3021},"(sql)) {\n",[2994,7381,7383,7386,7388,7391],{"class":2996,"line":7382},192,[2994,7384,7385],{"class":3191},"            rs",[2994,7387,3217],{"class":3021},[2994,7389,7390],{"class":3014},"next",[2994,7392,4382],{"class":3021},[2994,7394,7396,7398,7400,7402,7405,7407,7410],{"class":2996,"line":7395},193,[2994,7397,7211],{"class":3700},[2994,7399,7367],{"class":3191},[2994,7401,3217],{"class":3021},[2994,7403,7404],{"class":3014},"getLong",[2994,7406,3111],{"class":3021},[2994,7408,7409],{"class":3114},"1",[2994,7411,3055],{"class":3021},[2994,7413,7415],{"class":2996,"line":7414},194,[2994,7416,6709],{"class":3021},[2994,7418,7420],{"class":2996,"line":7419},195,[2994,7421,4387],{"class":3021},[2994,7423,7425],{"class":2996,"line":7424},196,[2994,7426,3062],{"emptyLinePlaceholder":3061},[2994,7428,7430],{"class":2996,"line":7429},197,[2994,7431,6000],{"class":3000},[2994,7433,7435],{"class":2996,"line":7434},198,[2994,7436,7437],{"class":3000},"     * Виконує довільний SQL-запит (для підготовки тестових даних).\n",[2994,7439,7441],{"class":2996,"line":7440},199,[2994,7442,6064],{"class":3000},[2994,7444,7446,7448,7450,7453,7455,7457,7459,7461,7463,7465],{"class":2996,"line":7445},200,[2994,7447,4396],{"class":3007},[2994,7449,6273],{"class":3187},[2994,7451,7452],{"class":3014}," executeSql",[2994,7454,3111],{"class":3021},[2994,7456,3188],{"class":3187},[2994,7458,3192],{"class":3191},[2994,7460,3118],{"class":3021},[2994,7462,6282],{"class":3007},[2994,7464,6505],{"class":3187},[2994,7466,4034],{"class":3021},[2994,7468,7470,7472,7474,7476,7478,7480,7482,7484,7486],{"class":2996,"line":7469},201,[2994,7471,6513],{"class":3700},[2994,7473,3769],{"class":3021},[2994,7475,6518],{"class":3187},[2994,7477,6521],{"class":3191},[2994,7479,3195],{"class":3021},[2994,7481,6526],{"class":3191},[2994,7483,3217],{"class":3021},[2994,7485,6531],{"class":3014},[2994,7487,4382],{"class":3021},[2994,7489,7491,7493,7495,7497,7499,7501,7503],{"class":2996,"line":7490},202,[2994,7492,6539],{"class":3187},[2994,7494,6542],{"class":3191},[2994,7496,3195],{"class":3021},[2994,7498,6547],{"class":3191},[2994,7500,3217],{"class":3021},[2994,7502,6552],{"class":3014},[2994,7504,6555],{"class":3021},[2994,7506,7508,7510,7512,7514],{"class":2996,"line":7507},203,[2994,7509,6579],{"class":3191},[2994,7511,3217],{"class":3021},[2994,7513,6584],{"class":3014},[2994,7515,7516],{"class":3021},"(sql);\n",[2994,7518,7520],{"class":2996,"line":7519},204,[2994,7521,6709],{"class":3021},[2994,7523,7525],{"class":2996,"line":7524},205,[2994,7526,4387],{"class":3021},[2994,7528,7530],{"class":2996,"line":7529},206,[2994,7531,4183],{"class":3021},[2964,7533,7534],{},[2971,7535,7536],{},"Ключові архітектурні рішення:",[4203,7538,7539,7552,7565,7585],{},[3962,7540,7541,3769,7544,7547,7548,7551],{},[2971,7542,7543],{},"Рядки 72–80",[2991,7545,7546],{},"@Container static","): Контейнер є ",[2991,7549,7550],{},"static"," — це означає, що він запускається один раз для всього тестового класу, а не для кожного тесту. Це критично важливо для продуктивності: запуск PostgreSQL контейнера займає 2–5 секунд, і робити це для кожного тесту неприйнятно.",[3962,7553,7554,3769,7557,7560,7561,7564],{},[2971,7555,7556],{},"Рядок 80",[2991,7558,7559],{},"withReuse(false)","): За замовчуванням Testcontainers видаляє контейнер після завершення тестів. ",[2991,7562,7563],{},"withReuse(true)"," дозволяє залишити контейнер запущеним між запусками тестів (для ще більшої швидкості), але це ускладнює налагодження.",[3962,7566,7567,3769,7570,7573,7574,7577,7578,7580,7581,7584],{},[2971,7568,7569],{},"Рядки 97–108",[2991,7571,7572],{},"initializeDatabase()","): Метод викликається ",[2971,7575,7576],{},"після"," запуску контейнера (Testcontainers гарантує це через ",[2991,7579,5607],{},"). На цьому етапі ",[2991,7582,7583],{},"POSTGRES.getJdbcUrl()"," вже повертає валідний URL з динамічним портом.",[3962,7586,7587,3769,7590,7593,7594,7597,7598,7601,7602],{},[2971,7588,7589],{},"Рядки 125–140",[2991,7591,7592],{},"cleanDatabase()","): Використовуємо ",[2991,7595,7596],{},"TRUNCATE"," замість ",[2991,7599,7600],{},"DELETE FROM"," з кількох причин:",[3959,7603,7604,7613,7624],{},[3962,7605,7606,7609,7610,7612],{},[2971,7607,7608],{},"Продуктивність:"," ",[2991,7611,7596],{}," не генерує WAL (Write-Ahead Log) записи для кожного рядка — він просто скидає файл даних таблиці.",[3962,7614,7615,7609,7618,7620,7621,7623],{},[2971,7616,7617],{},"Скидання послідовностей:",[2991,7619,7596],{}," скидає ",[2991,7622,3454],{}," лічильники до початкового значення.",[3962,7625,7626,7609,7629,7632],{},[2971,7627,7628],{},"CASCADE:",[2991,7630,7631],{},"TRUNCATE ... CASCADE"," автоматично очищає всі таблиці, що мають FK на цю таблицю.",[4762,7634,7635,7640,7646,7652],{},[2964,7636,7637],{},[2971,7638,7639],{},"Чому порядок TRUNCATE важливий?",[2964,7641,7642,7643,7645],{},"PostgreSQL не дозволяє ",[2991,7644,7596],{}," батьківської таблиці, якщо на неї посилаються дочірні таблиці (через FK), навіть якщо дочірні таблиці порожні. Тому ми очищаємо таблиці у порядку від дочірніх до батьківських:",[2984,7647,7650],{"className":7648,"code":7649,"language":4194},[4192],"audiobook_files (FK → audiobooks)\naudiobooks (FK → authors, genres)\nauthors (батьківська)\ngenres (батьківська)\n",[2991,7651,7649],{"__ignoreMap":2989},[2964,7653,7654,7655,7657],{},"Альтернатива — використовувати ",[2991,7656,7631],{},", що автоматично очистить всі пов'язані таблиці, але це менш явно і може приховати помилки у структурі FK.",[3307,7659],{},[2959,7661,7663],{"id":7662},"ddl-скрипт-для-postgresql","DDL-скрипт для PostgreSQL",[2964,7665,7666,7667,7670],{},"Створимо ",[2991,7668,7669],{},"src/test/resources/ddl_postgres.sql"," — повноцінний PostgreSQL DDL з усіма специфічними типами:",[2984,7672,7674],{"className":2986,"code":7673,"language":2988,"meta":5001,"style":2989},"-- PostgreSQL 15 DDL для тестів\n-- Використовує всі можливості PostgreSQL (ENUM, CHECK, CASCADE)\n\n-- ENUM для форматів аудіофайлів\nCREATE TYPE file_format_enum AS ENUM ('mp3', 'ogg', 'wav', 'm4b', 'aac', 'flac');\n\n-- Таблиця авторів\nCREATE TABLE authors (\n    id          UUID         PRIMARY KEY,\n    first_name  VARCHAR(64)  NOT NULL,\n    last_name   VARCHAR(64)  NOT NULL,\n    bio         TEXT,\n    image_path  VARCHAR(2048)\n);\n\n-- Таблиця жанрів\nCREATE TABLE genres (\n    id          UUID         PRIMARY KEY,\n    name        VARCHAR(64)  NOT NULL UNIQUE,\n    description TEXT\n);\n\n-- Таблиця аудіокниг\nCREATE TABLE audiobooks (\n    id               UUID         PRIMARY KEY,\n    author_id        UUID         NOT NULL,\n    genre_id         UUID         NOT NULL,\n    title            VARCHAR(255) NOT NULL,\n    duration         INTEGER      NOT NULL CHECK (duration > 0),\n    release_year     INTEGER      NOT NULL \n                     CHECK (release_year >= 1900 AND release_year \u003C= EXTRACT(YEAR FROM CURRENT_DATE) + 1),\n    description      TEXT,\n    cover_image_path VARCHAR(2048),\n    \n    CONSTRAINT audiobooks_author_fk \n        FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE,\n    CONSTRAINT audiobooks_genre_fk \n        FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE\n);\n\nCREATE INDEX audiobooks_author_id_idx ON audiobooks(author_id);\nCREATE INDEX audiobooks_genre_id_idx  ON audiobooks(genre_id);\n\n-- Таблиця користувачів\nCREATE TABLE users (\n    id            UUID         PRIMARY KEY,\n    username      VARCHAR(64)  NOT NULL UNIQUE \n                  CHECK (LENGTH(TRIM(username)) > 0),\n    password_hash VARCHAR(128) NOT NULL,\n    email         VARCHAR(376),\n    avatar_path   VARCHAR(2048)\n);\n\nCREATE INDEX users_email_idx ON users(email);\n\n-- Таблиця колекцій користувачів\nCREATE TABLE collections (\n    id         UUID      PRIMARY KEY,\n    user_id    UUID      NOT NULL,\n    name       VARCHAR(128) NOT NULL CHECK (LENGTH(TRIM(name)) > 0),\n    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    \n    CONSTRAINT collections_user_fk \n        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE\n);\n\n-- Зв'язок багато-до-багатьох: колекції ↔ аудіокниги\nCREATE TABLE audiobook_collection (\n    collection_id UUID NOT NULL,\n    audiobook_id  UUID NOT NULL,\n    \n    PRIMARY KEY (collection_id, audiobook_id),\n    \n    CONSTRAINT ac_collection_fk \n        FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,\n    CONSTRAINT ac_audiobook_fk \n        FOREIGN KEY (audiobook_id) REFERENCES audiobooks(id) ON DELETE CASCADE\n);\n\n-- Таблиця файлів аудіокниг (використовує ENUM)\nCREATE TABLE audiobook_files (\n    id           UUID             PRIMARY KEY,\n    audiobook_id UUID             NOT NULL,\n    file_path    VARCHAR(2048)    NOT NULL CHECK (LENGTH(TRIM(file_path)) > 0),\n    format       file_format_enum NOT NULL,  -- ← PostgreSQL ENUM\n    size         INTEGER          CHECK (size IS NULL OR size > 0),\n    \n    CONSTRAINT af_audiobook_fk \n        FOREIGN KEY (audiobook_id) REFERENCES audiobooks(id) ON DELETE CASCADE\n);\n\nCREATE INDEX audiobook_files_audiobook_id_idx ON audiobook_files(audiobook_id);\n\n-- Таблиця прогресу прослуховування\nCREATE TABLE listening_progresses (\n    id            UUID      PRIMARY KEY,\n    user_id       UUID      NOT NULL,\n    audiobook_id  UUID      NOT NULL,\n    position      INTEGER   NOT NULL CHECK (position >= 0),\n    last_listened TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n    \n    CONSTRAINT lp_user_fk \n        FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,\n    CONSTRAINT lp_audiobook_fk \n        FOREIGN KEY (audiobook_id) REFERENCES audiobooks(id) ON DELETE CASCADE\n);\n\nCREATE INDEX listening_progresses_user_id_idx      ON listening_progresses(user_id);\nCREATE INDEX listening_progresses_audiobook_id_idx ON listening_progresses(audiobook_id);\n",[2991,7675,7676,7681,7686,7690,7695,7731,7735,7740,7751,7760,7779,7796,7806,7819,7823,7827,7832,7843,7851,7872,7880,7884,7888,7893,7904,7913,7922,7931,7949,7972,7984,8014,8023,8036,8041,8049,8068,8075,8090,8094,8098,8114,8129,8133,8138,8149,8158,8177,8199,8217,8231,8244,8248,8252,8266,8270,8275,8286,8295,8304,8341,8355,8359,8366,8380,8384,8388,8393,8404,8413,8422,8426,8434,8438,8445,8461,8468,8482,8486,8490,8495,8505,8514,8523,8555,8565,8599,8603,8610,8622,8626,8630,8644,8648,8653,8664,8673,8682,8691,8710,8721,8725,8732,8746,8753,8765,8769,8773,8788],{"__ignoreMap":2989},[2994,7677,7678],{"class":2996,"line":2997},[2994,7679,7680],{"class":3000},"-- PostgreSQL 15 DDL для тестів\n",[2994,7682,7683],{"class":2996,"line":3004},[2994,7684,7685],{"class":3000},"-- Використовує всі можливості PostgreSQL (ENUM, CHECK, CASCADE)\n",[2994,7687,7688],{"class":2996,"line":3058},[2994,7689,3062],{"emptyLinePlaceholder":3061},[2994,7691,7692],{"class":2996,"line":3065},[2994,7693,7694],{"class":3000},"-- ENUM для форматів аудіофайлів\n",[2994,7696,7697,7699,7701,7703,7705,7707,7709,7711,7713,7715,7717,7719,7721,7723,7725,7727,7729],{"class":2996,"line":3079},[2994,7698,3008],{"class":3007},[2994,7700,3011],{"class":3007},[2994,7702,3015],{"class":3014},[2994,7704,3018],{"class":3007},[2994,7706,3022],{"class":3021},[2994,7708,3026],{"class":3025},[2994,7710,3029],{"class":3021},[2994,7712,3032],{"class":3025},[2994,7714,3029],{"class":3021},[2994,7716,3037],{"class":3025},[2994,7718,3029],{"class":3021},[2994,7720,3042],{"class":3025},[2994,7722,3029],{"class":3021},[2994,7724,3047],{"class":3025},[2994,7726,3029],{"class":3021},[2994,7728,3052],{"class":3025},[2994,7730,3055],{"class":3021},[2994,7732,7733],{"class":2996,"line":3091},[2994,7734,3062],{"emptyLinePlaceholder":3061},[2994,7736,7737],{"class":2996,"line":3102},[2994,7738,7739],{"class":3000},"-- Таблиця авторів\n",[2994,7741,7742,7744,7746,7749],{"class":2996,"line":3125},[2994,7743,3008],{"class":3007},[2994,7745,3070],{"class":3007},[2994,7747,7748],{"class":3014}," authors",[2994,7750,3076],{"class":3021},[2994,7752,7753,7756,7758],{"class":2996,"line":3139},[2994,7754,7755],{"class":3021},"    id          UUID         ",[2994,7757,3085],{"class":3007},[2994,7759,3088],{"class":3021},[2994,7761,7762,7765,7767,7769,7772,7775,7777],{"class":2996,"line":3148},[2994,7763,7764],{"class":3021},"    first_name  ",[2994,7766,3108],{"class":3007},[2994,7768,3111],{"class":3021},[2994,7770,7771],{"class":3114},"64",[2994,7773,7774],{"class":3021},")  ",[2994,7776,3097],{"class":3007},[2994,7778,3088],{"class":3021},[2994,7780,7781,7784,7786,7788,7790,7792,7794],{"class":2996,"line":3845},[2994,7782,7783],{"class":3021},"    last_name   ",[2994,7785,3108],{"class":3007},[2994,7787,3111],{"class":3021},[2994,7789,7771],{"class":3114},[2994,7791,7774],{"class":3021},[2994,7793,3097],{"class":3007},[2994,7795,3088],{"class":3021},[2994,7797,7798,7801,7804],{"class":2996,"line":3851},[2994,7799,7800],{"class":3021},"    bio         ",[2994,7802,7803],{"class":3007},"TEXT",[2994,7805,3088],{"class":3021},[2994,7807,7808,7811,7813,7815,7817],{"class":2996,"line":3856},[2994,7809,7810],{"class":3021},"    image_path  ",[2994,7812,3108],{"class":3007},[2994,7814,3111],{"class":3021},[2994,7816,3115],{"class":3114},[2994,7818,4084],{"class":3021},[2994,7820,7821],{"class":2996,"line":3861},[2994,7822,3055],{"class":3021},[2994,7824,7825],{"class":2996,"line":3867},[2994,7826,3062],{"emptyLinePlaceholder":3061},[2994,7828,7829],{"class":2996,"line":3873},[2994,7830,7831],{"class":3000},"-- Таблиця жанрів\n",[2994,7833,7834,7836,7838,7841],{"class":2996,"line":3879},[2994,7835,3008],{"class":3007},[2994,7837,3070],{"class":3007},[2994,7839,7840],{"class":3014}," genres",[2994,7842,3076],{"class":3021},[2994,7844,7845,7847,7849],{"class":2996,"line":3885},[2994,7846,7755],{"class":3021},[2994,7848,3085],{"class":3007},[2994,7850,3088],{"class":3021},[2994,7852,7853,7856,7859,7861,7863,7865,7867,7870],{"class":2996,"line":3891},[2994,7854,7855],{"class":3007},"    name",[2994,7857,7858],{"class":3007},"        VARCHAR",[2994,7860,3111],{"class":3021},[2994,7862,7771],{"class":3114},[2994,7864,7774],{"class":3021},[2994,7866,3097],{"class":3007},[2994,7868,7869],{"class":3007}," UNIQUE",[2994,7871,3088],{"class":3021},[2994,7873,7874,7877],{"class":2996,"line":3897},[2994,7875,7876],{"class":3007},"    description",[2994,7878,7879],{"class":3007}," TEXT\n",[2994,7881,7882],{"class":2996,"line":3903},[2994,7883,3055],{"class":3021},[2994,7885,7886],{"class":2996,"line":3908},[2994,7887,3062],{"emptyLinePlaceholder":3061},[2994,7889,7890],{"class":2996,"line":3914},[2994,7891,7892],{"class":3000},"-- Таблиця аудіокниг\n",[2994,7894,7895,7897,7899,7902],{"class":2996,"line":3920},[2994,7896,3008],{"class":3007},[2994,7898,3070],{"class":3007},[2994,7900,7901],{"class":3014}," audiobooks",[2994,7903,3076],{"class":3021},[2994,7905,7906,7909,7911],{"class":2996,"line":3926},[2994,7907,7908],{"class":3021},"    id               UUID         ",[2994,7910,3085],{"class":3007},[2994,7912,3088],{"class":3021},[2994,7914,7915,7918,7920],{"class":2996,"line":3931},[2994,7916,7917],{"class":3021},"    author_id        UUID         ",[2994,7919,3097],{"class":3007},[2994,7921,3088],{"class":3021},[2994,7923,7924,7927,7929],{"class":2996,"line":3937},[2994,7925,7926],{"class":3021},"    genre_id         UUID         ",[2994,7928,3097],{"class":3007},[2994,7930,3088],{"class":3021},[2994,7932,7933,7936,7938,7940,7943,7945,7947],{"class":2996,"line":3943},[2994,7934,7935],{"class":3021},"    title            ",[2994,7937,3108],{"class":3007},[2994,7939,3111],{"class":3021},[2994,7941,7942],{"class":3114},"255",[2994,7944,3118],{"class":3021},[2994,7946,3097],{"class":3007},[2994,7948,3088],{"class":3021},[2994,7950,7951,7954,7957,7960,7963,7966,7969],{"class":2996,"line":3949},[2994,7952,7953],{"class":3021},"    duration         ",[2994,7955,7956],{"class":3007},"INTEGER",[2994,7958,7959],{"class":3007},"      NOT NULL",[2994,7961,7962],{"class":3007}," CHECK",[2994,7964,7965],{"class":3021}," (duration > ",[2994,7967,7968],{"class":3114},"0",[2994,7970,7971],{"class":3021},"),\n",[2994,7973,7974,7977,7979,7981],{"class":2996,"line":5348},[2994,7975,7976],{"class":3021},"    release_year     ",[2994,7978,7956],{"class":3007},[2994,7980,7959],{"class":3007},[2994,7982,7983],{"class":3021}," \n",[2994,7985,7986,7989,7992,7995,7998,8001,8004,8007,8010,8012],{"class":2996,"line":5365},[2994,7987,7988],{"class":3007},"                     CHECK",[2994,7990,7991],{"class":3021}," (release_year >= ",[2994,7993,7994],{"class":3114},"1900",[2994,7996,7997],{"class":3007}," AND",[2994,7999,8000],{"class":3021}," release_year \u003C= EXTRACT(",[2994,8002,8003],{"class":3007},"YEAR",[2994,8005,8006],{"class":3007}," FROM",[2994,8008,8009],{"class":3021}," CURRENT_DATE) + ",[2994,8011,7409],{"class":3114},[2994,8013,7971],{"class":3021},[2994,8015,8016,8018,8021],{"class":2996,"line":5382},[2994,8017,7876],{"class":3007},[2994,8019,8020],{"class":3007},"      TEXT",[2994,8022,3088],{"class":3021},[2994,8024,8025,8028,8030,8032,8034],{"class":2996,"line":5391},[2994,8026,8027],{"class":3021},"    cover_image_path ",[2994,8029,3108],{"class":3007},[2994,8031,3111],{"class":3021},[2994,8033,3115],{"class":3114},[2994,8035,7971],{"class":3021},[2994,8037,8038],{"class":2996,"line":5396},[2994,8039,8040],{"class":3021},"    \n",[2994,8042,8043,8046],{"class":2996,"line":5402},[2994,8044,8045],{"class":3007},"    CONSTRAINT",[2994,8047,8048],{"class":3021}," audiobooks_author_fk \n",[2994,8050,8051,8054,8057,8060,8063,8066],{"class":2996,"line":5411},[2994,8052,8053],{"class":3007},"        FOREIGN KEY",[2994,8055,8056],{"class":3021}," (author_id) ",[2994,8058,8059],{"class":3007},"REFERENCES",[2994,8061,8062],{"class":3021}," authors(id) ",[2994,8064,8065],{"class":3007},"ON DELETE CASCADE",[2994,8067,3088],{"class":3021},[2994,8069,8070,8072],{"class":2996,"line":5429},[2994,8071,8045],{"class":3007},[2994,8073,8074],{"class":3021}," audiobooks_genre_fk \n",[2994,8076,8077,8079,8082,8084,8087],{"class":2996,"line":5446},[2994,8078,8053],{"class":3007},[2994,8080,8081],{"class":3021}," (genre_id) ",[2994,8083,8059],{"class":3007},[2994,8085,8086],{"class":3021}," genres(id) ",[2994,8088,8089],{"class":3007},"ON DELETE CASCADE\n",[2994,8091,8092],{"class":2996,"line":5464},[2994,8093,3055],{"class":3021},[2994,8095,8096],{"class":2996,"line":5473},[2994,8097,3062],{"emptyLinePlaceholder":3061},[2994,8099,8100,8102,8105,8108,8111],{"class":2996,"line":5478},[2994,8101,3008],{"class":3007},[2994,8103,8104],{"class":3007}," INDEX",[2994,8106,8107],{"class":3014}," audiobooks_author_id_idx",[2994,8109,8110],{"class":3007}," ON",[2994,8112,8113],{"class":3021}," audiobooks(author_id);\n",[2994,8115,8116,8118,8120,8123,8126],{"class":2996,"line":5484},[2994,8117,3008],{"class":3007},[2994,8119,8104],{"class":3007},[2994,8121,8122],{"class":3014}," audiobooks_genre_id_idx",[2994,8124,8125],{"class":3007},"  ON",[2994,8127,8128],{"class":3021}," audiobooks(genre_id);\n",[2994,8130,8131],{"class":2996,"line":5493},[2994,8132,3062],{"emptyLinePlaceholder":3061},[2994,8134,8135],{"class":2996,"line":5511},[2994,8136,8137],{"class":3000},"-- Таблиця користувачів\n",[2994,8139,8140,8142,8144,8147],{"class":2996,"line":5529},[2994,8141,3008],{"class":3007},[2994,8143,3070],{"class":3007},[2994,8145,8146],{"class":3014}," users",[2994,8148,3076],{"class":3021},[2994,8150,8151,8154,8156],{"class":2996,"line":5547},[2994,8152,8153],{"class":3021},"    id            UUID         ",[2994,8155,3085],{"class":3007},[2994,8157,3088],{"class":3021},[2994,8159,8160,8163,8165,8167,8169,8171,8173,8175],{"class":2996,"line":5564},[2994,8161,8162],{"class":3021},"    username      ",[2994,8164,3108],{"class":3007},[2994,8166,3111],{"class":3021},[2994,8168,7771],{"class":3114},[2994,8170,7774],{"class":3021},[2994,8172,3097],{"class":3007},[2994,8174,7869],{"class":3007},[2994,8176,7983],{"class":3021},[2994,8178,8179,8182,8184,8187,8189,8192,8195,8197],{"class":2996,"line":5573},[2994,8180,8181],{"class":3007},"                  CHECK",[2994,8183,3769],{"class":3021},[2994,8185,8186],{"class":3007},"LENGTH",[2994,8188,3111],{"class":3021},[2994,8190,8191],{"class":3014},"TRIM",[2994,8193,8194],{"class":3021},"(username)) > ",[2994,8196,7968],{"class":3114},[2994,8198,7971],{"class":3021},[2994,8200,8201,8204,8206,8208,8211,8213,8215],{"class":2996,"line":5939},[2994,8202,8203],{"class":3021},"    password_hash ",[2994,8205,3108],{"class":3007},[2994,8207,3111],{"class":3021},[2994,8209,8210],{"class":3114},"128",[2994,8212,3118],{"class":3021},[2994,8214,3097],{"class":3007},[2994,8216,3088],{"class":3021},[2994,8218,8219,8222,8224,8226,8229],{"class":2996,"line":5944},[2994,8220,8221],{"class":3021},"    email         ",[2994,8223,3108],{"class":3007},[2994,8225,3111],{"class":3021},[2994,8227,8228],{"class":3114},"376",[2994,8230,7971],{"class":3021},[2994,8232,8233,8236,8238,8240,8242],{"class":2996,"line":5950},[2994,8234,8235],{"class":3021},"    avatar_path   ",[2994,8237,3108],{"class":3007},[2994,8239,3111],{"class":3021},[2994,8241,3115],{"class":3114},[2994,8243,4084],{"class":3021},[2994,8245,8246],{"class":2996,"line":5956},[2994,8247,3055],{"class":3021},[2994,8249,8250],{"class":2996,"line":5962},[2994,8251,3062],{"emptyLinePlaceholder":3061},[2994,8253,8254,8256,8258,8261,8263],{"class":2996,"line":5968},[2994,8255,3008],{"class":3007},[2994,8257,8104],{"class":3007},[2994,8259,8260],{"class":3014}," users_email_idx",[2994,8262,8110],{"class":3007},[2994,8264,8265],{"class":3021}," users(email);\n",[2994,8267,8268],{"class":2996,"line":5978},[2994,8269,3062],{"emptyLinePlaceholder":3061},[2994,8271,8272],{"class":2996,"line":5992},[2994,8273,8274],{"class":3000},"-- Таблиця колекцій користувачів\n",[2994,8276,8277,8279,8281,8284],{"class":2996,"line":5997},[2994,8278,3008],{"class":3007},[2994,8280,3070],{"class":3007},[2994,8282,8283],{"class":3014}," collections",[2994,8285,3076],{"class":3021},[2994,8287,8288,8291,8293],{"class":2996,"line":6003},[2994,8289,8290],{"class":3021},"    id         UUID      ",[2994,8292,3085],{"class":3007},[2994,8294,3088],{"class":3021},[2994,8296,8297,8300,8302],{"class":2996,"line":6009},[2994,8298,8299],{"class":3021},"    user_id    UUID      ",[2994,8301,3097],{"class":3007},[2994,8303,3088],{"class":3021},[2994,8305,8306,8308,8311,8313,8315,8317,8319,8321,8323,8325,8327,8329,8331,8334,8337,8339],{"class":2996,"line":6015},[2994,8307,7855],{"class":3007},[2994,8309,8310],{"class":3007},"       VARCHAR",[2994,8312,3111],{"class":3021},[2994,8314,8210],{"class":3114},[2994,8316,3118],{"class":3021},[2994,8318,3097],{"class":3007},[2994,8320,7962],{"class":3007},[2994,8322,3769],{"class":3021},[2994,8324,8186],{"class":3007},[2994,8326,3111],{"class":3021},[2994,8328,8191],{"class":3014},[2994,8330,3111],{"class":3021},[2994,8332,8333],{"class":3007},"name",[2994,8335,8336],{"class":3021},")) > ",[2994,8338,7968],{"class":3114},[2994,8340,7971],{"class":3021},[2994,8342,8343,8346,8349,8352],{"class":2996,"line":6021},[2994,8344,8345],{"class":3021},"    created_at ",[2994,8347,8348],{"class":3007},"TIMESTAMP",[2994,8350,8351],{"class":3007}," DEFAULT",[2994,8353,8354],{"class":3021}," CURRENT_TIMESTAMP,\n",[2994,8356,8357],{"class":2996,"line":6027},[2994,8358,8040],{"class":3021},[2994,8360,8361,8363],{"class":2996,"line":6032},[2994,8362,8045],{"class":3007},[2994,8364,8365],{"class":3021}," collections_user_fk \n",[2994,8367,8368,8370,8373,8375,8378],{"class":2996,"line":6038},[2994,8369,8053],{"class":3007},[2994,8371,8372],{"class":3021}," (user_id) ",[2994,8374,8059],{"class":3007},[2994,8376,8377],{"class":3021}," users(id) ",[2994,8379,8089],{"class":3007},[2994,8381,8382],{"class":2996,"line":6044},[2994,8383,3055],{"class":3021},[2994,8385,8386],{"class":2996,"line":6049},[2994,8387,3062],{"emptyLinePlaceholder":3061},[2994,8389,8390],{"class":2996,"line":6055},[2994,8391,8392],{"class":3000},"-- Зв'язок багато-до-багатьох: колекції ↔ аудіокниги\n",[2994,8394,8395,8397,8399,8402],{"class":2996,"line":6061},[2994,8396,3008],{"class":3007},[2994,8398,3070],{"class":3007},[2994,8400,8401],{"class":3014}," audiobook_collection",[2994,8403,3076],{"class":3021},[2994,8405,8406,8409,8411],{"class":2996,"line":6067},[2994,8407,8408],{"class":3021},"    collection_id UUID ",[2994,8410,3097],{"class":3007},[2994,8412,3088],{"class":3021},[2994,8414,8415,8418,8420],{"class":2996,"line":6075},[2994,8416,8417],{"class":3021},"    audiobook_id  UUID ",[2994,8419,3097],{"class":3007},[2994,8421,3088],{"class":3021},[2994,8423,8424],{"class":2996,"line":6097},[2994,8425,8040],{"class":3021},[2994,8427,8428,8431],{"class":2996,"line":6122},[2994,8429,8430],{"class":3007},"    PRIMARY KEY",[2994,8432,8433],{"class":3021}," (collection_id, audiobook_id),\n",[2994,8435,8436],{"class":2996,"line":6136},[2994,8437,8040],{"class":3021},[2994,8439,8440,8442],{"class":2996,"line":6149},[2994,8441,8045],{"class":3007},[2994,8443,8444],{"class":3021}," ac_collection_fk \n",[2994,8446,8447,8449,8452,8454,8457,8459],{"class":2996,"line":6163},[2994,8448,8053],{"class":3007},[2994,8450,8451],{"class":3021}," (collection_id) ",[2994,8453,8059],{"class":3007},[2994,8455,8456],{"class":3021}," collections(id) ",[2994,8458,8065],{"class":3007},[2994,8460,3088],{"class":3021},[2994,8462,8463,8465],{"class":2996,"line":6180},[2994,8464,8045],{"class":3007},[2994,8466,8467],{"class":3021}," ac_audiobook_fk \n",[2994,8469,8470,8472,8475,8477,8480],{"class":2996,"line":6185},[2994,8471,8053],{"class":3007},[2994,8473,8474],{"class":3021}," (audiobook_id) ",[2994,8476,8059],{"class":3007},[2994,8478,8479],{"class":3021}," audiobooks(id) ",[2994,8481,8089],{"class":3007},[2994,8483,8484],{"class":2996,"line":6190},[2994,8485,3055],{"class":3021},[2994,8487,8488],{"class":2996,"line":6196},[2994,8489,3062],{"emptyLinePlaceholder":3061},[2994,8491,8492],{"class":2996,"line":6202},[2994,8493,8494],{"class":3000},"-- Таблиця файлів аудіокниг (використовує ENUM)\n",[2994,8496,8497,8499,8501,8503],{"class":2996,"line":6207},[2994,8498,3008],{"class":3007},[2994,8500,3070],{"class":3007},[2994,8502,3073],{"class":3014},[2994,8504,3076],{"class":3021},[2994,8506,8507,8510,8512],{"class":2996,"line":6222},[2994,8508,8509],{"class":3021},"    id           UUID             ",[2994,8511,3085],{"class":3007},[2994,8513,3088],{"class":3021},[2994,8515,8516,8519,8521],{"class":2996,"line":6227},[2994,8517,8518],{"class":3021},"    audiobook_id UUID             ",[2994,8520,3097],{"class":3007},[2994,8522,3088],{"class":3021},[2994,8524,8525,8527,8529,8531,8533,8536,8538,8540,8542,8544,8546,8548,8551,8553],{"class":2996,"line":6232},[2994,8526,3105],{"class":3021},[2994,8528,3108],{"class":3007},[2994,8530,3111],{"class":3021},[2994,8532,3115],{"class":3114},[2994,8534,8535],{"class":3021},")    ",[2994,8537,3097],{"class":3007},[2994,8539,7962],{"class":3007},[2994,8541,3769],{"class":3021},[2994,8543,8186],{"class":3007},[2994,8545,3111],{"class":3021},[2994,8547,8191],{"class":3014},[2994,8549,8550],{"class":3021},"(file_path)) > ",[2994,8552,7968],{"class":3114},[2994,8554,7971],{"class":3021},[2994,8556,8557,8559,8561,8563],{"class":2996,"line":6238},[2994,8558,3128],{"class":3021},[2994,8560,3097],{"class":3007},[2994,8562,3133],{"class":3021},[2994,8564,3136],{"class":3000},[2994,8566,8567,8569,8572,8575,8577,8580,8583,8586,8589,8592,8595,8597],{"class":2996,"line":6243},[2994,8568,3142],{"class":3007},[2994,8570,8571],{"class":3007},"         INTEGER",[2994,8573,8574],{"class":3007},"          CHECK",[2994,8576,3769],{"class":3021},[2994,8578,8579],{"class":3007},"size",[2994,8581,8582],{"class":3007}," IS",[2994,8584,8585],{"class":3007}," NULL",[2994,8587,8588],{"class":3007}," OR",[2994,8590,8591],{"class":3007}," size",[2994,8593,8594],{"class":3021}," > ",[2994,8596,7968],{"class":3114},[2994,8598,7971],{"class":3021},[2994,8600,8601],{"class":2996,"line":6249},[2994,8602,8040],{"class":3021},[2994,8604,8605,8607],{"class":2996,"line":6255},[2994,8606,8045],{"class":3007},[2994,8608,8609],{"class":3021}," af_audiobook_fk \n",[2994,8611,8612,8614,8616,8618,8620],{"class":2996,"line":6260},[2994,8613,8053],{"class":3007},[2994,8615,8474],{"class":3021},[2994,8617,8059],{"class":3007},[2994,8619,8479],{"class":3021},[2994,8621,8089],{"class":3007},[2994,8623,8624],{"class":2996,"line":6268},[2994,8625,3055],{"class":3021},[2994,8627,8628],{"class":2996,"line":6295},[2994,8629,3062],{"emptyLinePlaceholder":3061},[2994,8631,8632,8634,8636,8639,8641],{"class":2996,"line":6301},[2994,8633,3008],{"class":3007},[2994,8635,8104],{"class":3007},[2994,8637,8638],{"class":3014}," audiobook_files_audiobook_id_idx",[2994,8640,8110],{"class":3007},[2994,8642,8643],{"class":3021}," audiobook_files(audiobook_id);\n",[2994,8645,8646],{"class":2996,"line":6307},[2994,8647,3062],{"emptyLinePlaceholder":3061},[2994,8649,8650],{"class":2996,"line":6326},[2994,8651,8652],{"class":3000},"-- Таблиця прогресу прослуховування\n",[2994,8654,8655,8657,8659,8662],{"class":2996,"line":6345},[2994,8656,3008],{"class":3007},[2994,8658,3070],{"class":3007},[2994,8660,8661],{"class":3014}," listening_progresses",[2994,8663,3076],{"class":3021},[2994,8665,8666,8669,8671],{"class":2996,"line":6364},[2994,8667,8668],{"class":3021},"    id            UUID      ",[2994,8670,3085],{"class":3007},[2994,8672,3088],{"class":3021},[2994,8674,8675,8678,8680],{"class":2996,"line":6369},[2994,8676,8677],{"class":3021},"    user_id       UUID      ",[2994,8679,3097],{"class":3007},[2994,8681,3088],{"class":3021},[2994,8683,8684,8687,8689],{"class":2996,"line":6375},[2994,8685,8686],{"class":3021},"    audiobook_id  UUID      ",[2994,8688,3097],{"class":3007},[2994,8690,3088],{"class":3021},[2994,8692,8693,8696,8698,8701,8703,8706,8708],{"class":2996,"line":6388},[2994,8694,8695],{"class":3021},"    position      ",[2994,8697,7956],{"class":3007},[2994,8699,8700],{"class":3007},"   NOT NULL",[2994,8702,7962],{"class":3007},[2994,8704,8705],{"class":3021}," (position >= ",[2994,8707,7968],{"class":3114},[2994,8709,7971],{"class":3021},[2994,8711,8712,8715,8717,8719],{"class":2996,"line":6393},[2994,8713,8714],{"class":3021},"    last_listened ",[2994,8716,8348],{"class":3007},[2994,8718,8351],{"class":3007},[2994,8720,8354],{"class":3021},[2994,8722,8723],{"class":2996,"line":6399},[2994,8724,8040],{"class":3021},[2994,8726,8727,8729],{"class":2996,"line":6412},[2994,8728,8045],{"class":3007},[2994,8730,8731],{"class":3021}," lp_user_fk \n",[2994,8733,8734,8736,8738,8740,8742,8744],{"class":2996,"line":6417},[2994,8735,8053],{"class":3007},[2994,8737,8372],{"class":3021},[2994,8739,8059],{"class":3007},[2994,8741,8377],{"class":3021},[2994,8743,8065],{"class":3007},[2994,8745,3088],{"class":3021},[2994,8747,8748,8750],{"class":2996,"line":6422},[2994,8749,8045],{"class":3007},[2994,8751,8752],{"class":3021}," lp_audiobook_fk \n",[2994,8754,8755,8757,8759,8761,8763],{"class":2996,"line":6427},[2994,8756,8053],{"class":3007},[2994,8758,8474],{"class":3021},[2994,8760,8059],{"class":3007},[2994,8762,8479],{"class":3021},[2994,8764,8089],{"class":3007},[2994,8766,8767],{"class":2996,"line":6433},[2994,8768,3055],{"class":3021},[2994,8770,8771],{"class":2996,"line":6438},[2994,8772,3062],{"emptyLinePlaceholder":3061},[2994,8774,8775,8777,8779,8782,8785],{"class":2996,"line":6444},[2994,8776,3008],{"class":3007},[2994,8778,8104],{"class":3007},[2994,8780,8781],{"class":3014}," listening_progresses_user_id_idx",[2994,8783,8784],{"class":3007},"      ON",[2994,8786,8787],{"class":3021}," listening_progresses(user_id);\n",[2994,8789,8790,8792,8794,8797,8799],{"class":2996,"line":6450},[2994,8791,3008],{"class":3007},[2994,8793,8104],{"class":3007},[2994,8795,8796],{"class":3014}," listening_progresses_audiobook_id_idx",[2994,8798,8110],{"class":3007},[2994,8800,8801],{"class":3021}," listening_progresses(audiobook_id);\n",[2964,8803,8804],{},[2971,8805,8806],{},"Відмінності від H2 DDL:",[4203,8808,8809,8818,8830,8843],{},[3962,8810,8811,7609,8814,8817],{},[2971,8812,8813],{},"Рядок 5:",[2991,8815,8816],{},"CREATE TYPE ... AS ENUM"," — PostgreSQL-специфічний синтаксис. H2 не підтримує це.",[3962,8819,8820,7609,8823,8826,8827,3217],{},[2971,8821,8822],{},"Рядок 31:",[2991,8824,8825],{},"EXTRACT(YEAR FROM CURRENT_DATE)"," — PostgreSQL синтаксис. H2 використовує ",[2991,8828,8829],{},"YEAR(CURRENT_DATE)",[3962,8831,8832,7609,8835,8838,8839,3160,8841,3217],{},[2971,8833,8834],{},"Рядок 88:",[2991,8836,8837],{},"format file_format_enum"," — використання ENUM-типу. У H2 це був би ",[2991,8840,3108],{},[2991,8842,3163],{},[3962,8844,8845,7609,8848,8851],{},[2971,8846,8847],{},"Рядок 62:",[2991,8849,8850],{},"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"," — PostgreSQL автоматично встановлює поточний час.",[3307,8853],{},[2959,8855,8857],{"id":8856},"міграція-тестів-з-h2-на-postgresql","Міграція тестів з H2 на PostgreSQL",[2964,8859,8860,8861,8864],{},"Тепер адаптуємо тести з попередньої статті для роботи з Testcontainers. Більшість тестів залишаться ",[2971,8862,8863],{},"ідентичними"," — змінюється лише базовий клас.",[2976,8866,8868],{"id":8867},"тестування-базових-crud-операцій","Тестування базових CRUD-операцій",[2984,8870,8872],{"className":3173,"code":8871,"language":3175,"meta":5001,"style":2989},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.domain.Author;\nimport com.example.audiobook.repository.jdbc.JdbcAuthorRepository;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport static org.assertj.core.api.Assertions.*;\n\n/**\n * Інтеграційні тести {@link JdbcAuthorRepository} з реальною PostgreSQL.\n * \u003Cp>\n * \u003Cb>Відмінності від H2-версії:\u003C/b>\n * \u003Cul>\n *   \u003Cli>Базовий клас: {@link AbstractPostgresIntegrationTest} замість\n *       {@code AbstractRepositoryTest}\u003C/li>\n *   \u003Cli>БД: Реальна PostgreSQL у Docker-контейнері замість in-memory H2\u003C/li>\n *   \u003Cli>Швидкість: Повільніше (~50ms на тест замість ~5ms), але точніше\u003C/li>\n * \u003C/ul>\n * \u003Cp>\n * \u003Cb>Що тестується:\u003C/b>\n * \u003Cul>\n *   \u003Cli>CRUD-операції (save, findById, update, delete)\u003C/li>\n *   \u003Cli>Пошукові методи (findByLastName, findByFullName)\u003C/li>\n *   \u003Cli>Граничні випадки (null-значення, порожні результати)\u003C/li>\n *   \u003Cli>Constraints (PRIMARY KEY, NOT NULL)\u003C/li>\n * \u003C/ul>\n */\nclass JdbcAuthorRepositoryPostgresTest extends AbstractPostgresIntegrationTest {\n\n    private AuthorRepository repository;\n\n    @BeforeEach\n    void setUpRepository() {\n        // connectionManager вже ініціалізований у AbstractPostgresIntegrationTest\n        repository = new JdbcAuthorRepository(connectionManager);\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // Тести save() — ідентичні H2-версії\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void save_shouldInsertNewAuthor_whenValidData() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Іван\", \"Франко\");\n        author.setBio(\"Український письменник, поет, публіцист, політичний діяч\");\n        author.setImagePath(\"/images/franko.jpg\");\n\n        // ═══ Act ═══\n        repository.save(author);\n\n        // ═══ Assert ═══\n        assertThat(author.getId()).isNotNull();\n        \n        Author loaded = repository.findById(author.getId()).orElseThrow();\n        assertThat(loaded.getFirstName()).isEqualTo(\"Іван\");\n        assertThat(loaded.getLastName()).isEqualTo(\"Франко\");\n        assertThat(loaded.getBio()).isEqualTo(\"Український письменник, поет, публіцист, політичний діяч\");\n        assertThat(loaded.getImagePath()).isEqualTo(\"/images/franko.jpg\");\n        \n        assertThat(countRowsInTable(\"authors\")).isEqualTo(1);\n    }\n\n    @Test\n    void save_shouldHandleNullBio_whenBioNotProvided() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Леся\", \"Українка\");\n        // bio та imagePath залишаються null\n\n        // ═══ Act ═══\n        repository.save(author);\n\n        // ═══ Assert ═══\n        Author loaded = repository.findById(author.getId()).orElseThrow();\n        assertThat(loaded.getBio()).isNull();\n        assertThat(loaded.getImagePath()).isNull();\n    }\n\n    @Test\n    void save_shouldThrowException_whenDuplicateId() {\n        // ═══ Arrange ═══\n        UUID sharedId = UUID.randomUUID();\n        \n        Author author1 = new Author(\"Тарас\", \"Шевченко\");\n        author1.setId(sharedId);\n        \n        Author author2 = new Author(\"Іван\", \"Франко\");\n        author2.setId(sharedId);\n\n        repository.save(author1);\n\n        // ═══ Act & Assert ═══\n        assertThatThrownBy(() -> repository.save(author2))\n            .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n            .hasMessageContaining(\"duplicate key\"); // PostgreSQL повідомлення\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // Тести findById() — ідентичні H2-версії\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void findById_shouldReturnAuthor_whenExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Михайло\", \"Коцюбинський\");\n        repository.save(author);\n\n        // ═══ Act ═══\n        Optional\u003CAuthor> result = repository.findById(author.getId());\n\n        // ═══ Assert ═══\n        assertThat(result).isPresent();\n        assertThat(result.get().getFirstName()).isEqualTo(\"Михайло\");\n        assertThat(result.get().getLastName()).isEqualTo(\"Коцюбинський\");\n    }\n\n    @Test\n    void findById_shouldReturnEmpty_whenNotExists() {\n        // ═══ Arrange ═══\n        UUID nonExistentId = UUID.randomUUID();\n\n        // ═══ Act ═══\n        Optional\u003CAuthor> result = repository.findById(nonExistentId);\n\n        // ═══ Assert ═══\n        assertThat(result).isEmpty();\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // Тести update() та delete() — ідентичні H2-версії\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void update_shouldModifyAllFields_whenAuthorExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Іван\", \"Франко\");\n        author.setBio(\"Стара біографія\");\n        repository.save(author);\n\n        author.setFirstName(\"Іван Якович\");\n        author.setLastName(\"Франко-Захарченко\");\n        author.setBio(\"Нова біографія\");\n        author.setImagePath(\"/images/new.jpg\");\n\n        // ═══ Act ═══\n        repository.update(author);\n\n        // ═══ Assert ═══\n        Author loaded = repository.findById(author.getId()).orElseThrow();\n        assertThat(loaded.getFirstName()).isEqualTo(\"Іван Якович\");\n        assertThat(loaded.getLastName()).isEqualTo(\"Франко-Захарченко\");\n        assertThat(loaded.getBio()).isEqualTo(\"Нова біографія\");\n        assertThat(loaded.getImagePath()).isEqualTo(\"/images/new.jpg\");\n    }\n\n    @Test\n    void deleteById_shouldRemoveAuthor_whenExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Панас\", \"Мирний\");\n        repository.save(author);\n        UUID authorId = author.getId();\n\n        // ═══ Act ═══\n        boolean deleted = repository.deleteById(authorId);\n\n        // ═══ Assert ═══\n        assertThat(deleted).isTrue();\n        assertThat(repository.findById(authorId)).isEmpty();\n        assertThat(countRowsInTable(\"authors\")).isEqualTo(0);\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // Тести findByLastName() — ідентичні H2-версії\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void findByLastName_shouldReturnMatchingAuthors_whenPartialMatch() {\n        // ═══ Arrange ═══\n        repository.save(new Author(\"Тарас\", \"Шевченко\"));\n        repository.save(new Author(\"Іван\", \"Франко\"));\n        repository.save(new Author(\"Леся\", \"Українка\"));\n        repository.save(new Author(\"Григорій\", \"Сковорода\"));\n\n        // ═══ Act ═══\n        List\u003CAuthor> result = repository.findByLastName(\"ко\");\n\n        // ═══ Assert ═══\n        assertThat(result).hasSize(2);\n        assertThat(result)\n            .extracting(Author::getLastName)\n            .containsExactlyInAnyOrder(\"Франко\", \"Сковорода\");\n    }\n\n    @Test\n    void findByLastName_shouldBeCaseInsensitive_whenSearching() {\n        // ═══ Arrange ═══\n        repository.save(new Author(\"Тарас\", \"Шевченко\"));\n\n        // ═══ Act ═══\n        List\u003CAuthor> result1 = repository.findByLastName(\"шевч\");\n        List\u003CAuthor> result2 = repository.findByLastName(\"ШЕВЧ\");\n        List\u003CAuthor> result3 = repository.findByLastName(\"ШеВч\");\n\n        // ═══ Assert ═══\n        assertThat(result1).hasSize(1);\n        assertThat(result2).hasSize(1);\n        assertThat(result3).hasSize(1);\n    }\n}\n",[2991,8873,8874,8880,8884,8891,8898,8904,8911,8915,8922,8929,8936,8940,8949,8953,8957,8962,8966,8971,8975,8980,8985,8990,8995,8999,9003,9008,9012,9017,9022,9027,9032,9036,9040,9054,9058,9070,9074,9080,9089,9094,9107,9111,9115,9119,9124,9128,9132,9138,9147,9152,9179,9196,9212,9216,9221,9234,9238,9243,9266,9270,9302,9327,9350,9373,9396,9400,9425,9429,9433,9439,9448,9452,9476,9481,9485,9489,9499,9503,9507,9535,9554,9572,9576,9580,9586,9595,9599,9619,9623,9648,9661,9665,9688,9699,9703,9714,9718,9723,9742,9780,9797,9801,9805,9809,9814,9818,9822,9828,9837,9841,9865,9875,9879,9883,9917,9921,9925,9937,9964,9990,9994,9998,10004,10013,10017,10034,10038,10042,10065,10069,10073,10083,10087,10091,10095,10100,10104,10108,10114,10123,10127,10149,10164,10174,10178,10194,10210,10225,10240,10244,10248,10259,10263,10267,10295,10317,10339,10361,10383,10387,10391,10397,10406,10410,10434,10444,10461,10465,10469,10489,10493,10497,10509,10528,10550,10554,10558,10562,10567,10571,10575,10581,10590,10594,10618,10642,10666,10692,10696,10700,10729,10733,10737,10753,10760,10776,10793,10797,10801,10807,10816,10820,10844,10848,10852,10880,10908,10937,10942,10947,10963,10979,10995,11000],{"__ignoreMap":2989},[2994,8875,8876,8878],{"class":2996,"line":2997},[2994,8877,5671],{"class":3007},[2994,8879,5674],{"class":3021},[2994,8881,8882],{"class":2996,"line":3004},[2994,8883,3062],{"emptyLinePlaceholder":3061},[2994,8885,8886,8888],{"class":2996,"line":3058},[2994,8887,5683],{"class":3007},[2994,8889,8890],{"class":3021}," com.example.audiobook.domain.Author;\n",[2994,8892,8893,8895],{"class":2996,"line":3065},[2994,8894,5683],{"class":3007},[2994,8896,8897],{"class":3021}," com.example.audiobook.repository.jdbc.JdbcAuthorRepository;\n",[2994,8899,8900,8902],{"class":2996,"line":3079},[2994,8901,5683],{"class":3007},[2994,8903,5707],{"class":3021},[2994,8905,8906,8908],{"class":2996,"line":3091},[2994,8907,5683],{"class":3007},[2994,8909,8910],{"class":3021}," org.junit.jupiter.api.Test;\n",[2994,8912,8913],{"class":2996,"line":3102},[2994,8914,3062],{"emptyLinePlaceholder":3061},[2994,8916,8917,8919],{"class":2996,"line":3125},[2994,8918,5683],{"class":3007},[2994,8920,8921],{"class":3021}," java.util.List;\n",[2994,8923,8924,8926],{"class":2996,"line":3139},[2994,8925,5683],{"class":3007},[2994,8927,8928],{"class":3021}," java.util.Optional;\n",[2994,8930,8931,8933],{"class":2996,"line":3148},[2994,8932,5683],{"class":3007},[2994,8934,8935],{"class":3021}," java.util.UUID;\n",[2994,8937,8938],{"class":2996,"line":3845},[2994,8939,3062],{"emptyLinePlaceholder":3061},[2994,8941,8942,8944,8946],{"class":2996,"line":3851},[2994,8943,5683],{"class":3007},[2994,8945,4269],{"class":3007},[2994,8947,8948],{"class":3021}," org.assertj.core.api.Assertions.*;\n",[2994,8950,8951],{"class":2996,"line":3856},[2994,8952,3062],{"emptyLinePlaceholder":3061},[2994,8954,8955],{"class":2996,"line":3861},[2994,8956,5804],{"class":3000},[2994,8958,8959],{"class":2996,"line":3867},[2994,8960,8961],{"class":3000}," * Інтеграційні тести {@link JdbcAuthorRepository} з реальною PostgreSQL.\n",[2994,8963,8964],{"class":2996,"line":3873},[2994,8965,5814],{"class":3000},[2994,8967,8968],{"class":2996,"line":3879},[2994,8969,8970],{"class":3000}," * \u003Cb>Відмінності від H2-версії:\u003C/b>\n",[2994,8972,8973],{"class":2996,"line":3885},[2994,8974,5824],{"class":3000},[2994,8976,8977],{"class":2996,"line":3891},[2994,8978,8979],{"class":3000}," *   \u003Cli>Базовий клас: {@link AbstractPostgresIntegrationTest} замість\n",[2994,8981,8982],{"class":2996,"line":3897},[2994,8983,8984],{"class":3000}," *       {@code AbstractRepositoryTest}\u003C/li>\n",[2994,8986,8987],{"class":2996,"line":3903},[2994,8988,8989],{"class":3000}," *   \u003Cli>БД: Реальна PostgreSQL у Docker-контейнері замість in-memory H2\u003C/li>\n",[2994,8991,8992],{"class":2996,"line":3908},[2994,8993,8994],{"class":3000}," *   \u003Cli>Швидкість: Повільніше (~50ms на тест замість ~5ms), але точніше\u003C/li>\n",[2994,8996,8997],{"class":2996,"line":3914},[2994,8998,5864],{"class":3000},[2994,9000,9001],{"class":2996,"line":3920},[2994,9002,5814],{"class":3000},[2994,9004,9005],{"class":2996,"line":3926},[2994,9006,9007],{"class":3000}," * \u003Cb>Що тестується:\u003C/b>\n",[2994,9009,9010],{"class":2996,"line":3931},[2994,9011,5824],{"class":3000},[2994,9013,9014],{"class":2996,"line":3937},[2994,9015,9016],{"class":3000}," *   \u003Cli>CRUD-операції (save, findById, update, delete)\u003C/li>\n",[2994,9018,9019],{"class":2996,"line":3943},[2994,9020,9021],{"class":3000}," *   \u003Cli>Пошукові методи (findByLastName, findByFullName)\u003C/li>\n",[2994,9023,9024],{"class":2996,"line":3949},[2994,9025,9026],{"class":3000}," *   \u003Cli>Граничні випадки (null-значення, порожні результати)\u003C/li>\n",[2994,9028,9029],{"class":2996,"line":5348},[2994,9030,9031],{"class":3000}," *   \u003Cli>Constraints (PRIMARY KEY, NOT NULL)\u003C/li>\n",[2994,9033,9034],{"class":2996,"line":5365},[2994,9035,5864],{"class":3000},[2994,9037,9038],{"class":2996,"line":5382},[2994,9039,5965],{"class":3000},[2994,9041,9042,9044,9047,9050,9052],{"class":2996,"line":5391},[2994,9043,4028],{"class":3007},[2994,9045,9046],{"class":3187}," JdbcAuthorRepositoryPostgresTest",[2994,9048,9049],{"class":3007}," extends",[2994,9051,5987],{"class":3187},[2994,9053,4034],{"class":3021},[2994,9055,9056],{"class":2996,"line":5396},[2994,9057,3062],{"emptyLinePlaceholder":3061},[2994,9059,9060,9062,9065,9068],{"class":2996,"line":5402},[2994,9061,4266],{"class":3007},[2994,9063,9064],{"class":3187}," AuthorRepository",[2994,9066,9067],{"class":3191}," repository",[2994,9069,3209],{"class":3021},[2994,9071,9072],{"class":2996,"line":5411},[2994,9073,3062],{"emptyLinePlaceholder":3061},[2994,9075,9076,9078],{"class":2996,"line":5429},[2994,9077,4043],{"class":3021},[2994,9079,6490],{"class":3187},[2994,9081,9082,9084,9087],{"class":2996,"line":5446},[2994,9083,4143],{"class":3187},[2994,9085,9086],{"class":3014}," setUpRepository",[2994,9088,4407],{"class":3021},[2994,9090,9091],{"class":2996,"line":5464},[2994,9092,9093],{"class":3000},"        // connectionManager вже ініціалізований у AbstractPostgresIntegrationTest\n",[2994,9095,9096,9099,9101,9104],{"class":2996,"line":5473},[2994,9097,9098],{"class":3021},"        repository = ",[2994,9100,4073],{"class":3700},[2994,9102,9103],{"class":3014}," JdbcAuthorRepository",[2994,9105,9106],{"class":3021},"(connectionManager);\n",[2994,9108,9109],{"class":2996,"line":5478},[2994,9110,4387],{"class":3021},[2994,9112,9113],{"class":2996,"line":5484},[2994,9114,3062],{"emptyLinePlaceholder":3061},[2994,9116,9117],{"class":2996,"line":5493},[2994,9118,6787],{"class":3000},[2994,9120,9121],{"class":2996,"line":5511},[2994,9122,9123],{"class":3000},"    // Тести save() — ідентичні H2-версії\n",[2994,9125,9126],{"class":2996,"line":5529},[2994,9127,6787],{"class":3000},[2994,9129,9130],{"class":2996,"line":5547},[2994,9131,3062],{"emptyLinePlaceholder":3061},[2994,9133,9134,9136],{"class":2996,"line":5564},[2994,9135,4043],{"class":3021},[2994,9137,4138],{"class":3187},[2994,9139,9140,9142,9145],{"class":2996,"line":5573},[2994,9141,4143],{"class":3187},[2994,9143,9144],{"class":3014}," save_shouldInsertNewAuthor_whenValidData",[2994,9146,4407],{"class":3021},[2994,9148,9149],{"class":2996,"line":5939},[2994,9150,9151],{"class":3000},"        // ═══ Arrange ═══\n",[2994,9153,9154,9157,9160,9162,9164,9167,9169,9172,9174,9177],{"class":2996,"line":5944},[2994,9155,9156],{"class":3187},"        Author",[2994,9158,9159],{"class":3191}," author",[2994,9161,3195],{"class":3021},[2994,9163,4073],{"class":3700},[2994,9165,9166],{"class":3014}," Author",[2994,9168,3111],{"class":3021},[2994,9170,9171],{"class":3025},"\"Іван\"",[2994,9173,3029],{"class":3021},[2994,9175,9176],{"class":3025},"\"Франко\"",[2994,9178,3055],{"class":3021},[2994,9180,9181,9184,9186,9189,9191,9194],{"class":2996,"line":5950},[2994,9182,9183],{"class":3191},"        author",[2994,9185,3217],{"class":3021},[2994,9187,9188],{"class":3014},"setBio",[2994,9190,3111],{"class":3021},[2994,9192,9193],{"class":3025},"\"Український письменник, поет, публіцист, політичний діяч\"",[2994,9195,3055],{"class":3021},[2994,9197,9198,9200,9202,9205,9207,9210],{"class":2996,"line":5956},[2994,9199,9183],{"class":3191},[2994,9201,3217],{"class":3021},[2994,9203,9204],{"class":3014},"setImagePath",[2994,9206,3111],{"class":3021},[2994,9208,9209],{"class":3025},"\"/images/franko.jpg\"",[2994,9211,3055],{"class":3021},[2994,9213,9214],{"class":2996,"line":5962},[2994,9215,3062],{"emptyLinePlaceholder":3061},[2994,9217,9218],{"class":2996,"line":5968},[2994,9219,9220],{"class":3000},"        // ═══ Act ═══\n",[2994,9222,9223,9226,9228,9231],{"class":2996,"line":5978},[2994,9224,9225],{"class":3191},"        repository",[2994,9227,3217],{"class":3021},[2994,9229,9230],{"class":3014},"save",[2994,9232,9233],{"class":3021},"(author);\n",[2994,9235,9236],{"class":2996,"line":5992},[2994,9237,3062],{"emptyLinePlaceholder":3061},[2994,9239,9240],{"class":2996,"line":5997},[2994,9241,9242],{"class":3000},"        // ═══ Assert ═══\n",[2994,9244,9245,9248,9250,9253,9255,9258,9261,9264],{"class":2996,"line":6003},[2994,9246,9247],{"class":3014},"        assertThat",[2994,9249,3111],{"class":3021},[2994,9251,9252],{"class":3191},"author",[2994,9254,3217],{"class":3021},[2994,9256,9257],{"class":3014},"getId",[2994,9259,9260],{"class":3021},"()).",[2994,9262,9263],{"class":3014},"isNotNull",[2994,9265,4382],{"class":3021},[2994,9267,9268],{"class":2996,"line":6009},[2994,9269,6888],{"class":3021},[2994,9271,9272,9274,9277,9279,9282,9284,9287,9289,9291,9293,9295,9297,9300],{"class":2996,"line":6015},[2994,9273,9156],{"class":3187},[2994,9275,9276],{"class":3191}," loaded",[2994,9278,3195],{"class":3021},[2994,9280,9281],{"class":3191},"repository",[2994,9283,3217],{"class":3021},[2994,9285,9286],{"class":3014},"findById",[2994,9288,3111],{"class":3021},[2994,9290,9252],{"class":3191},[2994,9292,3217],{"class":3021},[2994,9294,9257],{"class":3014},[2994,9296,9260],{"class":3021},[2994,9298,9299],{"class":3014},"orElseThrow",[2994,9301,4382],{"class":3021},[2994,9303,9304,9306,9308,9311,9313,9316,9318,9321,9323,9325],{"class":2996,"line":6021},[2994,9305,9247],{"class":3014},[2994,9307,3111],{"class":3021},[2994,9309,9310],{"class":3191},"loaded",[2994,9312,3217],{"class":3021},[2994,9314,9315],{"class":3014},"getFirstName",[2994,9317,9260],{"class":3021},[2994,9319,9320],{"class":3014},"isEqualTo",[2994,9322,3111],{"class":3021},[2994,9324,9171],{"class":3025},[2994,9326,3055],{"class":3021},[2994,9328,9329,9331,9333,9335,9337,9340,9342,9344,9346,9348],{"class":2996,"line":6027},[2994,9330,9247],{"class":3014},[2994,9332,3111],{"class":3021},[2994,9334,9310],{"class":3191},[2994,9336,3217],{"class":3021},[2994,9338,9339],{"class":3014},"getLastName",[2994,9341,9260],{"class":3021},[2994,9343,9320],{"class":3014},[2994,9345,3111],{"class":3021},[2994,9347,9176],{"class":3025},[2994,9349,3055],{"class":3021},[2994,9351,9352,9354,9356,9358,9360,9363,9365,9367,9369,9371],{"class":2996,"line":6032},[2994,9353,9247],{"class":3014},[2994,9355,3111],{"class":3021},[2994,9357,9310],{"class":3191},[2994,9359,3217],{"class":3021},[2994,9361,9362],{"class":3014},"getBio",[2994,9364,9260],{"class":3021},[2994,9366,9320],{"class":3014},[2994,9368,3111],{"class":3021},[2994,9370,9193],{"class":3025},[2994,9372,3055],{"class":3021},[2994,9374,9375,9377,9379,9381,9383,9386,9388,9390,9392,9394],{"class":2996,"line":6038},[2994,9376,9247],{"class":3014},[2994,9378,3111],{"class":3021},[2994,9380,9310],{"class":3191},[2994,9382,3217],{"class":3021},[2994,9384,9385],{"class":3014},"getImagePath",[2994,9387,9260],{"class":3021},[2994,9389,9320],{"class":3014},[2994,9391,3111],{"class":3021},[2994,9393,9209],{"class":3025},[2994,9395,3055],{"class":3021},[2994,9397,9398],{"class":2996,"line":6044},[2994,9399,6888],{"class":3021},[2994,9401,9402,9404,9406,9409,9411,9414,9417,9419,9421,9423],{"class":2996,"line":6049},[2994,9403,9247],{"class":3014},[2994,9405,3111],{"class":3021},[2994,9407,9408],{"class":3014},"countRowsInTable",[2994,9410,3111],{"class":3021},[2994,9412,9413],{"class":3025},"\"authors\"",[2994,9415,9416],{"class":3021},")).",[2994,9418,9320],{"class":3014},[2994,9420,3111],{"class":3021},[2994,9422,7409],{"class":3114},[2994,9424,3055],{"class":3021},[2994,9426,9427],{"class":2996,"line":6055},[2994,9428,4387],{"class":3021},[2994,9430,9431],{"class":2996,"line":6061},[2994,9432,3062],{"emptyLinePlaceholder":3061},[2994,9434,9435,9437],{"class":2996,"line":6067},[2994,9436,4043],{"class":3021},[2994,9438,4138],{"class":3187},[2994,9440,9441,9443,9446],{"class":2996,"line":6075},[2994,9442,4143],{"class":3187},[2994,9444,9445],{"class":3014}," save_shouldHandleNullBio_whenBioNotProvided",[2994,9447,4407],{"class":3021},[2994,9449,9450],{"class":2996,"line":6097},[2994,9451,9151],{"class":3000},[2994,9453,9454,9456,9458,9460,9462,9464,9466,9469,9471,9474],{"class":2996,"line":6122},[2994,9455,9156],{"class":3187},[2994,9457,9159],{"class":3191},[2994,9459,3195],{"class":3021},[2994,9461,4073],{"class":3700},[2994,9463,9166],{"class":3014},[2994,9465,3111],{"class":3021},[2994,9467,9468],{"class":3025},"\"Леся\"",[2994,9470,3029],{"class":3021},[2994,9472,9473],{"class":3025},"\"Українка\"",[2994,9475,3055],{"class":3021},[2994,9477,9478],{"class":2996,"line":6136},[2994,9479,9480],{"class":3000},"        // bio та imagePath залишаються null\n",[2994,9482,9483],{"class":2996,"line":6149},[2994,9484,3062],{"emptyLinePlaceholder":3061},[2994,9486,9487],{"class":2996,"line":6163},[2994,9488,9220],{"class":3000},[2994,9490,9491,9493,9495,9497],{"class":2996,"line":6180},[2994,9492,9225],{"class":3191},[2994,9494,3217],{"class":3021},[2994,9496,9230],{"class":3014},[2994,9498,9233],{"class":3021},[2994,9500,9501],{"class":2996,"line":6185},[2994,9502,3062],{"emptyLinePlaceholder":3061},[2994,9504,9505],{"class":2996,"line":6190},[2994,9506,9242],{"class":3000},[2994,9508,9509,9511,9513,9515,9517,9519,9521,9523,9525,9527,9529,9531,9533],{"class":2996,"line":6196},[2994,9510,9156],{"class":3187},[2994,9512,9276],{"class":3191},[2994,9514,3195],{"class":3021},[2994,9516,9281],{"class":3191},[2994,9518,3217],{"class":3021},[2994,9520,9286],{"class":3014},[2994,9522,3111],{"class":3021},[2994,9524,9252],{"class":3191},[2994,9526,3217],{"class":3021},[2994,9528,9257],{"class":3014},[2994,9530,9260],{"class":3021},[2994,9532,9299],{"class":3014},[2994,9534,4382],{"class":3021},[2994,9536,9537,9539,9541,9543,9545,9547,9549,9552],{"class":2996,"line":6202},[2994,9538,9247],{"class":3014},[2994,9540,3111],{"class":3021},[2994,9542,9310],{"class":3191},[2994,9544,3217],{"class":3021},[2994,9546,9362],{"class":3014},[2994,9548,9260],{"class":3021},[2994,9550,9551],{"class":3014},"isNull",[2994,9553,4382],{"class":3021},[2994,9555,9556,9558,9560,9562,9564,9566,9568,9570],{"class":2996,"line":6207},[2994,9557,9247],{"class":3014},[2994,9559,3111],{"class":3021},[2994,9561,9310],{"class":3191},[2994,9563,3217],{"class":3021},[2994,9565,9385],{"class":3014},[2994,9567,9260],{"class":3021},[2994,9569,9551],{"class":3014},[2994,9571,4382],{"class":3021},[2994,9573,9574],{"class":2996,"line":6222},[2994,9575,4387],{"class":3021},[2994,9577,9578],{"class":2996,"line":6227},[2994,9579,3062],{"emptyLinePlaceholder":3061},[2994,9581,9582,9584],{"class":2996,"line":6232},[2994,9583,4043],{"class":3021},[2994,9585,4138],{"class":3187},[2994,9587,9588,9590,9593],{"class":2996,"line":6238},[2994,9589,4143],{"class":3187},[2994,9591,9592],{"class":3014}," save_shouldThrowException_whenDuplicateId",[2994,9594,4407],{"class":3021},[2994,9596,9597],{"class":2996,"line":6243},[2994,9598,9151],{"class":3000},[2994,9600,9601,9604,9607,9609,9612,9614,9617],{"class":2996,"line":6249},[2994,9602,9603],{"class":3187},"        UUID",[2994,9605,9606],{"class":3191}," sharedId",[2994,9608,3195],{"class":3021},[2994,9610,9611],{"class":3191},"UUID",[2994,9613,3217],{"class":3021},[2994,9615,9616],{"class":3014},"randomUUID",[2994,9618,4382],{"class":3021},[2994,9620,9621],{"class":2996,"line":6255},[2994,9622,6888],{"class":3021},[2994,9624,9625,9627,9630,9632,9634,9636,9638,9641,9643,9646],{"class":2996,"line":6260},[2994,9626,9156],{"class":3187},[2994,9628,9629],{"class":3191}," author1",[2994,9631,3195],{"class":3021},[2994,9633,4073],{"class":3700},[2994,9635,9166],{"class":3014},[2994,9637,3111],{"class":3021},[2994,9639,9640],{"class":3025},"\"Тарас\"",[2994,9642,3029],{"class":3021},[2994,9644,9645],{"class":3025},"\"Шевченко\"",[2994,9647,3055],{"class":3021},[2994,9649,9650,9653,9655,9658],{"class":2996,"line":6268},[2994,9651,9652],{"class":3191},"        author1",[2994,9654,3217],{"class":3021},[2994,9656,9657],{"class":3014},"setId",[2994,9659,9660],{"class":3021},"(sharedId);\n",[2994,9662,9663],{"class":2996,"line":6295},[2994,9664,6888],{"class":3021},[2994,9666,9667,9669,9672,9674,9676,9678,9680,9682,9684,9686],{"class":2996,"line":6301},[2994,9668,9156],{"class":3187},[2994,9670,9671],{"class":3191}," author2",[2994,9673,3195],{"class":3021},[2994,9675,4073],{"class":3700},[2994,9677,9166],{"class":3014},[2994,9679,3111],{"class":3021},[2994,9681,9171],{"class":3025},[2994,9683,3029],{"class":3021},[2994,9685,9176],{"class":3025},[2994,9687,3055],{"class":3021},[2994,9689,9690,9693,9695,9697],{"class":2996,"line":6307},[2994,9691,9692],{"class":3191},"        author2",[2994,9694,3217],{"class":3021},[2994,9696,9657],{"class":3014},[2994,9698,9660],{"class":3021},[2994,9700,9701],{"class":2996,"line":6326},[2994,9702,3062],{"emptyLinePlaceholder":3061},[2994,9704,9705,9707,9709,9711],{"class":2996,"line":6345},[2994,9706,9225],{"class":3191},[2994,9708,3217],{"class":3021},[2994,9710,9230],{"class":3014},[2994,9712,9713],{"class":3021},"(author1);\n",[2994,9715,9716],{"class":2996,"line":6364},[2994,9717,3062],{"emptyLinePlaceholder":3061},[2994,9719,9720],{"class":2996,"line":6369},[2994,9721,9722],{"class":3000},"        // ═══ Act & Assert ═══\n",[2994,9724,9725,9728,9731,9733,9735,9737,9739],{"class":2996,"line":6375},[2994,9726,9727],{"class":3014},"        assertThatThrownBy",[2994,9729,9730],{"class":3021},"(() ",[2994,9732,3385],{"class":3007},[2994,9734,9067],{"class":3191},[2994,9736,3217],{"class":3021},[2994,9738,9230],{"class":3014},[2994,9740,9741],{"class":3021},"(author2))\n",[2994,9743,9744,9746,9749,9751,9754,9756,9759,9761,9764,9766,9769,9771,9774,9776,9778],{"class":2996,"line":6388},[2994,9745,4315],{"class":3021},[2994,9747,9748],{"class":3014},"isInstanceOf",[2994,9750,3111],{"class":3021},[2994,9752,9753],{"class":3191},"com",[2994,9755,3217],{"class":3021},[2994,9757,9758],{"class":3191},"example",[2994,9760,3217],{"class":3021},[2994,9762,9763],{"class":3191},"audiobook",[2994,9765,3217],{"class":3021},[2994,9767,9768],{"class":3191},"db",[2994,9770,3217],{"class":3021},[2994,9772,9773],{"class":3191},"DatabaseException",[2994,9775,3217],{"class":3021},[2994,9777,4028],{"class":3191},[2994,9779,4084],{"class":3021},[2994,9781,9782,9784,9787,9789,9792,9794],{"class":2996,"line":6393},[2994,9783,4315],{"class":3021},[2994,9785,9786],{"class":3014},"hasMessageContaining",[2994,9788,3111],{"class":3021},[2994,9790,9791],{"class":3025},"\"duplicate key\"",[2994,9793,3233],{"class":3021},[2994,9795,9796],{"class":3000},"// PostgreSQL повідомлення\n",[2994,9798,9799],{"class":2996,"line":6399},[2994,9800,4387],{"class":3021},[2994,9802,9803],{"class":2996,"line":6412},[2994,9804,3062],{"emptyLinePlaceholder":3061},[2994,9806,9807],{"class":2996,"line":6417},[2994,9808,6787],{"class":3000},[2994,9810,9811],{"class":2996,"line":6422},[2994,9812,9813],{"class":3000},"    // Тести findById() — ідентичні H2-версії\n",[2994,9815,9816],{"class":2996,"line":6427},[2994,9817,6787],{"class":3000},[2994,9819,9820],{"class":2996,"line":6433},[2994,9821,3062],{"emptyLinePlaceholder":3061},[2994,9823,9824,9826],{"class":2996,"line":6438},[2994,9825,4043],{"class":3021},[2994,9827,4138],{"class":3187},[2994,9829,9830,9832,9835],{"class":2996,"line":6444},[2994,9831,4143],{"class":3187},[2994,9833,9834],{"class":3014}," findById_shouldReturnAuthor_whenExists",[2994,9836,4407],{"class":3021},[2994,9838,9839],{"class":2996,"line":6450},[2994,9840,9151],{"class":3000},[2994,9842,9843,9845,9847,9849,9851,9853,9855,9858,9860,9863],{"class":2996,"line":6456},[2994,9844,9156],{"class":3187},[2994,9846,9159],{"class":3191},[2994,9848,3195],{"class":3021},[2994,9850,4073],{"class":3700},[2994,9852,9166],{"class":3014},[2994,9854,3111],{"class":3021},[2994,9856,9857],{"class":3025},"\"Михайло\"",[2994,9859,3029],{"class":3021},[2994,9861,9862],{"class":3025},"\"Коцюбинський\"",[2994,9864,3055],{"class":3021},[2994,9866,9867,9869,9871,9873],{"class":2996,"line":6462},[2994,9868,9225],{"class":3191},[2994,9870,3217],{"class":3021},[2994,9872,9230],{"class":3014},[2994,9874,9233],{"class":3021},[2994,9876,9877],{"class":2996,"line":6468},[2994,9878,3062],{"emptyLinePlaceholder":3061},[2994,9880,9881],{"class":2996,"line":6474},[2994,9882,9220],{"class":3000},[2994,9884,9885,9888,9890,9893,9895,9898,9900,9902,9904,9906,9908,9910,9912,9914],{"class":2996,"line":6480},[2994,9886,9887],{"class":3187},"        Optional",[2994,9889,4060],{"class":3021},[2994,9891,9892],{"class":3187},"Author",[2994,9894,4065],{"class":3021},[2994,9896,9897],{"class":3191},"result",[2994,9899,3195],{"class":3021},[2994,9901,9281],{"class":3191},[2994,9903,3217],{"class":3021},[2994,9905,9286],{"class":3014},[2994,9907,3111],{"class":3021},[2994,9909,9252],{"class":3191},[2994,9911,3217],{"class":3021},[2994,9913,9257],{"class":3014},[2994,9915,9916],{"class":3021},"());\n",[2994,9918,9919],{"class":2996,"line":6485},[2994,9920,3062],{"emptyLinePlaceholder":3061},[2994,9922,9923],{"class":2996,"line":6493},[2994,9924,9242],{"class":3000},[2994,9926,9927,9929,9932,9935],{"class":2996,"line":6510},[2994,9928,9247],{"class":3014},[2994,9930,9931],{"class":3021},"(result).",[2994,9933,9934],{"class":3014},"isPresent",[2994,9936,4382],{"class":3021},[2994,9938,9939,9941,9943,9945,9947,9950,9952,9954,9956,9958,9960,9962],{"class":2996,"line":6536},[2994,9940,9247],{"class":3014},[2994,9942,3111],{"class":3021},[2994,9944,9897],{"class":3191},[2994,9946,3217],{"class":3021},[2994,9948,9949],{"class":3014},"get",[2994,9951,7221],{"class":3021},[2994,9953,9315],{"class":3014},[2994,9955,9260],{"class":3021},[2994,9957,9320],{"class":3014},[2994,9959,3111],{"class":3021},[2994,9961,9857],{"class":3025},[2994,9963,3055],{"class":3021},[2994,9965,9966,9968,9970,9972,9974,9976,9978,9980,9982,9984,9986,9988],{"class":2996,"line":6558},[2994,9967,9247],{"class":3014},[2994,9969,3111],{"class":3021},[2994,9971,9897],{"class":3191},[2994,9973,3217],{"class":3021},[2994,9975,9949],{"class":3014},[2994,9977,7221],{"class":3021},[2994,9979,9339],{"class":3014},[2994,9981,9260],{"class":3021},[2994,9983,9320],{"class":3014},[2994,9985,3111],{"class":3021},[2994,9987,9862],{"class":3025},[2994,9989,3055],{"class":3021},[2994,9991,9992],{"class":2996,"line":6564},[2994,9993,4387],{"class":3021},[2994,9995,9996],{"class":2996,"line":6570},[2994,9997,3062],{"emptyLinePlaceholder":3061},[2994,9999,10000,10002],{"class":2996,"line":6576},[2994,10001,4043],{"class":3021},[2994,10003,4138],{"class":3187},[2994,10005,10006,10008,10011],{"class":2996,"line":6594},[2994,10007,4143],{"class":3187},[2994,10009,10010],{"class":3014}," findById_shouldReturnEmpty_whenNotExists",[2994,10012,4407],{"class":3021},[2994,10014,10015],{"class":2996,"line":6610},[2994,10016,9151],{"class":3000},[2994,10018,10019,10021,10024,10026,10028,10030,10032],{"class":2996,"line":6626},[2994,10020,9603],{"class":3187},[2994,10022,10023],{"class":3191}," nonExistentId",[2994,10025,3195],{"class":3021},[2994,10027,9611],{"class":3191},[2994,10029,3217],{"class":3021},[2994,10031,9616],{"class":3014},[2994,10033,4382],{"class":3021},[2994,10035,10036],{"class":2996,"line":6642},[2994,10037,3062],{"emptyLinePlaceholder":3061},[2994,10039,10040],{"class":2996,"line":6658},[2994,10041,9220],{"class":3000},[2994,10043,10044,10046,10048,10050,10052,10054,10056,10058,10060,10062],{"class":2996,"line":6674},[2994,10045,9887],{"class":3187},[2994,10047,4060],{"class":3021},[2994,10049,9892],{"class":3187},[2994,10051,4065],{"class":3021},[2994,10053,9897],{"class":3191},[2994,10055,3195],{"class":3021},[2994,10057,9281],{"class":3191},[2994,10059,3217],{"class":3021},[2994,10061,9286],{"class":3014},[2994,10063,10064],{"class":3021},"(nonExistentId);\n",[2994,10066,10067],{"class":2996,"line":6690},[2994,10068,3062],{"emptyLinePlaceholder":3061},[2994,10070,10071],{"class":2996,"line":6706},[2994,10072,9242],{"class":3000},[2994,10074,10075,10077,10079,10081],{"class":2996,"line":6712},[2994,10076,9247],{"class":3014},[2994,10078,9931],{"class":3021},[2994,10080,7022],{"class":3014},[2994,10082,4382],{"class":3021},[2994,10084,10085],{"class":2996,"line":6717},[2994,10086,4387],{"class":3021},[2994,10088,10089],{"class":2996,"line":6722},[2994,10090,3062],{"emptyLinePlaceholder":3061},[2994,10092,10093],{"class":2996,"line":6727},[2994,10094,6787],{"class":3000},[2994,10096,10097],{"class":2996,"line":6733},[2994,10098,10099],{"class":3000},"    // Тести update() та delete() — ідентичні H2-версії\n",[2994,10101,10102],{"class":2996,"line":6739},[2994,10103,6787],{"class":3000},[2994,10105,10106],{"class":2996,"line":6744},[2994,10107,3062],{"emptyLinePlaceholder":3061},[2994,10109,10110,10112],{"class":2996,"line":6752},[2994,10111,4043],{"class":3021},[2994,10113,4138],{"class":3187},[2994,10115,10116,10118,10121],{"class":2996,"line":6762},[2994,10117,4143],{"class":3187},[2994,10119,10120],{"class":3014}," update_shouldModifyAllFields_whenAuthorExists",[2994,10122,4407],{"class":3021},[2994,10124,10125],{"class":2996,"line":6768},[2994,10126,9151],{"class":3000},[2994,10128,10129,10131,10133,10135,10137,10139,10141,10143,10145,10147],{"class":2996,"line":6774},[2994,10130,9156],{"class":3187},[2994,10132,9159],{"class":3191},[2994,10134,3195],{"class":3021},[2994,10136,4073],{"class":3700},[2994,10138,9166],{"class":3014},[2994,10140,3111],{"class":3021},[2994,10142,9171],{"class":3025},[2994,10144,3029],{"class":3021},[2994,10146,9176],{"class":3025},[2994,10148,3055],{"class":3021},[2994,10150,10151,10153,10155,10157,10159,10162],{"class":2996,"line":6779},[2994,10152,9183],{"class":3191},[2994,10154,3217],{"class":3021},[2994,10156,9188],{"class":3014},[2994,10158,3111],{"class":3021},[2994,10160,10161],{"class":3025},"\"Стара біографія\"",[2994,10163,3055],{"class":3021},[2994,10165,10166,10168,10170,10172],{"class":2996,"line":6784},[2994,10167,9225],{"class":3191},[2994,10169,3217],{"class":3021},[2994,10171,9230],{"class":3014},[2994,10173,9233],{"class":3021},[2994,10175,10176],{"class":2996,"line":6790},[2994,10177,3062],{"emptyLinePlaceholder":3061},[2994,10179,10180,10182,10184,10187,10189,10192],{"class":2996,"line":6796},[2994,10181,9183],{"class":3191},[2994,10183,3217],{"class":3021},[2994,10185,10186],{"class":3014},"setFirstName",[2994,10188,3111],{"class":3021},[2994,10190,10191],{"class":3025},"\"Іван Якович\"",[2994,10193,3055],{"class":3021},[2994,10195,10196,10198,10200,10203,10205,10208],{"class":2996,"line":6801},[2994,10197,9183],{"class":3191},[2994,10199,3217],{"class":3021},[2994,10201,10202],{"class":3014},"setLastName",[2994,10204,3111],{"class":3021},[2994,10206,10207],{"class":3025},"\"Франко-Захарченко\"",[2994,10209,3055],{"class":3021},[2994,10211,10212,10214,10216,10218,10220,10223],{"class":2996,"line":6806},[2994,10213,9183],{"class":3191},[2994,10215,3217],{"class":3021},[2994,10217,9188],{"class":3014},[2994,10219,3111],{"class":3021},[2994,10221,10222],{"class":3025},"\"Нова біографія\"",[2994,10224,3055],{"class":3021},[2994,10226,10227,10229,10231,10233,10235,10238],{"class":2996,"line":6811},[2994,10228,9183],{"class":3191},[2994,10230,3217],{"class":3021},[2994,10232,9204],{"class":3014},[2994,10234,3111],{"class":3021},[2994,10236,10237],{"class":3025},"\"/images/new.jpg\"",[2994,10239,3055],{"class":3021},[2994,10241,10242],{"class":2996,"line":6817},[2994,10243,3062],{"emptyLinePlaceholder":3061},[2994,10245,10246],{"class":2996,"line":6822},[2994,10247,9220],{"class":3000},[2994,10249,10250,10252,10254,10257],{"class":2996,"line":6828},[2994,10251,9225],{"class":3191},[2994,10253,3217],{"class":3021},[2994,10255,10256],{"class":3014},"update",[2994,10258,9233],{"class":3021},[2994,10260,10261],{"class":2996,"line":6834},[2994,10262,3062],{"emptyLinePlaceholder":3061},[2994,10264,10265],{"class":2996,"line":6839},[2994,10266,9242],{"class":3000},[2994,10268,10269,10271,10273,10275,10277,10279,10281,10283,10285,10287,10289,10291,10293],{"class":2996,"line":6870},[2994,10270,9156],{"class":3187},[2994,10272,9276],{"class":3191},[2994,10274,3195],{"class":3021},[2994,10276,9281],{"class":3191},[2994,10278,3217],{"class":3021},[2994,10280,9286],{"class":3014},[2994,10282,3111],{"class":3021},[2994,10284,9252],{"class":3191},[2994,10286,3217],{"class":3021},[2994,10288,9257],{"class":3014},[2994,10290,9260],{"class":3021},[2994,10292,9299],{"class":3014},[2994,10294,4382],{"class":3021},[2994,10296,10297,10299,10301,10303,10305,10307,10309,10311,10313,10315],{"class":2996,"line":6885},[2994,10298,9247],{"class":3014},[2994,10300,3111],{"class":3021},[2994,10302,9310],{"class":3191},[2994,10304,3217],{"class":3021},[2994,10306,9315],{"class":3014},[2994,10308,9260],{"class":3021},[2994,10310,9320],{"class":3014},[2994,10312,3111],{"class":3021},[2994,10314,10191],{"class":3025},[2994,10316,3055],{"class":3021},[2994,10318,10319,10321,10323,10325,10327,10329,10331,10333,10335,10337],{"class":2996,"line":6891},[2994,10320,9247],{"class":3014},[2994,10322,3111],{"class":3021},[2994,10324,9310],{"class":3191},[2994,10326,3217],{"class":3021},[2994,10328,9339],{"class":3014},[2994,10330,9260],{"class":3021},[2994,10332,9320],{"class":3014},[2994,10334,3111],{"class":3021},[2994,10336,10207],{"class":3025},[2994,10338,3055],{"class":3021},[2994,10340,10341,10343,10345,10347,10349,10351,10353,10355,10357,10359],{"class":2996,"line":6912},[2994,10342,9247],{"class":3014},[2994,10344,3111],{"class":3021},[2994,10346,9310],{"class":3191},[2994,10348,3217],{"class":3021},[2994,10350,9362],{"class":3014},[2994,10352,9260],{"class":3021},[2994,10354,9320],{"class":3014},[2994,10356,3111],{"class":3021},[2994,10358,10222],{"class":3025},[2994,10360,3055],{"class":3021},[2994,10362,10363,10365,10367,10369,10371,10373,10375,10377,10379,10381],{"class":2996,"line":6929},[2994,10364,9247],{"class":3014},[2994,10366,3111],{"class":3021},[2994,10368,9310],{"class":3191},[2994,10370,3217],{"class":3021},[2994,10372,9385],{"class":3014},[2994,10374,9260],{"class":3021},[2994,10376,9320],{"class":3014},[2994,10378,3111],{"class":3021},[2994,10380,10237],{"class":3025},[2994,10382,3055],{"class":3021},[2994,10384,10385],{"class":2996,"line":6934},[2994,10386,4387],{"class":3021},[2994,10388,10389],{"class":2996,"line":6940},[2994,10390,3062],{"emptyLinePlaceholder":3061},[2994,10392,10393,10395],{"class":2996,"line":6968},[2994,10394,4043],{"class":3021},[2994,10396,4138],{"class":3187},[2994,10398,10399,10401,10404],{"class":2996,"line":6987},[2994,10400,4143],{"class":3187},[2994,10402,10403],{"class":3014}," deleteById_shouldRemoveAuthor_whenExists",[2994,10405,4407],{"class":3021},[2994,10407,10408],{"class":2996,"line":7008},[2994,10409,9151],{"class":3000},[2994,10411,10412,10414,10416,10418,10420,10422,10424,10427,10429,10432],{"class":2996,"line":7043},[2994,10413,9156],{"class":3187},[2994,10415,9159],{"class":3191},[2994,10417,3195],{"class":3021},[2994,10419,4073],{"class":3700},[2994,10421,9166],{"class":3014},[2994,10423,3111],{"class":3021},[2994,10425,10426],{"class":3025},"\"Панас\"",[2994,10428,3029],{"class":3021},[2994,10430,10431],{"class":3025},"\"Мирний\"",[2994,10433,3055],{"class":3021},[2994,10435,10436,10438,10440,10442],{"class":2996,"line":7056},[2994,10437,9225],{"class":3191},[2994,10439,3217],{"class":3021},[2994,10441,9230],{"class":3014},[2994,10443,9233],{"class":3021},[2994,10445,10446,10448,10451,10453,10455,10457,10459],{"class":2996,"line":7062},[2994,10447,9603],{"class":3187},[2994,10449,10450],{"class":3191}," authorId",[2994,10452,3195],{"class":3021},[2994,10454,9252],{"class":3191},[2994,10456,3217],{"class":3021},[2994,10458,9257],{"class":3014},[2994,10460,4382],{"class":3021},[2994,10462,10463],{"class":2996,"line":7068},[2994,10464,3062],{"emptyLinePlaceholder":3061},[2994,10466,10467],{"class":2996,"line":7073},[2994,10468,9220],{"class":3000},[2994,10470,10471,10474,10477,10479,10481,10483,10486],{"class":2996,"line":7078},[2994,10472,10473],{"class":3187},"        boolean",[2994,10475,10476],{"class":3191}," deleted",[2994,10478,3195],{"class":3021},[2994,10480,9281],{"class":3191},[2994,10482,3217],{"class":3021},[2994,10484,10485],{"class":3014},"deleteById",[2994,10487,10488],{"class":3021},"(authorId);\n",[2994,10490,10491],{"class":2996,"line":7083},[2994,10492,3062],{"emptyLinePlaceholder":3061},[2994,10494,10495],{"class":2996,"line":7088},[2994,10496,9242],{"class":3000},[2994,10498,10499,10501,10504,10507],{"class":2996,"line":7094},[2994,10500,9247],{"class":3014},[2994,10502,10503],{"class":3021},"(deleted).",[2994,10505,10506],{"class":3014},"isTrue",[2994,10508,4382],{"class":3021},[2994,10510,10511,10513,10515,10517,10519,10521,10524,10526],{"class":2996,"line":7099},[2994,10512,9247],{"class":3014},[2994,10514,3111],{"class":3021},[2994,10516,9281],{"class":3191},[2994,10518,3217],{"class":3021},[2994,10520,9286],{"class":3014},[2994,10522,10523],{"class":3021},"(authorId)).",[2994,10525,7022],{"class":3014},[2994,10527,4382],{"class":3021},[2994,10529,10530,10532,10534,10536,10538,10540,10542,10544,10546,10548],{"class":2996,"line":7126},[2994,10531,9247],{"class":3014},[2994,10533,3111],{"class":3021},[2994,10535,9408],{"class":3014},[2994,10537,3111],{"class":3021},[2994,10539,9413],{"class":3025},[2994,10541,9416],{"class":3021},[2994,10543,9320],{"class":3014},[2994,10545,3111],{"class":3021},[2994,10547,7968],{"class":3114},[2994,10549,3055],{"class":3021},[2994,10551,10552],{"class":2996,"line":7149},[2994,10553,4387],{"class":3021},[2994,10555,10556],{"class":2996,"line":7160},[2994,10557,3062],{"emptyLinePlaceholder":3061},[2994,10559,10560],{"class":2996,"line":7171},[2994,10561,6787],{"class":3000},[2994,10563,10564],{"class":2996,"line":7183},[2994,10565,10566],{"class":3000},"    // Тести findByLastName() — ідентичні H2-версії\n",[2994,10568,10569],{"class":2996,"line":7194},[2994,10570,6787],{"class":3000},[2994,10572,10573],{"class":2996,"line":7208},[2994,10574,3062],{"emptyLinePlaceholder":3061},[2994,10576,10577,10579],{"class":2996,"line":7251},[2994,10578,4043],{"class":3021},[2994,10580,4138],{"class":3187},[2994,10582,10583,10585,10588],{"class":2996,"line":7256},[2994,10584,4143],{"class":3187},[2994,10586,10587],{"class":3014}," findByLastName_shouldReturnMatchingAuthors_whenPartialMatch",[2994,10589,4407],{"class":3021},[2994,10591,10592],{"class":2996,"line":7261},[2994,10593,9151],{"class":3000},[2994,10595,10596,10598,10600,10602,10604,10606,10608,10610,10612,10614,10616],{"class":2996,"line":7266},[2994,10597,9225],{"class":3191},[2994,10599,3217],{"class":3021},[2994,10601,9230],{"class":3014},[2994,10603,3111],{"class":3021},[2994,10605,4073],{"class":3700},[2994,10607,9166],{"class":3014},[2994,10609,3111],{"class":3021},[2994,10611,9640],{"class":3025},[2994,10613,3029],{"class":3021},[2994,10615,9645],{"class":3025},[2994,10617,7248],{"class":3021},[2994,10619,10620,10622,10624,10626,10628,10630,10632,10634,10636,10638,10640],{"class":2996,"line":7271},[2994,10621,9225],{"class":3191},[2994,10623,3217],{"class":3021},[2994,10625,9230],{"class":3014},[2994,10627,3111],{"class":3021},[2994,10629,4073],{"class":3700},[2994,10631,9166],{"class":3014},[2994,10633,3111],{"class":3021},[2994,10635,9171],{"class":3025},[2994,10637,3029],{"class":3021},[2994,10639,9176],{"class":3025},[2994,10641,7248],{"class":3021},[2994,10643,10644,10646,10648,10650,10652,10654,10656,10658,10660,10662,10664],{"class":2996,"line":7277},[2994,10645,9225],{"class":3191},[2994,10647,3217],{"class":3021},[2994,10649,9230],{"class":3014},[2994,10651,3111],{"class":3021},[2994,10653,4073],{"class":3700},[2994,10655,9166],{"class":3014},[2994,10657,3111],{"class":3021},[2994,10659,9468],{"class":3025},[2994,10661,3029],{"class":3021},[2994,10663,9473],{"class":3025},[2994,10665,7248],{"class":3021},[2994,10667,10668,10670,10672,10674,10676,10678,10680,10682,10685,10687,10690],{"class":2996,"line":7282},[2994,10669,9225],{"class":3191},[2994,10671,3217],{"class":3021},[2994,10673,9230],{"class":3014},[2994,10675,3111],{"class":3021},[2994,10677,4073],{"class":3700},[2994,10679,9166],{"class":3014},[2994,10681,3111],{"class":3021},[2994,10683,10684],{"class":3025},"\"Григорій\"",[2994,10686,3029],{"class":3021},[2994,10688,10689],{"class":3025},"\"Сковорода\"",[2994,10691,7248],{"class":3021},[2994,10693,10694],{"class":2996,"line":7308},[2994,10695,3062],{"emptyLinePlaceholder":3061},[2994,10697,10698],{"class":2996,"line":7323},[2994,10699,9220],{"class":3000},[2994,10701,10702,10705,10707,10709,10711,10713,10715,10717,10719,10722,10724,10727],{"class":2996,"line":7344},[2994,10703,10704],{"class":3187},"        List",[2994,10706,4060],{"class":3021},[2994,10708,9892],{"class":3187},[2994,10710,4065],{"class":3021},[2994,10712,9897],{"class":3191},[2994,10714,3195],{"class":3021},[2994,10716,9281],{"class":3191},[2994,10718,3217],{"class":3021},[2994,10720,10721],{"class":3014},"findByLastName",[2994,10723,3111],{"class":3021},[2994,10725,10726],{"class":3025},"\"ко\"",[2994,10728,3055],{"class":3021},[2994,10730,10731],{"class":2996,"line":7361},[2994,10732,3062],{"emptyLinePlaceholder":3061},[2994,10734,10735],{"class":2996,"line":7382},[2994,10736,9242],{"class":3000},[2994,10738,10739,10741,10743,10746,10748,10751],{"class":2996,"line":7395},[2994,10740,9247],{"class":3014},[2994,10742,9931],{"class":3021},[2994,10744,10745],{"class":3014},"hasSize",[2994,10747,3111],{"class":3021},[2994,10749,10750],{"class":3114},"2",[2994,10752,3055],{"class":3021},[2994,10754,10755,10757],{"class":2996,"line":7414},[2994,10756,9247],{"class":3014},[2994,10758,10759],{"class":3021},"(result)\n",[2994,10761,10762,10764,10767,10770,10773],{"class":2996,"line":7419},[2994,10763,4315],{"class":3021},[2994,10765,10766],{"class":3014},"extracting",[2994,10768,10769],{"class":3021},"(Author",[2994,10771,10772],{"class":3700},"::",[2994,10774,10775],{"class":3021},"getLastName)\n",[2994,10777,10778,10780,10783,10785,10787,10789,10791],{"class":2996,"line":7424},[2994,10779,4315],{"class":3021},[2994,10781,10782],{"class":3014},"containsExactlyInAnyOrder",[2994,10784,3111],{"class":3021},[2994,10786,9176],{"class":3025},[2994,10788,3029],{"class":3021},[2994,10790,10689],{"class":3025},[2994,10792,3055],{"class":3021},[2994,10794,10795],{"class":2996,"line":7429},[2994,10796,4387],{"class":3021},[2994,10798,10799],{"class":2996,"line":7434},[2994,10800,3062],{"emptyLinePlaceholder":3061},[2994,10802,10803,10805],{"class":2996,"line":7440},[2994,10804,4043],{"class":3021},[2994,10806,4138],{"class":3187},[2994,10808,10809,10811,10814],{"class":2996,"line":7445},[2994,10810,4143],{"class":3187},[2994,10812,10813],{"class":3014}," findByLastName_shouldBeCaseInsensitive_whenSearching",[2994,10815,4407],{"class":3021},[2994,10817,10818],{"class":2996,"line":7469},[2994,10819,9151],{"class":3000},[2994,10821,10822,10824,10826,10828,10830,10832,10834,10836,10838,10840,10842],{"class":2996,"line":7490},[2994,10823,9225],{"class":3191},[2994,10825,3217],{"class":3021},[2994,10827,9230],{"class":3014},[2994,10829,3111],{"class":3021},[2994,10831,4073],{"class":3700},[2994,10833,9166],{"class":3014},[2994,10835,3111],{"class":3021},[2994,10837,9640],{"class":3025},[2994,10839,3029],{"class":3021},[2994,10841,9645],{"class":3025},[2994,10843,7248],{"class":3021},[2994,10845,10846],{"class":2996,"line":7507},[2994,10847,3062],{"emptyLinePlaceholder":3061},[2994,10849,10850],{"class":2996,"line":7519},[2994,10851,9220],{"class":3000},[2994,10853,10854,10856,10858,10860,10862,10865,10867,10869,10871,10873,10875,10878],{"class":2996,"line":7524},[2994,10855,10704],{"class":3187},[2994,10857,4060],{"class":3021},[2994,10859,9892],{"class":3187},[2994,10861,4065],{"class":3021},[2994,10863,10864],{"class":3191},"result1",[2994,10866,3195],{"class":3021},[2994,10868,9281],{"class":3191},[2994,10870,3217],{"class":3021},[2994,10872,10721],{"class":3014},[2994,10874,3111],{"class":3021},[2994,10876,10877],{"class":3025},"\"шевч\"",[2994,10879,3055],{"class":3021},[2994,10881,10882,10884,10886,10888,10890,10893,10895,10897,10899,10901,10903,10906],{"class":2996,"line":7529},[2994,10883,10704],{"class":3187},[2994,10885,4060],{"class":3021},[2994,10887,9892],{"class":3187},[2994,10889,4065],{"class":3021},[2994,10891,10892],{"class":3191},"result2",[2994,10894,3195],{"class":3021},[2994,10896,9281],{"class":3191},[2994,10898,3217],{"class":3021},[2994,10900,10721],{"class":3014},[2994,10902,3111],{"class":3021},[2994,10904,10905],{"class":3025},"\"ШЕВЧ\"",[2994,10907,3055],{"class":3021},[2994,10909,10911,10913,10915,10917,10919,10922,10924,10926,10928,10930,10932,10935],{"class":2996,"line":10910},207,[2994,10912,10704],{"class":3187},[2994,10914,4060],{"class":3021},[2994,10916,9892],{"class":3187},[2994,10918,4065],{"class":3021},[2994,10920,10921],{"class":3191},"result3",[2994,10923,3195],{"class":3021},[2994,10925,9281],{"class":3191},[2994,10927,3217],{"class":3021},[2994,10929,10721],{"class":3014},[2994,10931,3111],{"class":3021},[2994,10933,10934],{"class":3025},"\"ШеВч\"",[2994,10936,3055],{"class":3021},[2994,10938,10940],{"class":2996,"line":10939},208,[2994,10941,3062],{"emptyLinePlaceholder":3061},[2994,10943,10945],{"class":2996,"line":10944},209,[2994,10946,9242],{"class":3000},[2994,10948,10950,10952,10955,10957,10959,10961],{"class":2996,"line":10949},210,[2994,10951,9247],{"class":3014},[2994,10953,10954],{"class":3021},"(result1).",[2994,10956,10745],{"class":3014},[2994,10958,3111],{"class":3021},[2994,10960,7409],{"class":3114},[2994,10962,3055],{"class":3021},[2994,10964,10966,10968,10971,10973,10975,10977],{"class":2996,"line":10965},211,[2994,10967,9247],{"class":3014},[2994,10969,10970],{"class":3021},"(result2).",[2994,10972,10745],{"class":3014},[2994,10974,3111],{"class":3021},[2994,10976,7409],{"class":3114},[2994,10978,3055],{"class":3021},[2994,10980,10982,10984,10987,10989,10991,10993],{"class":2996,"line":10981},212,[2994,10983,9247],{"class":3014},[2994,10985,10986],{"class":3021},"(result3).",[2994,10988,10745],{"class":3014},[2994,10990,3111],{"class":3021},[2994,10992,7409],{"class":3114},[2994,10994,3055],{"class":3021},[2994,10996,10998],{"class":2996,"line":10997},213,[2994,10999,4387],{"class":3021},[2994,11001,11003],{"class":2996,"line":11002},214,[2994,11004,4183],{"class":3021},[2964,11006,11007],{},[2971,11008,11009],{},"Ключові спостереження:",[4203,11011,11012,11021,11038],{},[3962,11013,11014,3769,11017,11020],{},[2971,11015,11016],{},"Рядок 33",[2991,11018,11019],{},"extends AbstractPostgresIntegrationTest","): Єдина зміна відносно H2-версії — базовий клас. Вся логіка тестів залишається ідентичною.",[3962,11022,11023,3769,11026,11029,11030,11033,11034,11037],{},[2971,11024,11025],{},"Рядок 103",[2991,11027,11028],{},"hasMessageContaining(\"duplicate key\")","): PostgreSQL повертає інше повідомлення про помилку, ніж H2. H2 повідомляє ",[2991,11031,11032],{},"\"PRIMARY KEY\"",", PostgreSQL — ",[2991,11035,11036],{},"\"duplicate key value violates unique constraint\"",". Це єдина відмінність у тестах.",[3962,11039,11040,11043],{},[2971,11041,11042],{},"Швидкість виконання:"," Тести на PostgreSQL виконуються повільніше (~30–50ms на тест) порівняно з H2 (~5–10ms), але це ціна за точність емуляції.",[4961,11045,11046,11051,11062],{},[2964,11047,11048],{},[2971,11049,11050],{},"Чому більшість тестів ідентичні?",[2964,11052,11053,11054,11057,11058,11061],{},"Це демонструє силу абстракції через ",[2991,11055,11056],{},"Repository\u003CT, ID>"," інтерфейс. Репозиторій не знає, чи він працює з H2, PostgreSQL, MySQL чи Oracle — він просто виконує SQL через JDBC. Тести перевіряють ",[2971,11059,11060],{},"контракт репозиторію",", а не специфіку СУБД.",[2964,11063,11064],{},"Специфічні для PostgreSQL тести (ENUM, JSON, full-text search) ми розглянемо у наступному розділі.",[3307,11066],{},[2959,11068,11070],{"id":11069},"тестування-postgresql-специфічних-функцій","Тестування PostgreSQL-специфічних функцій",[2964,11072,11073,11074,11077],{},"Тепер розглянемо тести, що ",[2971,11075,11076],{},"неможливо"," виконати на H2 — вони вимагають справжньої PostgreSQL.",[2976,11079,11081],{"id":11080},"тестування-enum-типів","Тестування ENUM-типів",[2964,11083,11084],{},"PostgreSQL ENUM є окремим типом даних з type safety на рівні БД. Спроба вставити значення, що не входить до ENUM, призводить до помилки на рівні БД, а не на рівні додатку.",[2984,11086,11088],{"className":3173,"code":11087,"language":3175,"meta":5001,"style":2989},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.domain.Author;\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.domain.AudiobookFile;\nimport com.example.audiobook.domain.Genre;\nimport com.example.audiobook.repository.jdbc.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.sql.Connection;\nimport java.sql.PreparedStatement;\nimport java.sql.SQLException;\nimport java.util.UUID;\n\nimport static org.assertj.core.api.Assertions.*;\n\n/**\n * Тести для PostgreSQL ENUM-типів.\n * \u003Cp>\n * \u003Cb>Що тестується:\u003C/b>\n * \u003Cul>\n *   \u003Cli>Вставка валідних ENUM-значень\u003C/li>\n *   \u003Cli>Відхилення невалідних ENUM-значень на рівні БД\u003C/li>\n *   \u003Cli>Маппінг ENUM → Java enum (якщо використовується)\u003C/li>\n * \u003C/ul>\n * \u003Cp>\n * \u003Cb>Чому це неможливо на H2:\u003C/b>\n * H2 не має справжніх ENUM — лише VARCHAR з CHECK constraint.\n * CHECK constraint перевіряє значення на рівні рядка, а не типу.\n */\nclass AudiobookFileEnumTest extends AbstractPostgresIntegrationTest {\n\n    private AudiobookFileRepository fileRepo;\n    private AudiobookRepository audiobookRepo;\n    private AuthorRepository authorRepo;\n    private GenreRepository genreRepo;\n\n    @BeforeEach\n    void setUpRepositories() {\n        fileRepo = new JdbcAudiobookFileRepository(connectionManager);\n        audiobookRepo = new JdbcAudiobookRepository(connectionManager);\n        authorRepo = new JdbcAuthorRepository(connectionManager);\n        genreRepo = new JdbcGenreRepository(connectionManager);\n    }\n\n    @Test\n    void save_shouldInsertFile_whenValidEnumFormat() throws SQLException {\n        // ═══ Arrange ═══\n        // Створити батьківські сутності\n        Author author = new Author(\"Автор\", \"Тест\");\n        Genre genre = new Genre(\"Жанр\");\n        authorRepo.save(author);\n        genreRepo.save(genre);\n\n        Audiobook book = new Audiobook(\"Книга\", author, genre);\n        book.setDuration(3600);\n        book.setReleaseYear(2020);\n        audiobookRepo.save(book);\n\n        // Створити файл з валідним ENUM-форматом\n        AudiobookFile file = new AudiobookFile();\n        file.setId(UUID.randomUUID());\n        file.setAudiobookId(book.getId());\n        file.setFilePath(\"/files/book.mp3\");\n        file.setFormat(\"mp3\"); // ← валідне значення з file_format_enum\n        file.setSize(10485760); // 10 MB\n\n        // ═══ Act ═══\n        fileRepo.save(file);\n\n        // ═══ Assert ═══\n        AudiobookFile loaded = fileRepo.findById(file.getId()).orElseThrow();\n        assertThat(loaded.getFormat()).isEqualTo(\"mp3\");\n        assertThat(loaded.getFilePath()).isEqualTo(\"/files/book.mp3\");\n    }\n\n    @Test\n    void save_shouldThrowException_whenInvalidEnumFormat() throws SQLException {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Автор\", \"Тест\");\n        Genre genre = new Genre(\"Жанр\");\n        authorRepo.save(author);\n        genreRepo.save(genre);\n\n        Audiobook book = new Audiobook(\"Книга\", author, genre);\n        book.setDuration(3600);\n        book.setReleaseYear(2020);\n        audiobookRepo.save(book);\n\n        // ═══ Act & Assert ═══\n        // Спроба вставити невалідне значення ENUM через сирий SQL\n        // (обходимо Java-валідацію, щоб протестувати БД-рівень)\n        String sql = \"INSERT INTO audiobook_files (id, audiobook_id, file_path, format, size) \" +\n                     \"VALUES (?, ?, ?, ?::file_format_enum, ?)\";\n\n        assertThatThrownBy(() -> {\n            try (Connection conn = connectionManager.getConnection();\n                 PreparedStatement stmt = conn.prepareStatement(sql)) {\n                stmt.setObject(1, UUID.randomUUID());\n                stmt.setObject(2, book.getId());\n                stmt.setString(3, \"/files/book.xyz\");\n                stmt.setString(4, \"xyz\"); // ← НЕВАЛІДНЕ значення (не у ENUM)\n                stmt.setInt(5, 1000);\n                stmt.executeUpdate();\n            }\n        })\n        .isInstanceOf(SQLException.class)\n        .hasMessageContaining(\"invalid input value for enum file_format_enum\");\n        // PostgreSQL чітко повідомляє, що значення не входить до ENUM\n    }\n\n    @Test\n    void save_shouldAcceptAllValidEnumValues_whenIteratingFormats() throws SQLException {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Автор\", \"Тест\");\n        Genre genre = new Genre(\"Жанр\");\n        authorRepo.save(author);\n        genreRepo.save(genre);\n\n        Audiobook book = new Audiobook(\"Книга\", author, genre);\n        book.setDuration(3600);\n        book.setReleaseYear(2020);\n        audiobookRepo.save(book);\n\n        // Всі валідні значення з ENUM\n        String[] validFormats = {\"mp3\", \"ogg\", \"wav\", \"m4b\", \"aac\", \"flac\"};\n\n        // ═══ Act & Assert ═══\n        for (String format : validFormats) {\n            AudiobookFile file = new AudiobookFile();\n            file.setId(UUID.randomUUID());\n            file.setAudiobookId(book.getId());\n            file.setFilePath(\"/files/book.\" + format);\n            file.setFormat(format);\n            file.setSize(1000);\n\n            // Має пройти без виключень\n            assertThatCode(() -> fileRepo.save(file))\n                .doesNotThrowAnyException();\n        }\n\n        // Перевірка: у БД має бути 6 файлів\n        assertThat(countRowsInTable(\"audiobook_files\")).isEqualTo(6);\n    }\n}\n",[2991,11089,11090,11096,11100,11106,11113,11120,11127,11134,11140,11146,11150,11156,11163,11169,11175,11179,11187,11191,11195,11200,11204,11208,11212,11217,11222,11227,11231,11235,11240,11245,11250,11254,11267,11271,11283,11295,11306,11318,11322,11328,11337,11349,11361,11372,11384,11388,11392,11398,11413,11417,11422,11446,11468,11479,11491,11495,11518,11535,11551,11563,11567,11572,11589,11608,11628,11644,11662,11681,11685,11689,11701,11705,11709,11739,11762,11785,11789,11793,11799,11814,11818,11840,11858,11868,11878,11882,11900,11914,11928,11938,11942,11946,11951,11956,11968,11975,11979,11989,12010,12028,12051,12073,12093,12115,12136,12147,12151,12156,12172,12185,12190,12194,12198,12204,12219,12223,12245,12263,12273,12283,12287,12305,12319,12333,12343,12347,12352,12394,12398,12402,12419,12434,12453,12471,12487,12498,12512,12516,12521,12539,12549,12553,12557,12562,12586,12590],{"__ignoreMap":2989},[2994,11091,11092,11094],{"class":2996,"line":2997},[2994,11093,5671],{"class":3007},[2994,11095,5674],{"class":3021},[2994,11097,11098],{"class":2996,"line":3004},[2994,11099,3062],{"emptyLinePlaceholder":3061},[2994,11101,11102,11104],{"class":2996,"line":3058},[2994,11103,5683],{"class":3007},[2994,11105,8890],{"class":3021},[2994,11107,11108,11110],{"class":2996,"line":3065},[2994,11109,5683],{"class":3007},[2994,11111,11112],{"class":3021}," com.example.audiobook.domain.Audiobook;\n",[2994,11114,11115,11117],{"class":2996,"line":3079},[2994,11116,5683],{"class":3007},[2994,11118,11119],{"class":3021}," com.example.audiobook.domain.AudiobookFile;\n",[2994,11121,11122,11124],{"class":2996,"line":3091},[2994,11123,5683],{"class":3007},[2994,11125,11126],{"class":3021}," com.example.audiobook.domain.Genre;\n",[2994,11128,11129,11131],{"class":2996,"line":3102},[2994,11130,5683],{"class":3007},[2994,11132,11133],{"class":3021}," com.example.audiobook.repository.jdbc.*;\n",[2994,11135,11136,11138],{"class":2996,"line":3125},[2994,11137,5683],{"class":3007},[2994,11139,5707],{"class":3021},[2994,11141,11142,11144],{"class":2996,"line":3139},[2994,11143,5683],{"class":3007},[2994,11145,8910],{"class":3021},[2994,11147,11148],{"class":2996,"line":3148},[2994,11149,3062],{"emptyLinePlaceholder":3061},[2994,11151,11152,11154],{"class":2996,"line":3845},[2994,11153,5683],{"class":3007},[2994,11155,5774],{"class":3021},[2994,11157,11158,11160],{"class":2996,"line":3851},[2994,11159,5683],{"class":3007},[2994,11161,11162],{"class":3021}," java.sql.PreparedStatement;\n",[2994,11164,11165,11167],{"class":2996,"line":3856},[2994,11166,5683],{"class":3007},[2994,11168,5781],{"class":3021},[2994,11170,11171,11173],{"class":2996,"line":3861},[2994,11172,5683],{"class":3007},[2994,11174,8935],{"class":3021},[2994,11176,11177],{"class":2996,"line":3867},[2994,11178,3062],{"emptyLinePlaceholder":3061},[2994,11180,11181,11183,11185],{"class":2996,"line":3873},[2994,11182,5683],{"class":3007},[2994,11184,4269],{"class":3007},[2994,11186,8948],{"class":3021},[2994,11188,11189],{"class":2996,"line":3879},[2994,11190,3062],{"emptyLinePlaceholder":3061},[2994,11192,11193],{"class":2996,"line":3885},[2994,11194,5804],{"class":3000},[2994,11196,11197],{"class":2996,"line":3891},[2994,11198,11199],{"class":3000}," * Тести для PostgreSQL ENUM-типів.\n",[2994,11201,11202],{"class":2996,"line":3897},[2994,11203,5814],{"class":3000},[2994,11205,11206],{"class":2996,"line":3903},[2994,11207,9007],{"class":3000},[2994,11209,11210],{"class":2996,"line":3908},[2994,11211,5824],{"class":3000},[2994,11213,11214],{"class":2996,"line":3914},[2994,11215,11216],{"class":3000}," *   \u003Cli>Вставка валідних ENUM-значень\u003C/li>\n",[2994,11218,11219],{"class":2996,"line":3920},[2994,11220,11221],{"class":3000}," *   \u003Cli>Відхилення невалідних ENUM-значень на рівні БД\u003C/li>\n",[2994,11223,11224],{"class":2996,"line":3926},[2994,11225,11226],{"class":3000}," *   \u003Cli>Маппінг ENUM → Java enum (якщо використовується)\u003C/li>\n",[2994,11228,11229],{"class":2996,"line":3931},[2994,11230,5864],{"class":3000},[2994,11232,11233],{"class":2996,"line":3937},[2994,11234,5814],{"class":3000},[2994,11236,11237],{"class":2996,"line":3943},[2994,11238,11239],{"class":3000}," * \u003Cb>Чому це неможливо на H2:\u003C/b>\n",[2994,11241,11242],{"class":2996,"line":3949},[2994,11243,11244],{"class":3000}," * H2 не має справжніх ENUM — лише VARCHAR з CHECK constraint.\n",[2994,11246,11247],{"class":2996,"line":5348},[2994,11248,11249],{"class":3000}," * CHECK constraint перевіряє значення на рівні рядка, а не типу.\n",[2994,11251,11252],{"class":2996,"line":5365},[2994,11253,5965],{"class":3000},[2994,11255,11256,11258,11261,11263,11265],{"class":2996,"line":5382},[2994,11257,4028],{"class":3007},[2994,11259,11260],{"class":3187}," AudiobookFileEnumTest",[2994,11262,9049],{"class":3007},[2994,11264,5987],{"class":3187},[2994,11266,4034],{"class":3021},[2994,11268,11269],{"class":2996,"line":5391},[2994,11270,3062],{"emptyLinePlaceholder":3061},[2994,11272,11273,11275,11278,11281],{"class":2996,"line":5396},[2994,11274,4266],{"class":3007},[2994,11276,11277],{"class":3187}," AudiobookFileRepository",[2994,11279,11280],{"class":3191}," fileRepo",[2994,11282,3209],{"class":3021},[2994,11284,11285,11287,11290,11293],{"class":2996,"line":5402},[2994,11286,4266],{"class":3007},[2994,11288,11289],{"class":3187}," AudiobookRepository",[2994,11291,11292],{"class":3191}," audiobookRepo",[2994,11294,3209],{"class":3021},[2994,11296,11297,11299,11301,11304],{"class":2996,"line":5411},[2994,11298,4266],{"class":3007},[2994,11300,9064],{"class":3187},[2994,11302,11303],{"class":3191}," authorRepo",[2994,11305,3209],{"class":3021},[2994,11307,11308,11310,11313,11316],{"class":2996,"line":5429},[2994,11309,4266],{"class":3007},[2994,11311,11312],{"class":3187}," GenreRepository",[2994,11314,11315],{"class":3191}," genreRepo",[2994,11317,3209],{"class":3021},[2994,11319,11320],{"class":2996,"line":5446},[2994,11321,3062],{"emptyLinePlaceholder":3061},[2994,11323,11324,11326],{"class":2996,"line":5464},[2994,11325,4043],{"class":3021},[2994,11327,6490],{"class":3187},[2994,11329,11330,11332,11335],{"class":2996,"line":5473},[2994,11331,4143],{"class":3187},[2994,11333,11334],{"class":3014}," setUpRepositories",[2994,11336,4407],{"class":3021},[2994,11338,11339,11342,11344,11347],{"class":2996,"line":5478},[2994,11340,11341],{"class":3021},"        fileRepo = ",[2994,11343,4073],{"class":3700},[2994,11345,11346],{"class":3014}," JdbcAudiobookFileRepository",[2994,11348,9106],{"class":3021},[2994,11350,11351,11354,11356,11359],{"class":2996,"line":5484},[2994,11352,11353],{"class":3021},"        audiobookRepo = ",[2994,11355,4073],{"class":3700},[2994,11357,11358],{"class":3014}," JdbcAudiobookRepository",[2994,11360,9106],{"class":3021},[2994,11362,11363,11366,11368,11370],{"class":2996,"line":5493},[2994,11364,11365],{"class":3021},"        authorRepo = ",[2994,11367,4073],{"class":3700},[2994,11369,9103],{"class":3014},[2994,11371,9106],{"class":3021},[2994,11373,11374,11377,11379,11382],{"class":2996,"line":5511},[2994,11375,11376],{"class":3021},"        genreRepo = ",[2994,11378,4073],{"class":3700},[2994,11380,11381],{"class":3014}," JdbcGenreRepository",[2994,11383,9106],{"class":3021},[2994,11385,11386],{"class":2996,"line":5529},[2994,11387,4387],{"class":3021},[2994,11389,11390],{"class":2996,"line":5547},[2994,11391,3062],{"emptyLinePlaceholder":3061},[2994,11393,11394,11396],{"class":2996,"line":5564},[2994,11395,4043],{"class":3021},[2994,11397,4138],{"class":3187},[2994,11399,11400,11402,11405,11407,11409,11411],{"class":2996,"line":5573},[2994,11401,4143],{"class":3187},[2994,11403,11404],{"class":3014}," save_shouldInsertFile_whenValidEnumFormat",[2994,11406,6279],{"class":3021},[2994,11408,6282],{"class":3007},[2994,11410,6505],{"class":3187},[2994,11412,4034],{"class":3021},[2994,11414,11415],{"class":2996,"line":5939},[2994,11416,9151],{"class":3000},[2994,11418,11419],{"class":2996,"line":5944},[2994,11420,11421],{"class":3000},"        // Створити батьківські сутності\n",[2994,11423,11424,11426,11428,11430,11432,11434,11436,11439,11441,11444],{"class":2996,"line":5950},[2994,11425,9156],{"class":3187},[2994,11427,9159],{"class":3191},[2994,11429,3195],{"class":3021},[2994,11431,4073],{"class":3700},[2994,11433,9166],{"class":3014},[2994,11435,3111],{"class":3021},[2994,11437,11438],{"class":3025},"\"Автор\"",[2994,11440,3029],{"class":3021},[2994,11442,11443],{"class":3025},"\"Тест\"",[2994,11445,3055],{"class":3021},[2994,11447,11448,11451,11454,11456,11458,11461,11463,11466],{"class":2996,"line":5956},[2994,11449,11450],{"class":3187},"        Genre",[2994,11452,11453],{"class":3191}," genre",[2994,11455,3195],{"class":3021},[2994,11457,4073],{"class":3700},[2994,11459,11460],{"class":3014}," Genre",[2994,11462,3111],{"class":3021},[2994,11464,11465],{"class":3025},"\"Жанр\"",[2994,11467,3055],{"class":3021},[2994,11469,11470,11473,11475,11477],{"class":2996,"line":5962},[2994,11471,11472],{"class":3191},"        authorRepo",[2994,11474,3217],{"class":3021},[2994,11476,9230],{"class":3014},[2994,11478,9233],{"class":3021},[2994,11480,11481,11484,11486,11488],{"class":2996,"line":5968},[2994,11482,11483],{"class":3191},"        genreRepo",[2994,11485,3217],{"class":3021},[2994,11487,9230],{"class":3014},[2994,11489,11490],{"class":3021},"(genre);\n",[2994,11492,11493],{"class":2996,"line":5978},[2994,11494,3062],{"emptyLinePlaceholder":3061},[2994,11496,11497,11500,11503,11505,11507,11510,11512,11515],{"class":2996,"line":5992},[2994,11498,11499],{"class":3187},"        Audiobook",[2994,11501,11502],{"class":3191}," book",[2994,11504,3195],{"class":3021},[2994,11506,4073],{"class":3700},[2994,11508,11509],{"class":3014}," Audiobook",[2994,11511,3111],{"class":3021},[2994,11513,11514],{"class":3025},"\"Книга\"",[2994,11516,11517],{"class":3021},", author, genre);\n",[2994,11519,11520,11523,11525,11528,11530,11533],{"class":2996,"line":5997},[2994,11521,11522],{"class":3191},"        book",[2994,11524,3217],{"class":3021},[2994,11526,11527],{"class":3014},"setDuration",[2994,11529,3111],{"class":3021},[2994,11531,11532],{"class":3114},"3600",[2994,11534,3055],{"class":3021},[2994,11536,11537,11539,11541,11544,11546,11549],{"class":2996,"line":6003},[2994,11538,11522],{"class":3191},[2994,11540,3217],{"class":3021},[2994,11542,11543],{"class":3014},"setReleaseYear",[2994,11545,3111],{"class":3021},[2994,11547,11548],{"class":3114},"2020",[2994,11550,3055],{"class":3021},[2994,11552,11553,11556,11558,11560],{"class":2996,"line":6009},[2994,11554,11555],{"class":3191},"        audiobookRepo",[2994,11557,3217],{"class":3021},[2994,11559,9230],{"class":3014},[2994,11561,11562],{"class":3021},"(book);\n",[2994,11564,11565],{"class":2996,"line":6015},[2994,11566,3062],{"emptyLinePlaceholder":3061},[2994,11568,11569],{"class":2996,"line":6021},[2994,11570,11571],{"class":3000},"        // Створити файл з валідним ENUM-форматом\n",[2994,11573,11574,11577,11580,11582,11584,11587],{"class":2996,"line":6027},[2994,11575,11576],{"class":3187},"        AudiobookFile",[2994,11578,11579],{"class":3191}," file",[2994,11581,3195],{"class":3021},[2994,11583,4073],{"class":3700},[2994,11585,11586],{"class":3014}," AudiobookFile",[2994,11588,4382],{"class":3021},[2994,11590,11591,11594,11596,11598,11600,11602,11604,11606],{"class":2996,"line":6032},[2994,11592,11593],{"class":3191},"        file",[2994,11595,3217],{"class":3021},[2994,11597,9657],{"class":3014},[2994,11599,3111],{"class":3021},[2994,11601,9611],{"class":3191},[2994,11603,3217],{"class":3021},[2994,11605,9616],{"class":3014},[2994,11607,9916],{"class":3021},[2994,11609,11610,11612,11614,11617,11619,11622,11624,11626],{"class":2996,"line":6038},[2994,11611,11593],{"class":3191},[2994,11613,3217],{"class":3021},[2994,11615,11616],{"class":3014},"setAudiobookId",[2994,11618,3111],{"class":3021},[2994,11620,11621],{"class":3191},"book",[2994,11623,3217],{"class":3021},[2994,11625,9257],{"class":3014},[2994,11627,9916],{"class":3021},[2994,11629,11630,11632,11634,11637,11639,11642],{"class":2996,"line":6044},[2994,11631,11593],{"class":3191},[2994,11633,3217],{"class":3021},[2994,11635,11636],{"class":3014},"setFilePath",[2994,11638,3111],{"class":3021},[2994,11640,11641],{"class":3025},"\"/files/book.mp3\"",[2994,11643,3055],{"class":3021},[2994,11645,11646,11648,11650,11653,11655,11657,11659],{"class":2996,"line":6049},[2994,11647,11593],{"class":3191},[2994,11649,3217],{"class":3021},[2994,11651,11652],{"class":3014},"setFormat",[2994,11654,3111],{"class":3021},[2994,11656,3230],{"class":3025},[2994,11658,3233],{"class":3021},[2994,11660,11661],{"class":3000},"// ← валідне значення з file_format_enum\n",[2994,11663,11664,11666,11668,11671,11673,11676,11678],{"class":2996,"line":6055},[2994,11665,11593],{"class":3191},[2994,11667,3217],{"class":3021},[2994,11669,11670],{"class":3014},"setSize",[2994,11672,3111],{"class":3021},[2994,11674,11675],{"class":3114},"10485760",[2994,11677,3233],{"class":3021},[2994,11679,11680],{"class":3000},"// 10 MB\n",[2994,11682,11683],{"class":2996,"line":6061},[2994,11684,3062],{"emptyLinePlaceholder":3061},[2994,11686,11687],{"class":2996,"line":6067},[2994,11688,9220],{"class":3000},[2994,11690,11691,11694,11696,11698],{"class":2996,"line":6075},[2994,11692,11693],{"class":3191},"        fileRepo",[2994,11695,3217],{"class":3021},[2994,11697,9230],{"class":3014},[2994,11699,11700],{"class":3021},"(file);\n",[2994,11702,11703],{"class":2996,"line":6097},[2994,11704,3062],{"emptyLinePlaceholder":3061},[2994,11706,11707],{"class":2996,"line":6122},[2994,11708,9242],{"class":3000},[2994,11710,11711,11713,11715,11717,11720,11722,11724,11726,11729,11731,11733,11735,11737],{"class":2996,"line":6136},[2994,11712,11576],{"class":3187},[2994,11714,9276],{"class":3191},[2994,11716,3195],{"class":3021},[2994,11718,11719],{"class":3191},"fileRepo",[2994,11721,3217],{"class":3021},[2994,11723,9286],{"class":3014},[2994,11725,3111],{"class":3021},[2994,11727,11728],{"class":3191},"file",[2994,11730,3217],{"class":3021},[2994,11732,9257],{"class":3014},[2994,11734,9260],{"class":3021},[2994,11736,9299],{"class":3014},[2994,11738,4382],{"class":3021},[2994,11740,11741,11743,11745,11747,11749,11752,11754,11756,11758,11760],{"class":2996,"line":6149},[2994,11742,9247],{"class":3014},[2994,11744,3111],{"class":3021},[2994,11746,9310],{"class":3191},[2994,11748,3217],{"class":3021},[2994,11750,11751],{"class":3014},"getFormat",[2994,11753,9260],{"class":3021},[2994,11755,9320],{"class":3014},[2994,11757,3111],{"class":3021},[2994,11759,3230],{"class":3025},[2994,11761,3055],{"class":3021},[2994,11763,11764,11766,11768,11770,11772,11775,11777,11779,11781,11783],{"class":2996,"line":6163},[2994,11765,9247],{"class":3014},[2994,11767,3111],{"class":3021},[2994,11769,9310],{"class":3191},[2994,11771,3217],{"class":3021},[2994,11773,11774],{"class":3014},"getFilePath",[2994,11776,9260],{"class":3021},[2994,11778,9320],{"class":3014},[2994,11780,3111],{"class":3021},[2994,11782,11641],{"class":3025},[2994,11784,3055],{"class":3021},[2994,11786,11787],{"class":2996,"line":6180},[2994,11788,4387],{"class":3021},[2994,11790,11791],{"class":2996,"line":6185},[2994,11792,3062],{"emptyLinePlaceholder":3061},[2994,11794,11795,11797],{"class":2996,"line":6190},[2994,11796,4043],{"class":3021},[2994,11798,4138],{"class":3187},[2994,11800,11801,11803,11806,11808,11810,11812],{"class":2996,"line":6196},[2994,11802,4143],{"class":3187},[2994,11804,11805],{"class":3014}," save_shouldThrowException_whenInvalidEnumFormat",[2994,11807,6279],{"class":3021},[2994,11809,6282],{"class":3007},[2994,11811,6505],{"class":3187},[2994,11813,4034],{"class":3021},[2994,11815,11816],{"class":2996,"line":6202},[2994,11817,9151],{"class":3000},[2994,11819,11820,11822,11824,11826,11828,11830,11832,11834,11836,11838],{"class":2996,"line":6207},[2994,11821,9156],{"class":3187},[2994,11823,9159],{"class":3191},[2994,11825,3195],{"class":3021},[2994,11827,4073],{"class":3700},[2994,11829,9166],{"class":3014},[2994,11831,3111],{"class":3021},[2994,11833,11438],{"class":3025},[2994,11835,3029],{"class":3021},[2994,11837,11443],{"class":3025},[2994,11839,3055],{"class":3021},[2994,11841,11842,11844,11846,11848,11850,11852,11854,11856],{"class":2996,"line":6222},[2994,11843,11450],{"class":3187},[2994,11845,11453],{"class":3191},[2994,11847,3195],{"class":3021},[2994,11849,4073],{"class":3700},[2994,11851,11460],{"class":3014},[2994,11853,3111],{"class":3021},[2994,11855,11465],{"class":3025},[2994,11857,3055],{"class":3021},[2994,11859,11860,11862,11864,11866],{"class":2996,"line":6227},[2994,11861,11472],{"class":3191},[2994,11863,3217],{"class":3021},[2994,11865,9230],{"class":3014},[2994,11867,9233],{"class":3021},[2994,11869,11870,11872,11874,11876],{"class":2996,"line":6232},[2994,11871,11483],{"class":3191},[2994,11873,3217],{"class":3021},[2994,11875,9230],{"class":3014},[2994,11877,11490],{"class":3021},[2994,11879,11880],{"class":2996,"line":6238},[2994,11881,3062],{"emptyLinePlaceholder":3061},[2994,11883,11884,11886,11888,11890,11892,11894,11896,11898],{"class":2996,"line":6243},[2994,11885,11499],{"class":3187},[2994,11887,11502],{"class":3191},[2994,11889,3195],{"class":3021},[2994,11891,4073],{"class":3700},[2994,11893,11509],{"class":3014},[2994,11895,3111],{"class":3021},[2994,11897,11514],{"class":3025},[2994,11899,11517],{"class":3021},[2994,11901,11902,11904,11906,11908,11910,11912],{"class":2996,"line":6249},[2994,11903,11522],{"class":3191},[2994,11905,3217],{"class":3021},[2994,11907,11527],{"class":3014},[2994,11909,3111],{"class":3021},[2994,11911,11532],{"class":3114},[2994,11913,3055],{"class":3021},[2994,11915,11916,11918,11920,11922,11924,11926],{"class":2996,"line":6255},[2994,11917,11522],{"class":3191},[2994,11919,3217],{"class":3021},[2994,11921,11543],{"class":3014},[2994,11923,3111],{"class":3021},[2994,11925,11548],{"class":3114},[2994,11927,3055],{"class":3021},[2994,11929,11930,11932,11934,11936],{"class":2996,"line":6260},[2994,11931,11555],{"class":3191},[2994,11933,3217],{"class":3021},[2994,11935,9230],{"class":3014},[2994,11937,11562],{"class":3021},[2994,11939,11940],{"class":2996,"line":6268},[2994,11941,3062],{"emptyLinePlaceholder":3061},[2994,11943,11944],{"class":2996,"line":6295},[2994,11945,9722],{"class":3000},[2994,11947,11948],{"class":2996,"line":6301},[2994,11949,11950],{"class":3000},"        // Спроба вставити невалідне значення ENUM через сирий SQL\n",[2994,11952,11953],{"class":2996,"line":6307},[2994,11954,11955],{"class":3000},"        // (обходимо Java-валідацію, щоб протестувати БД-рівень)\n",[2994,11957,11958,11960,11962,11964,11966],{"class":2996,"line":6326},[2994,11959,6310],{"class":3187},[2994,11961,3192],{"class":3191},[2994,11963,3195],{"class":3021},[2994,11965,3198],{"class":3025},[2994,11967,3201],{"class":3021},[2994,11969,11970,11973],{"class":2996,"line":6345},[2994,11971,11972],{"class":3025},"                     \"VALUES (?, ?, ?, ?::file_format_enum, ?)\"",[2994,11974,3209],{"class":3021},[2994,11976,11977],{"class":2996,"line":6364},[2994,11978,3062],{"emptyLinePlaceholder":3061},[2994,11980,11981,11983,11985,11987],{"class":2996,"line":6369},[2994,11982,9727],{"class":3014},[2994,11984,9730],{"class":3021},[2994,11986,3385],{"class":3007},[2994,11988,4034],{"class":3021},[2994,11990,11991,11994,11996,11998,12000,12002,12004,12006,12008],{"class":2996,"line":6375},[2994,11992,11993],{"class":3700},"            try",[2994,11995,3769],{"class":3021},[2994,11997,6518],{"class":3187},[2994,11999,6521],{"class":3191},[2994,12001,3195],{"class":3021},[2994,12003,6526],{"class":3191},[2994,12005,3217],{"class":3021},[2994,12007,6531],{"class":3014},[2994,12009,4382],{"class":3021},[2994,12011,12012,12015,12017,12019,12021,12023,12026],{"class":2996,"line":6388},[2994,12013,12014],{"class":3187},"                 PreparedStatement",[2994,12016,6542],{"class":3191},[2994,12018,3195],{"class":3021},[2994,12020,6547],{"class":3191},[2994,12022,3217],{"class":3021},[2994,12024,12025],{"class":3014},"prepareStatement",[2994,12027,7379],{"class":3021},[2994,12029,12030,12033,12035,12037,12039,12041,12043,12045,12047,12049],{"class":2996,"line":6393},[2994,12031,12032],{"class":3191},"                stmt",[2994,12034,3217],{"class":3021},[2994,12036,3263],{"class":3014},[2994,12038,3111],{"class":3021},[2994,12040,7409],{"class":3114},[2994,12042,3029],{"class":3021},[2994,12044,9611],{"class":3191},[2994,12046,3217],{"class":3021},[2994,12048,9616],{"class":3014},[2994,12050,9916],{"class":3021},[2994,12052,12053,12055,12057,12059,12061,12063,12065,12067,12069,12071],{"class":2996,"line":6399},[2994,12054,12032],{"class":3191},[2994,12056,3217],{"class":3021},[2994,12058,3263],{"class":3014},[2994,12060,3111],{"class":3021},[2994,12062,10750],{"class":3114},[2994,12064,3029],{"class":3021},[2994,12066,11621],{"class":3191},[2994,12068,3217],{"class":3021},[2994,12070,9257],{"class":3014},[2994,12072,9916],{"class":3021},[2994,12074,12075,12077,12079,12081,12083,12086,12088,12091],{"class":2996,"line":6412},[2994,12076,12032],{"class":3191},[2994,12078,3217],{"class":3021},[2994,12080,3220],{"class":3014},[2994,12082,3111],{"class":3021},[2994,12084,12085],{"class":3114},"3",[2994,12087,3029],{"class":3021},[2994,12089,12090],{"class":3025},"\"/files/book.xyz\"",[2994,12092,3055],{"class":3021},[2994,12094,12095,12097,12099,12101,12103,12105,12107,12110,12112],{"class":2996,"line":6417},[2994,12096,12032],{"class":3191},[2994,12098,3217],{"class":3021},[2994,12100,3220],{"class":3014},[2994,12102,3111],{"class":3021},[2994,12104,3225],{"class":3114},[2994,12106,3029],{"class":3021},[2994,12108,12109],{"class":3025},"\"xyz\"",[2994,12111,3233],{"class":3021},[2994,12113,12114],{"class":3000},"// ← НЕВАЛІДНЕ значення (не у ENUM)\n",[2994,12116,12117,12119,12121,12124,12126,12129,12131,12134],{"class":2996,"line":6422},[2994,12118,12032],{"class":3191},[2994,12120,3217],{"class":3021},[2994,12122,12123],{"class":3014},"setInt",[2994,12125,3111],{"class":3021},[2994,12127,12128],{"class":3114},"5",[2994,12130,3029],{"class":3021},[2994,12132,12133],{"class":3114},"1000",[2994,12135,3055],{"class":3021},[2994,12137,12138,12140,12142,12145],{"class":2996,"line":6427},[2994,12139,12032],{"class":3191},[2994,12141,3217],{"class":3021},[2994,12143,12144],{"class":3014},"executeUpdate",[2994,12146,4382],{"class":3021},[2994,12148,12149],{"class":2996,"line":6433},[2994,12150,7065],{"class":3021},[2994,12152,12153],{"class":2996,"line":6438},[2994,12154,12155],{"class":3021},"        })\n",[2994,12157,12158,12160,12162,12164,12166,12168,12170],{"class":2996,"line":6444},[2994,12159,4089],{"class":3021},[2994,12161,9748],{"class":3014},[2994,12163,3111],{"class":3021},[2994,12165,6290],{"class":3191},[2994,12167,3217],{"class":3021},[2994,12169,4028],{"class":3191},[2994,12171,4084],{"class":3021},[2994,12173,12174,12176,12178,12180,12183],{"class":2996,"line":6450},[2994,12175,4089],{"class":3021},[2994,12177,9786],{"class":3014},[2994,12179,3111],{"class":3021},[2994,12181,12182],{"class":3025},"\"invalid input value for enum file_format_enum\"",[2994,12184,3055],{"class":3021},[2994,12186,12187],{"class":2996,"line":6456},[2994,12188,12189],{"class":3000},"        // PostgreSQL чітко повідомляє, що значення не входить до ENUM\n",[2994,12191,12192],{"class":2996,"line":6462},[2994,12193,4387],{"class":3021},[2994,12195,12196],{"class":2996,"line":6468},[2994,12197,3062],{"emptyLinePlaceholder":3061},[2994,12199,12200,12202],{"class":2996,"line":6474},[2994,12201,4043],{"class":3021},[2994,12203,4138],{"class":3187},[2994,12205,12206,12208,12211,12213,12215,12217],{"class":2996,"line":6480},[2994,12207,4143],{"class":3187},[2994,12209,12210],{"class":3014}," save_shouldAcceptAllValidEnumValues_whenIteratingFormats",[2994,12212,6279],{"class":3021},[2994,12214,6282],{"class":3007},[2994,12216,6505],{"class":3187},[2994,12218,4034],{"class":3021},[2994,12220,12221],{"class":2996,"line":6485},[2994,12222,9151],{"class":3000},[2994,12224,12225,12227,12229,12231,12233,12235,12237,12239,12241,12243],{"class":2996,"line":6493},[2994,12226,9156],{"class":3187},[2994,12228,9159],{"class":3191},[2994,12230,3195],{"class":3021},[2994,12232,4073],{"class":3700},[2994,12234,9166],{"class":3014},[2994,12236,3111],{"class":3021},[2994,12238,11438],{"class":3025},[2994,12240,3029],{"class":3021},[2994,12242,11443],{"class":3025},[2994,12244,3055],{"class":3021},[2994,12246,12247,12249,12251,12253,12255,12257,12259,12261],{"class":2996,"line":6510},[2994,12248,11450],{"class":3187},[2994,12250,11453],{"class":3191},[2994,12252,3195],{"class":3021},[2994,12254,4073],{"class":3700},[2994,12256,11460],{"class":3014},[2994,12258,3111],{"class":3021},[2994,12260,11465],{"class":3025},[2994,12262,3055],{"class":3021},[2994,12264,12265,12267,12269,12271],{"class":2996,"line":6536},[2994,12266,11472],{"class":3191},[2994,12268,3217],{"class":3021},[2994,12270,9230],{"class":3014},[2994,12272,9233],{"class":3021},[2994,12274,12275,12277,12279,12281],{"class":2996,"line":6558},[2994,12276,11483],{"class":3191},[2994,12278,3217],{"class":3021},[2994,12280,9230],{"class":3014},[2994,12282,11490],{"class":3021},[2994,12284,12285],{"class":2996,"line":6564},[2994,12286,3062],{"emptyLinePlaceholder":3061},[2994,12288,12289,12291,12293,12295,12297,12299,12301,12303],{"class":2996,"line":6570},[2994,12290,11499],{"class":3187},[2994,12292,11502],{"class":3191},[2994,12294,3195],{"class":3021},[2994,12296,4073],{"class":3700},[2994,12298,11509],{"class":3014},[2994,12300,3111],{"class":3021},[2994,12302,11514],{"class":3025},[2994,12304,11517],{"class":3021},[2994,12306,12307,12309,12311,12313,12315,12317],{"class":2996,"line":6576},[2994,12308,11522],{"class":3191},[2994,12310,3217],{"class":3021},[2994,12312,11527],{"class":3014},[2994,12314,3111],{"class":3021},[2994,12316,11532],{"class":3114},[2994,12318,3055],{"class":3021},[2994,12320,12321,12323,12325,12327,12329,12331],{"class":2996,"line":6594},[2994,12322,11522],{"class":3191},[2994,12324,3217],{"class":3021},[2994,12326,11543],{"class":3014},[2994,12328,3111],{"class":3021},[2994,12330,11548],{"class":3114},[2994,12332,3055],{"class":3021},[2994,12334,12335,12337,12339,12341],{"class":2996,"line":6610},[2994,12336,11555],{"class":3191},[2994,12338,3217],{"class":3021},[2994,12340,9230],{"class":3014},[2994,12342,11562],{"class":3021},[2994,12344,12345],{"class":2996,"line":6626},[2994,12346,3062],{"emptyLinePlaceholder":3061},[2994,12348,12349],{"class":2996,"line":6642},[2994,12350,12351],{"class":3000},"        // Всі валідні значення з ENUM\n",[2994,12353,12354,12356,12358,12361,12364,12366,12368,12371,12373,12376,12378,12381,12383,12386,12388,12391],{"class":2996,"line":6658},[2994,12355,6310],{"class":3187},[2994,12357,6946],{"class":3021},[2994,12359,12360],{"class":3191},"validFormats",[2994,12362,12363],{"class":3021}," = {",[2994,12365,3230],{"class":3025},[2994,12367,3029],{"class":3021},[2994,12369,12370],{"class":3025},"\"ogg\"",[2994,12372,3029],{"class":3021},[2994,12374,12375],{"class":3025},"\"wav\"",[2994,12377,3029],{"class":3021},[2994,12379,12380],{"class":3025},"\"m4b\"",[2994,12382,3029],{"class":3021},[2994,12384,12385],{"class":3025},"\"aac\"",[2994,12387,3029],{"class":3021},[2994,12389,12390],{"class":3025},"\"flac\"",[2994,12392,12393],{"class":3021},"};\n",[2994,12395,12396],{"class":2996,"line":6674},[2994,12397,3062],{"emptyLinePlaceholder":3061},[2994,12399,12400],{"class":2996,"line":6690},[2994,12401,9722],{"class":3000},[2994,12403,12404,12407,12409,12411,12414,12416],{"class":2996,"line":6706},[2994,12405,12406],{"class":3700},"        for",[2994,12408,3769],{"class":3021},[2994,12410,3188],{"class":3187},[2994,12412,12413],{"class":3191}," format",[2994,12415,6981],{"class":3700},[2994,12417,12418],{"class":3021}," validFormats) {\n",[2994,12420,12421,12424,12426,12428,12430,12432],{"class":2996,"line":6712},[2994,12422,12423],{"class":3187},"            AudiobookFile",[2994,12425,11579],{"class":3191},[2994,12427,3195],{"class":3021},[2994,12429,4073],{"class":3700},[2994,12431,11586],{"class":3014},[2994,12433,4382],{"class":3021},[2994,12435,12436,12439,12441,12443,12445,12447,12449,12451],{"class":2996,"line":6717},[2994,12437,12438],{"class":3191},"            file",[2994,12440,3217],{"class":3021},[2994,12442,9657],{"class":3014},[2994,12444,3111],{"class":3021},[2994,12446,9611],{"class":3191},[2994,12448,3217],{"class":3021},[2994,12450,9616],{"class":3014},[2994,12452,9916],{"class":3021},[2994,12454,12455,12457,12459,12461,12463,12465,12467,12469],{"class":2996,"line":6722},[2994,12456,12438],{"class":3191},[2994,12458,3217],{"class":3021},[2994,12460,11616],{"class":3014},[2994,12462,3111],{"class":3021},[2994,12464,11621],{"class":3191},[2994,12466,3217],{"class":3021},[2994,12468,9257],{"class":3014},[2994,12470,9916],{"class":3021},[2994,12472,12473,12475,12477,12479,12481,12484],{"class":2996,"line":6727},[2994,12474,12438],{"class":3191},[2994,12476,3217],{"class":3021},[2994,12478,11636],{"class":3014},[2994,12480,3111],{"class":3021},[2994,12482,12483],{"class":3025},"\"/files/book.\"",[2994,12485,12486],{"class":3021}," + format);\n",[2994,12488,12489,12491,12493,12495],{"class":2996,"line":6733},[2994,12490,12438],{"class":3191},[2994,12492,3217],{"class":3021},[2994,12494,11652],{"class":3014},[2994,12496,12497],{"class":3021},"(format);\n",[2994,12499,12500,12502,12504,12506,12508,12510],{"class":2996,"line":6739},[2994,12501,12438],{"class":3191},[2994,12503,3217],{"class":3021},[2994,12505,11670],{"class":3014},[2994,12507,3111],{"class":3021},[2994,12509,12133],{"class":3114},[2994,12511,3055],{"class":3021},[2994,12513,12514],{"class":2996,"line":6744},[2994,12515,3062],{"emptyLinePlaceholder":3061},[2994,12517,12518],{"class":2996,"line":6752},[2994,12519,12520],{"class":3000},"            // Має пройти без виключень\n",[2994,12522,12523,12526,12528,12530,12532,12534,12536],{"class":2996,"line":6762},[2994,12524,12525],{"class":3014},"            assertThatCode",[2994,12527,9730],{"class":3021},[2994,12529,3385],{"class":3007},[2994,12531,11280],{"class":3191},[2994,12533,3217],{"class":3021},[2994,12535,9230],{"class":3014},[2994,12537,12538],{"class":3021},"(file))\n",[2994,12540,12541,12544,12547],{"class":2996,"line":6768},[2994,12542,12543],{"class":3021},"                .",[2994,12545,12546],{"class":3014},"doesNotThrowAnyException",[2994,12548,4382],{"class":3021},[2994,12550,12551],{"class":2996,"line":6774},[2994,12552,6709],{"class":3021},[2994,12554,12555],{"class":2996,"line":6779},[2994,12556,3062],{"emptyLinePlaceholder":3061},[2994,12558,12559],{"class":2996,"line":6784},[2994,12560,12561],{"class":3000},"        // Перевірка: у БД має бути 6 файлів\n",[2994,12563,12564,12566,12568,12570,12572,12575,12577,12579,12581,12584],{"class":2996,"line":6790},[2994,12565,9247],{"class":3014},[2994,12567,3111],{"class":3021},[2994,12569,9408],{"class":3014},[2994,12571,3111],{"class":3021},[2994,12573,12574],{"class":3025},"\"audiobook_files\"",[2994,12576,9416],{"class":3021},[2994,12578,9320],{"class":3014},[2994,12580,3111],{"class":3021},[2994,12582,12583],{"class":3114},"6",[2994,12585,3055],{"class":3021},[2994,12587,12588],{"class":2996,"line":6796},[2994,12589,4387],{"class":3021},[2994,12591,12592],{"class":2996,"line":6801},[2994,12593,4183],{"class":3021},[2964,12595,12596],{},[2971,12597,12598],{},"Ключові моменти:",[4203,12600,12601,12621,12637],{},[3962,12602,12603,3769,12606,12609,12610,12612,12613,3356,12615,12617,12618,3217],{},[2971,12604,12605],{},"Рядки 95–113",[2991,12607,12608],{},"save_shouldThrowException_whenInvalidEnumFormat","): Цей тест ",[2971,12611,11076],{}," виконати на H2. H2 з ",[2991,12614,3108],{},[2991,12616,3163],{}," constraint поверне загальне повідомлення про порушення constraint, а не специфічне для ENUM. PostgreSQL чітко повідомляє: ",[2991,12619,12620],{},"\"invalid input value for enum file_format_enum: \"xyz\"\"",[3962,12622,12623,3769,12626,12629,12630,12632,12633,12636],{},[2971,12624,12625],{},"Рядок 99",[2991,12627,12628],{},"?::file_format_enum","): Явне приведення типу у PostgreSQL. Це необхідно, оскільки JDBC передає параметр як ",[2991,12631,3108],{},", а PostgreSQL вимагає ENUM. Без ",[2991,12634,12635],{},"::file_format_enum"," PostgreSQL поверне помилку типу.",[3962,12638,12639,3769,12642,12645,12646,12649],{},[2971,12640,12641],{},"Рядки 115–148",[2991,12643,12644],{},"save_shouldAcceptAllValidEnumValues","): Тест перевіряє, що ",[2971,12647,12648],{},"всі"," значення з ENUM приймаються БД. Це гарантує, що DDL-скрипт і Java-код синхронізовані.",[4762,12651,12652,12657,12667,12762],{},[2964,12653,12654],{},[2971,12655,12656],{},"Best practice: Java enum для PostgreSQL ENUM",[2964,12658,12659,12660,12663,12664,3563],{},"Замість ",[2991,12661,12662],{},"String format"," у доменній моделі, використовуйте Java ",[2991,12665,12666],{},"enum",[2984,12668,12670],{"className":3173,"code":12669,"language":3175,"meta":2989,"style":2989},"public enum AudioFileFormat {\n    MP3, OGG, WAV, M4B, AAC, FLAC;\n    \n    public String toPostgresEnum() {\n        return name().toLowerCase(); // MP3 → \"mp3\"\n    }\n}\n\npublic class AudiobookFile {\n    private AudioFileFormat format; // замість String\n}\n",[2991,12671,12672,12684,12689,12693,12705,12723,12727,12731,12735,12745,12758],{"__ignoreMap":2989},[2994,12673,12674,12676,12679,12682],{"class":2996,"line":2997},[2994,12675,4246],{"class":3007},[2994,12677,12678],{"class":3007}," enum",[2994,12680,12681],{"class":3187}," AudioFileFormat",[2994,12683,4034],{"class":3021},[2994,12685,12686],{"class":2996,"line":3004},[2994,12687,12688],{"class":3021},"    MP3, OGG, WAV, M4B, AAC, FLAC;\n",[2994,12690,12691],{"class":2996,"line":3058},[2994,12692,8040],{"class":3021},[2994,12694,12695,12698,12700,12703],{"class":2996,"line":3065},[2994,12696,12697],{"class":3007},"    public",[2994,12699,4401],{"class":3187},[2994,12701,12702],{"class":3014}," toPostgresEnum",[2994,12704,4407],{"class":3021},[2994,12706,12707,12709,12712,12714,12717,12720],{"class":2996,"line":3079},[2994,12708,4412],{"class":3700},[2994,12710,12711],{"class":3014}," name",[2994,12713,7221],{"class":3021},[2994,12715,12716],{"class":3014},"toLowerCase",[2994,12718,12719],{"class":3021},"(); ",[2994,12721,12722],{"class":3000},"// MP3 → \"mp3\"\n",[2994,12724,12725],{"class":2996,"line":3091},[2994,12726,4387],{"class":3021},[2994,12728,12729],{"class":2996,"line":3102},[2994,12730,4183],{"class":3021},[2994,12732,12733],{"class":2996,"line":3125},[2994,12734,3062],{"emptyLinePlaceholder":3061},[2994,12736,12737,12739,12741,12743],{"class":2996,"line":3139},[2994,12738,4246],{"class":3007},[2994,12740,4252],{"class":3007},[2994,12742,11586],{"class":3187},[2994,12744,4034],{"class":3021},[2994,12746,12747,12749,12751,12753,12755],{"class":2996,"line":3148},[2994,12748,4266],{"class":3007},[2994,12750,12681],{"class":3187},[2994,12752,12413],{"class":3191},[2994,12754,3735],{"class":3021},[2994,12756,12757],{"class":3000},"// замість String\n",[2994,12759,12760],{"class":2996,"line":3845},[2994,12761,4183],{"class":3021},[2964,12763,12764,12765,12768],{},"Це дає ",[2971,12766,12767],{},"type safety"," на рівні Java і гарантує, що невалідне значення не потрапить до БД.",[3307,12770],{},[2976,12772,12774],{"id":12773},"тестування-каскадного-видалення-on-delete-cascade","Тестування каскадного видалення (ON DELETE CASCADE)",[2964,12776,12777],{},"PostgreSQL підтримує складні каскадні операції через кілька рівнів FK. Розглянемо ланцюжок:",[2984,12779,12782],{"className":12780,"code":12781,"language":4194},[4192],"Author → Audiobook → AudiobookFile\n       → Audiobook → AudiobookCollection → Collection\n",[2991,12783,12781],{"__ignoreMap":2989},[2964,12785,12786],{},"Видалення автора має каскадно видалити:",[3959,12788,12789,12792,12795],{},[3962,12790,12791],{},"Всі його аудіокниги",[3962,12793,12794],{},"Всі файли цих аудіокниг",[3962,12796,12797],{},"Всі зв'язки цих аудіокниг з колекціями",[2984,12799,12801],{"className":3173,"code":12800,"language":3175,"meta":5001,"style":2989},"@Test\nvoid deleteAuthor_shouldCascadeDeleteAudiobooksAndFiles_whenAuthorHasComplexRelations() \n        throws SQLException {\n    // ═══ Arrange ═══\n    // Створити автора\n    Author author = new Author(\"Тарас\", \"Шевченко\");\n    authorRepo.save(author);\n\n    // Створити жанр\n    Genre genre = new Genre(\"Поезія\");\n    genreRepo.save(genre);\n\n    // Створити 2 аудіокниги цього автора\n    Audiobook book1 = new Audiobook(\"Кобзар\", author, genre);\n    book1.setDuration(10000);\n    book1.setReleaseYear(1840);\n    audiobookRepo.save(book1);\n\n    Audiobook book2 = new Audiobook(\"Гайдамаки\", author, genre);\n    book2.setDuration(8000);\n    book2.setReleaseYear(1841);\n    audiobookRepo.save(book2);\n\n    // Створити файли для кожної книги\n    AudiobookFile file1 = new AudiobookFile();\n    file1.setId(UUID.randomUUID());\n    file1.setAudiobookId(book1.getId());\n    file1.setFilePath(\"/files/kobzar.mp3\");\n    file1.setFormat(\"mp3\");\n    fileRepo.save(file1);\n\n    AudiobookFile file2 = new AudiobookFile();\n    file2.setId(UUID.randomUUID());\n    file2.setAudiobookId(book2.getId());\n    file2.setFilePath(\"/files/haidamaky.mp3\");\n    file2.setFormat(\"mp3\");\n    fileRepo.save(file2);\n\n    // Створити користувача та колекцію\n    User user = new User(\"testuser\", \"hash\");\n    userRepo.save(user);\n\n    Collection collection = new Collection(user, \"Українська класика\");\n    collectionRepo.save(collection);\n\n    // Додати книги до колекції\n    collectionRepo.addAudiobook(collection.getId(), book1.getId());\n    collectionRepo.addAudiobook(collection.getId(), book2.getId());\n\n    // Перевірка початкового стану\n    assertThat(countRowsInTable(\"authors\")).isEqualTo(1);\n    assertThat(countRowsInTable(\"audiobooks\")).isEqualTo(2);\n    assertThat(countRowsInTable(\"audiobook_files\")).isEqualTo(2);\n    assertThat(countRowsInTable(\"audiobook_collection\")).isEqualTo(2);\n\n    // ═══ Act ═══\n    // Видалити автора → має каскадно видалити ВСЕ\n    authorRepo.deleteById(author.getId());\n\n    // ═══ Assert ═══\n    // Автор видалений\n    assertThat(authorRepo.findById(author.getId())).isEmpty();\n    \n    // Аудіокниги видалені (ON DELETE CASCADE з authors)\n    assertThat(audiobookRepo.findById(book1.getId())).isEmpty();\n    assertThat(audiobookRepo.findById(book2.getId())).isEmpty();\n    assertThat(countRowsInTable(\"audiobooks\")).isEqualTo(0);\n    \n    // Файли видалені (ON DELETE CASCADE з audiobooks)\n    assertThat(fileRepo.findById(file1.getId())).isEmpty();\n    assertThat(fileRepo.findById(file2.getId())).isEmpty();\n    assertThat(countRowsInTable(\"audiobook_files\")).isEqualTo(0);\n    \n    // Зв'язки з колекціями видалені (ON DELETE CASCADE з audiobooks)\n    assertThat(countRowsInTable(\"audiobook_collection\")).isEqualTo(0);\n    \n    // Але колекція та користувач залишилися (немає FK на автора)\n    assertThat(collectionRepo.findById(collection.getId())).isPresent();\n    assertThat(userRepo.findById(user.getId())).isPresent();\n}\n",[2991,12802,12803,12809,12820,12825,12830,12835,12858,12869,12873,12878,12898,12909,12913,12918,12939,12955,12970,12982,12986,13006,13022,13037,13048,13052,13057,13073,13092,13111,13126,13140,13152,13156,13171,13190,13209,13224,13238,13249,13253,13258,13285,13297,13301,13324,13336,13340,13345,13374,13400,13404,13409,13432,13455,13477,13500,13504,13509,13514,13532,13536,13541,13546,13574,13578,13583,13610,13636,13658,13662,13667,13694,13721,13743,13747,13752,13774,13778,13783,13810,13838],{"__ignoreMap":2989},[2994,12804,12805,12807],{"class":2996,"line":2997},[2994,12806,4018],{"class":3021},[2994,12808,4138],{"class":3187},[2994,12810,12811,12814,12817],{"class":2996,"line":3004},[2994,12812,12813],{"class":3187},"void",[2994,12815,12816],{"class":3014}," deleteAuthor_shouldCascadeDeleteAudiobooksAndFiles_whenAuthorHasComplexRelations",[2994,12818,12819],{"class":3021},"() \n",[2994,12821,12822],{"class":2996,"line":3058},[2994,12823,12824],{"class":3021},"        throws SQLException {\n",[2994,12826,12827],{"class":2996,"line":3065},[2994,12828,12829],{"class":3000},"    // ═══ Arrange ═══\n",[2994,12831,12832],{"class":2996,"line":3079},[2994,12833,12834],{"class":3000},"    // Створити автора\n",[2994,12836,12837,12840,12842,12844,12846,12848,12850,12852,12854,12856],{"class":2996,"line":3091},[2994,12838,12839],{"class":3187},"    Author",[2994,12841,9159],{"class":3191},[2994,12843,3195],{"class":3021},[2994,12845,4073],{"class":3700},[2994,12847,9166],{"class":3014},[2994,12849,3111],{"class":3021},[2994,12851,9640],{"class":3025},[2994,12853,3029],{"class":3021},[2994,12855,9645],{"class":3025},[2994,12857,3055],{"class":3021},[2994,12859,12860,12863,12865,12867],{"class":2996,"line":3102},[2994,12861,12862],{"class":3191},"    authorRepo",[2994,12864,3217],{"class":3021},[2994,12866,9230],{"class":3014},[2994,12868,9233],{"class":3021},[2994,12870,12871],{"class":2996,"line":3125},[2994,12872,3062],{"emptyLinePlaceholder":3061},[2994,12874,12875],{"class":2996,"line":3139},[2994,12876,12877],{"class":3000},"    // Створити жанр\n",[2994,12879,12880,12883,12885,12887,12889,12891,12893,12896],{"class":2996,"line":3148},[2994,12881,12882],{"class":3187},"    Genre",[2994,12884,11453],{"class":3191},[2994,12886,3195],{"class":3021},[2994,12888,4073],{"class":3700},[2994,12890,11460],{"class":3014},[2994,12892,3111],{"class":3021},[2994,12894,12895],{"class":3025},"\"Поезія\"",[2994,12897,3055],{"class":3021},[2994,12899,12900,12903,12905,12907],{"class":2996,"line":3845},[2994,12901,12902],{"class":3191},"    genreRepo",[2994,12904,3217],{"class":3021},[2994,12906,9230],{"class":3014},[2994,12908,11490],{"class":3021},[2994,12910,12911],{"class":2996,"line":3851},[2994,12912,3062],{"emptyLinePlaceholder":3061},[2994,12914,12915],{"class":2996,"line":3856},[2994,12916,12917],{"class":3000},"    // Створити 2 аудіокниги цього автора\n",[2994,12919,12920,12923,12926,12928,12930,12932,12934,12937],{"class":2996,"line":3861},[2994,12921,12922],{"class":3187},"    Audiobook",[2994,12924,12925],{"class":3191}," book1",[2994,12927,3195],{"class":3021},[2994,12929,4073],{"class":3700},[2994,12931,11509],{"class":3014},[2994,12933,3111],{"class":3021},[2994,12935,12936],{"class":3025},"\"Кобзар\"",[2994,12938,11517],{"class":3021},[2994,12940,12941,12944,12946,12948,12950,12953],{"class":2996,"line":3867},[2994,12942,12943],{"class":3191},"    book1",[2994,12945,3217],{"class":3021},[2994,12947,11527],{"class":3014},[2994,12949,3111],{"class":3021},[2994,12951,12952],{"class":3114},"10000",[2994,12954,3055],{"class":3021},[2994,12956,12957,12959,12961,12963,12965,12968],{"class":2996,"line":3873},[2994,12958,12943],{"class":3191},[2994,12960,3217],{"class":3021},[2994,12962,11543],{"class":3014},[2994,12964,3111],{"class":3021},[2994,12966,12967],{"class":3114},"1840",[2994,12969,3055],{"class":3021},[2994,12971,12972,12975,12977,12979],{"class":2996,"line":3879},[2994,12973,12974],{"class":3191},"    audiobookRepo",[2994,12976,3217],{"class":3021},[2994,12978,9230],{"class":3014},[2994,12980,12981],{"class":3021},"(book1);\n",[2994,12983,12984],{"class":2996,"line":3885},[2994,12985,3062],{"emptyLinePlaceholder":3061},[2994,12987,12988,12990,12993,12995,12997,12999,13001,13004],{"class":2996,"line":3891},[2994,12989,12922],{"class":3187},[2994,12991,12992],{"class":3191}," book2",[2994,12994,3195],{"class":3021},[2994,12996,4073],{"class":3700},[2994,12998,11509],{"class":3014},[2994,13000,3111],{"class":3021},[2994,13002,13003],{"class":3025},"\"Гайдамаки\"",[2994,13005,11517],{"class":3021},[2994,13007,13008,13011,13013,13015,13017,13020],{"class":2996,"line":3897},[2994,13009,13010],{"class":3191},"    book2",[2994,13012,3217],{"class":3021},[2994,13014,11527],{"class":3014},[2994,13016,3111],{"class":3021},[2994,13018,13019],{"class":3114},"8000",[2994,13021,3055],{"class":3021},[2994,13023,13024,13026,13028,13030,13032,13035],{"class":2996,"line":3903},[2994,13025,13010],{"class":3191},[2994,13027,3217],{"class":3021},[2994,13029,11543],{"class":3014},[2994,13031,3111],{"class":3021},[2994,13033,13034],{"class":3114},"1841",[2994,13036,3055],{"class":3021},[2994,13038,13039,13041,13043,13045],{"class":2996,"line":3908},[2994,13040,12974],{"class":3191},[2994,13042,3217],{"class":3021},[2994,13044,9230],{"class":3014},[2994,13046,13047],{"class":3021},"(book2);\n",[2994,13049,13050],{"class":2996,"line":3914},[2994,13051,3062],{"emptyLinePlaceholder":3061},[2994,13053,13054],{"class":2996,"line":3920},[2994,13055,13056],{"class":3000},"    // Створити файли для кожної книги\n",[2994,13058,13059,13062,13065,13067,13069,13071],{"class":2996,"line":3926},[2994,13060,13061],{"class":3187},"    AudiobookFile",[2994,13063,13064],{"class":3191}," file1",[2994,13066,3195],{"class":3021},[2994,13068,4073],{"class":3700},[2994,13070,11586],{"class":3014},[2994,13072,4382],{"class":3021},[2994,13074,13075,13078,13080,13082,13084,13086,13088,13090],{"class":2996,"line":3931},[2994,13076,13077],{"class":3191},"    file1",[2994,13079,3217],{"class":3021},[2994,13081,9657],{"class":3014},[2994,13083,3111],{"class":3021},[2994,13085,9611],{"class":3191},[2994,13087,3217],{"class":3021},[2994,13089,9616],{"class":3014},[2994,13091,9916],{"class":3021},[2994,13093,13094,13096,13098,13100,13102,13105,13107,13109],{"class":2996,"line":3937},[2994,13095,13077],{"class":3191},[2994,13097,3217],{"class":3021},[2994,13099,11616],{"class":3014},[2994,13101,3111],{"class":3021},[2994,13103,13104],{"class":3191},"book1",[2994,13106,3217],{"class":3021},[2994,13108,9257],{"class":3014},[2994,13110,9916],{"class":3021},[2994,13112,13113,13115,13117,13119,13121,13124],{"class":2996,"line":3943},[2994,13114,13077],{"class":3191},[2994,13116,3217],{"class":3021},[2994,13118,11636],{"class":3014},[2994,13120,3111],{"class":3021},[2994,13122,13123],{"class":3025},"\"/files/kobzar.mp3\"",[2994,13125,3055],{"class":3021},[2994,13127,13128,13130,13132,13134,13136,13138],{"class":2996,"line":3949},[2994,13129,13077],{"class":3191},[2994,13131,3217],{"class":3021},[2994,13133,11652],{"class":3014},[2994,13135,3111],{"class":3021},[2994,13137,3230],{"class":3025},[2994,13139,3055],{"class":3021},[2994,13141,13142,13145,13147,13149],{"class":2996,"line":5348},[2994,13143,13144],{"class":3191},"    fileRepo",[2994,13146,3217],{"class":3021},[2994,13148,9230],{"class":3014},[2994,13150,13151],{"class":3021},"(file1);\n",[2994,13153,13154],{"class":2996,"line":5365},[2994,13155,3062],{"emptyLinePlaceholder":3061},[2994,13157,13158,13160,13163,13165,13167,13169],{"class":2996,"line":5382},[2994,13159,13061],{"class":3187},[2994,13161,13162],{"class":3191}," file2",[2994,13164,3195],{"class":3021},[2994,13166,4073],{"class":3700},[2994,13168,11586],{"class":3014},[2994,13170,4382],{"class":3021},[2994,13172,13173,13176,13178,13180,13182,13184,13186,13188],{"class":2996,"line":5391},[2994,13174,13175],{"class":3191},"    file2",[2994,13177,3217],{"class":3021},[2994,13179,9657],{"class":3014},[2994,13181,3111],{"class":3021},[2994,13183,9611],{"class":3191},[2994,13185,3217],{"class":3021},[2994,13187,9616],{"class":3014},[2994,13189,9916],{"class":3021},[2994,13191,13192,13194,13196,13198,13200,13203,13205,13207],{"class":2996,"line":5396},[2994,13193,13175],{"class":3191},[2994,13195,3217],{"class":3021},[2994,13197,11616],{"class":3014},[2994,13199,3111],{"class":3021},[2994,13201,13202],{"class":3191},"book2",[2994,13204,3217],{"class":3021},[2994,13206,9257],{"class":3014},[2994,13208,9916],{"class":3021},[2994,13210,13211,13213,13215,13217,13219,13222],{"class":2996,"line":5402},[2994,13212,13175],{"class":3191},[2994,13214,3217],{"class":3021},[2994,13216,11636],{"class":3014},[2994,13218,3111],{"class":3021},[2994,13220,13221],{"class":3025},"\"/files/haidamaky.mp3\"",[2994,13223,3055],{"class":3021},[2994,13225,13226,13228,13230,13232,13234,13236],{"class":2996,"line":5411},[2994,13227,13175],{"class":3191},[2994,13229,3217],{"class":3021},[2994,13231,11652],{"class":3014},[2994,13233,3111],{"class":3021},[2994,13235,3230],{"class":3025},[2994,13237,3055],{"class":3021},[2994,13239,13240,13242,13244,13246],{"class":2996,"line":5429},[2994,13241,13144],{"class":3191},[2994,13243,3217],{"class":3021},[2994,13245,9230],{"class":3014},[2994,13247,13248],{"class":3021},"(file2);\n",[2994,13250,13251],{"class":2996,"line":5446},[2994,13252,3062],{"emptyLinePlaceholder":3061},[2994,13254,13255],{"class":2996,"line":5464},[2994,13256,13257],{"class":3000},"    // Створити користувача та колекцію\n",[2994,13259,13260,13263,13266,13268,13270,13273,13275,13278,13280,13283],{"class":2996,"line":5473},[2994,13261,13262],{"class":3187},"    User",[2994,13264,13265],{"class":3191}," user",[2994,13267,3195],{"class":3021},[2994,13269,4073],{"class":3700},[2994,13271,13272],{"class":3014}," User",[2994,13274,3111],{"class":3021},[2994,13276,13277],{"class":3025},"\"testuser\"",[2994,13279,3029],{"class":3021},[2994,13281,13282],{"class":3025},"\"hash\"",[2994,13284,3055],{"class":3021},[2994,13286,13287,13290,13292,13294],{"class":2996,"line":5478},[2994,13288,13289],{"class":3191},"    userRepo",[2994,13291,3217],{"class":3021},[2994,13293,9230],{"class":3014},[2994,13295,13296],{"class":3021},"(user);\n",[2994,13298,13299],{"class":2996,"line":5484},[2994,13300,3062],{"emptyLinePlaceholder":3061},[2994,13302,13303,13306,13309,13311,13313,13316,13319,13322],{"class":2996,"line":5493},[2994,13304,13305],{"class":3187},"    Collection",[2994,13307,13308],{"class":3191}," collection",[2994,13310,3195],{"class":3021},[2994,13312,4073],{"class":3700},[2994,13314,13315],{"class":3014}," Collection",[2994,13317,13318],{"class":3021},"(user, ",[2994,13320,13321],{"class":3025},"\"Українська класика\"",[2994,13323,3055],{"class":3021},[2994,13325,13326,13329,13331,13333],{"class":2996,"line":5511},[2994,13327,13328],{"class":3191},"    collectionRepo",[2994,13330,3217],{"class":3021},[2994,13332,9230],{"class":3014},[2994,13334,13335],{"class":3021},"(collection);\n",[2994,13337,13338],{"class":2996,"line":5529},[2994,13339,3062],{"emptyLinePlaceholder":3061},[2994,13341,13342],{"class":2996,"line":5547},[2994,13343,13344],{"class":3000},"    // Додати книги до колекції\n",[2994,13346,13347,13349,13351,13354,13356,13359,13361,13363,13366,13368,13370,13372],{"class":2996,"line":5564},[2994,13348,13328],{"class":3191},[2994,13350,3217],{"class":3021},[2994,13352,13353],{"class":3014},"addAudiobook",[2994,13355,3111],{"class":3021},[2994,13357,13358],{"class":3191},"collection",[2994,13360,3217],{"class":3021},[2994,13362,9257],{"class":3014},[2994,13364,13365],{"class":3021},"(), ",[2994,13367,13104],{"class":3191},[2994,13369,3217],{"class":3021},[2994,13371,9257],{"class":3014},[2994,13373,9916],{"class":3021},[2994,13375,13376,13378,13380,13382,13384,13386,13388,13390,13392,13394,13396,13398],{"class":2996,"line":5573},[2994,13377,13328],{"class":3191},[2994,13379,3217],{"class":3021},[2994,13381,13353],{"class":3014},[2994,13383,3111],{"class":3021},[2994,13385,13358],{"class":3191},[2994,13387,3217],{"class":3021},[2994,13389,9257],{"class":3014},[2994,13391,13365],{"class":3021},[2994,13393,13202],{"class":3191},[2994,13395,3217],{"class":3021},[2994,13397,9257],{"class":3014},[2994,13399,9916],{"class":3021},[2994,13401,13402],{"class":2996,"line":5939},[2994,13403,3062],{"emptyLinePlaceholder":3061},[2994,13405,13406],{"class":2996,"line":5944},[2994,13407,13408],{"class":3000},"    // Перевірка початкового стану\n",[2994,13410,13411,13414,13416,13418,13420,13422,13424,13426,13428,13430],{"class":2996,"line":5950},[2994,13412,13413],{"class":3014},"    assertThat",[2994,13415,3111],{"class":3021},[2994,13417,9408],{"class":3014},[2994,13419,3111],{"class":3021},[2994,13421,9413],{"class":3025},[2994,13423,9416],{"class":3021},[2994,13425,9320],{"class":3014},[2994,13427,3111],{"class":3021},[2994,13429,7409],{"class":3114},[2994,13431,3055],{"class":3021},[2994,13433,13434,13436,13438,13440,13442,13445,13447,13449,13451,13453],{"class":2996,"line":5956},[2994,13435,13413],{"class":3014},[2994,13437,3111],{"class":3021},[2994,13439,9408],{"class":3014},[2994,13441,3111],{"class":3021},[2994,13443,13444],{"class":3025},"\"audiobooks\"",[2994,13446,9416],{"class":3021},[2994,13448,9320],{"class":3014},[2994,13450,3111],{"class":3021},[2994,13452,10750],{"class":3114},[2994,13454,3055],{"class":3021},[2994,13456,13457,13459,13461,13463,13465,13467,13469,13471,13473,13475],{"class":2996,"line":5962},[2994,13458,13413],{"class":3014},[2994,13460,3111],{"class":3021},[2994,13462,9408],{"class":3014},[2994,13464,3111],{"class":3021},[2994,13466,12574],{"class":3025},[2994,13468,9416],{"class":3021},[2994,13470,9320],{"class":3014},[2994,13472,3111],{"class":3021},[2994,13474,10750],{"class":3114},[2994,13476,3055],{"class":3021},[2994,13478,13479,13481,13483,13485,13487,13490,13492,13494,13496,13498],{"class":2996,"line":5968},[2994,13480,13413],{"class":3014},[2994,13482,3111],{"class":3021},[2994,13484,9408],{"class":3014},[2994,13486,3111],{"class":3021},[2994,13488,13489],{"class":3025},"\"audiobook_collection\"",[2994,13491,9416],{"class":3021},[2994,13493,9320],{"class":3014},[2994,13495,3111],{"class":3021},[2994,13497,10750],{"class":3114},[2994,13499,3055],{"class":3021},[2994,13501,13502],{"class":2996,"line":5978},[2994,13503,3062],{"emptyLinePlaceholder":3061},[2994,13505,13506],{"class":2996,"line":5992},[2994,13507,13508],{"class":3000},"    // ═══ Act ═══\n",[2994,13510,13511],{"class":2996,"line":5997},[2994,13512,13513],{"class":3000},"    // Видалити автора → має каскадно видалити ВСЕ\n",[2994,13515,13516,13518,13520,13522,13524,13526,13528,13530],{"class":2996,"line":6003},[2994,13517,12862],{"class":3191},[2994,13519,3217],{"class":3021},[2994,13521,10485],{"class":3014},[2994,13523,3111],{"class":3021},[2994,13525,9252],{"class":3191},[2994,13527,3217],{"class":3021},[2994,13529,9257],{"class":3014},[2994,13531,9916],{"class":3021},[2994,13533,13534],{"class":2996,"line":6009},[2994,13535,3062],{"emptyLinePlaceholder":3061},[2994,13537,13538],{"class":2996,"line":6015},[2994,13539,13540],{"class":3000},"    // ═══ Assert ═══\n",[2994,13542,13543],{"class":2996,"line":6021},[2994,13544,13545],{"class":3000},"    // Автор видалений\n",[2994,13547,13548,13550,13552,13555,13557,13559,13561,13563,13565,13567,13570,13572],{"class":2996,"line":6027},[2994,13549,13413],{"class":3014},[2994,13551,3111],{"class":3021},[2994,13553,13554],{"class":3191},"authorRepo",[2994,13556,3217],{"class":3021},[2994,13558,9286],{"class":3014},[2994,13560,3111],{"class":3021},[2994,13562,9252],{"class":3191},[2994,13564,3217],{"class":3021},[2994,13566,9257],{"class":3014},[2994,13568,13569],{"class":3021},"())).",[2994,13571,7022],{"class":3014},[2994,13573,4382],{"class":3021},[2994,13575,13576],{"class":2996,"line":6032},[2994,13577,8040],{"class":3021},[2994,13579,13580],{"class":2996,"line":6038},[2994,13581,13582],{"class":3000},"    // Аудіокниги видалені (ON DELETE CASCADE з authors)\n",[2994,13584,13585,13587,13589,13592,13594,13596,13598,13600,13602,13604,13606,13608],{"class":2996,"line":6044},[2994,13586,13413],{"class":3014},[2994,13588,3111],{"class":3021},[2994,13590,13591],{"class":3191},"audiobookRepo",[2994,13593,3217],{"class":3021},[2994,13595,9286],{"class":3014},[2994,13597,3111],{"class":3021},[2994,13599,13104],{"class":3191},[2994,13601,3217],{"class":3021},[2994,13603,9257],{"class":3014},[2994,13605,13569],{"class":3021},[2994,13607,7022],{"class":3014},[2994,13609,4382],{"class":3021},[2994,13611,13612,13614,13616,13618,13620,13622,13624,13626,13628,13630,13632,13634],{"class":2996,"line":6049},[2994,13613,13413],{"class":3014},[2994,13615,3111],{"class":3021},[2994,13617,13591],{"class":3191},[2994,13619,3217],{"class":3021},[2994,13621,9286],{"class":3014},[2994,13623,3111],{"class":3021},[2994,13625,13202],{"class":3191},[2994,13627,3217],{"class":3021},[2994,13629,9257],{"class":3014},[2994,13631,13569],{"class":3021},[2994,13633,7022],{"class":3014},[2994,13635,4382],{"class":3021},[2994,13637,13638,13640,13642,13644,13646,13648,13650,13652,13654,13656],{"class":2996,"line":6055},[2994,13639,13413],{"class":3014},[2994,13641,3111],{"class":3021},[2994,13643,9408],{"class":3014},[2994,13645,3111],{"class":3021},[2994,13647,13444],{"class":3025},[2994,13649,9416],{"class":3021},[2994,13651,9320],{"class":3014},[2994,13653,3111],{"class":3021},[2994,13655,7968],{"class":3114},[2994,13657,3055],{"class":3021},[2994,13659,13660],{"class":2996,"line":6061},[2994,13661,8040],{"class":3021},[2994,13663,13664],{"class":2996,"line":6067},[2994,13665,13666],{"class":3000},"    // Файли видалені (ON DELETE CASCADE з audiobooks)\n",[2994,13668,13669,13671,13673,13675,13677,13679,13681,13684,13686,13688,13690,13692],{"class":2996,"line":6075},[2994,13670,13413],{"class":3014},[2994,13672,3111],{"class":3021},[2994,13674,11719],{"class":3191},[2994,13676,3217],{"class":3021},[2994,13678,9286],{"class":3014},[2994,13680,3111],{"class":3021},[2994,13682,13683],{"class":3191},"file1",[2994,13685,3217],{"class":3021},[2994,13687,9257],{"class":3014},[2994,13689,13569],{"class":3021},[2994,13691,7022],{"class":3014},[2994,13693,4382],{"class":3021},[2994,13695,13696,13698,13700,13702,13704,13706,13708,13711,13713,13715,13717,13719],{"class":2996,"line":6097},[2994,13697,13413],{"class":3014},[2994,13699,3111],{"class":3021},[2994,13701,11719],{"class":3191},[2994,13703,3217],{"class":3021},[2994,13705,9286],{"class":3014},[2994,13707,3111],{"class":3021},[2994,13709,13710],{"class":3191},"file2",[2994,13712,3217],{"class":3021},[2994,13714,9257],{"class":3014},[2994,13716,13569],{"class":3021},[2994,13718,7022],{"class":3014},[2994,13720,4382],{"class":3021},[2994,13722,13723,13725,13727,13729,13731,13733,13735,13737,13739,13741],{"class":2996,"line":6122},[2994,13724,13413],{"class":3014},[2994,13726,3111],{"class":3021},[2994,13728,9408],{"class":3014},[2994,13730,3111],{"class":3021},[2994,13732,12574],{"class":3025},[2994,13734,9416],{"class":3021},[2994,13736,9320],{"class":3014},[2994,13738,3111],{"class":3021},[2994,13740,7968],{"class":3114},[2994,13742,3055],{"class":3021},[2994,13744,13745],{"class":2996,"line":6136},[2994,13746,8040],{"class":3021},[2994,13748,13749],{"class":2996,"line":6149},[2994,13750,13751],{"class":3000},"    // Зв'язки з колекціями видалені (ON DELETE CASCADE з audiobooks)\n",[2994,13753,13754,13756,13758,13760,13762,13764,13766,13768,13770,13772],{"class":2996,"line":6163},[2994,13755,13413],{"class":3014},[2994,13757,3111],{"class":3021},[2994,13759,9408],{"class":3014},[2994,13761,3111],{"class":3021},[2994,13763,13489],{"class":3025},[2994,13765,9416],{"class":3021},[2994,13767,9320],{"class":3014},[2994,13769,3111],{"class":3021},[2994,13771,7968],{"class":3114},[2994,13773,3055],{"class":3021},[2994,13775,13776],{"class":2996,"line":6180},[2994,13777,8040],{"class":3021},[2994,13779,13780],{"class":2996,"line":6185},[2994,13781,13782],{"class":3000},"    // Але колекція та користувач залишилися (немає FK на автора)\n",[2994,13784,13785,13787,13789,13792,13794,13796,13798,13800,13802,13804,13806,13808],{"class":2996,"line":6190},[2994,13786,13413],{"class":3014},[2994,13788,3111],{"class":3021},[2994,13790,13791],{"class":3191},"collectionRepo",[2994,13793,3217],{"class":3021},[2994,13795,9286],{"class":3014},[2994,13797,3111],{"class":3021},[2994,13799,13358],{"class":3191},[2994,13801,3217],{"class":3021},[2994,13803,9257],{"class":3014},[2994,13805,13569],{"class":3021},[2994,13807,9934],{"class":3014},[2994,13809,4382],{"class":3021},[2994,13811,13812,13814,13816,13819,13821,13823,13825,13828,13830,13832,13834,13836],{"class":2996,"line":6196},[2994,13813,13413],{"class":3014},[2994,13815,3111],{"class":3021},[2994,13817,13818],{"class":3191},"userRepo",[2994,13820,3217],{"class":3021},[2994,13822,9286],{"class":3014},[2994,13824,3111],{"class":3021},[2994,13826,13827],{"class":3191},"user",[2994,13829,3217],{"class":3021},[2994,13831,9257],{"class":3014},[2994,13833,13569],{"class":3021},[2994,13835,9934],{"class":3014},[2994,13837,4382],{"class":3021},[2994,13839,13840],{"class":2996,"line":6202},[2994,13841,4183],{"class":3021},[2964,13843,13844],{},[2971,13845,13846],{},"Що перевіряє цей тест:",[3959,13848,13849,13855,13865],{},[3962,13850,13851,13854],{},[2971,13852,13853],{},"Багаторівневий CASCADE:"," Видалення автора → видалення книг → видалення файлів. Це 3 рівні каскаду.",[3962,13856,13857,13860,13861,13864],{},[2971,13858,13859],{},"Many-to-Many CASCADE:"," Видалення книг → видалення зв'язків у ",[2991,13862,13863],{},"audiobook_collection"," (проміжна таблиця).",[3962,13866,13867,13870,13871,13874],{},[2971,13868,13869],{},"Селективний CASCADE:"," Колекція та користувач ",[2971,13872,13873],{},"не"," видаляються, оскільки немає FK на автора.",[5628,13876,13877,13882,13888,13907],{},[2964,13878,13879],{},[2971,13880,13881],{},"Чому H2 може провалити цей тест:",[2964,13883,13884,13885,13887],{},"H2 підтримує ",[2991,13886,8065],{},", але його реалізація може відрізнятися від PostgreSQL у граничних випадках:",[4203,13889,13890,13896,13902],{},[3962,13891,13892,13895],{},[2971,13893,13894],{},"Порядок виконання:"," PostgreSQL виконує CASCADE у правильному порядку (від дочірніх до батьківських). H2 може виконати у іншому порядку і отримати FK violation.",[3962,13897,13898,13901],{},[2971,13899,13900],{},"Циклічні FK:"," Якщо є циклічні залежності (A → B → C → A), PostgreSQL коректно обробляє це через deferred constraints. H2 може провалитися.",[3962,13903,13904,13906],{},[2971,13905,7608],{}," PostgreSQL оптимізує CASCADE через batch-операції. H2 може виконувати DELETE для кожного рядка окремо.",[2964,13908,13909],{},"Тестування на реальній PostgreSQL гарантує, що CASCADE працює так, як очікується у production.",[3307,13911],{},[2959,13913,13915],{"id":13914},"порівняння-швидкості-виконання","Порівняння швидкості виконання",[2964,13917,13918],{},"Проведемо емпіричне порівняння швидкості виконання тестів на H2 vs Testcontainers:",[3317,13920,13921,13937],{},[3320,13922,13923],{},[3323,13924,13925,13928,13931,13934],{},[3326,13926,13927],{},"Операція",[3326,13929,13930],{},"H2 (in-memory)",[3326,13932,13933],{},"Testcontainers (PostgreSQL)",[3326,13935,13936],{},"Різниця",[3339,13938,13939,13955,13971,13987,14002,14015,14028,14044,14059],{},[3323,13940,13941,13946,13949,13952],{},[3344,13942,13943],{},[2971,13944,13945],{},"Запуск контейнера",[3344,13947,13948],{},"0ms (не потрібен)",[3344,13950,13951],{},"2000–5000ms (один раз)",[3344,13953,13954],{},"—",[3323,13956,13957,13962,13965,13968],{},[3344,13958,13959],{},[2971,13960,13961],{},"Створення схеми (DDL)",[3344,13963,13964],{},"50–100ms",[3344,13966,13967],{},"200–300ms",[3344,13969,13970],{},"3× повільніше",[3323,13972,13973,13978,13981,13984],{},[3344,13974,13975],{},[2971,13976,13977],{},"INSERT (1 рядок)",[3344,13979,13980],{},"1–2ms",[3344,13982,13983],{},"3–5ms",[3344,13985,13986],{},"2–3× повільніше",[3323,13988,13989,13994,13997,14000],{},[3344,13990,13991],{},[2971,13992,13993],{},"SELECT за PK",[3344,13995,13996],{},"0.5–1ms",[3344,13998,13999],{},"2–3ms",[3344,14001,13986],{},[3323,14003,14004,14009,14011,14013],{},[3344,14005,14006],{},[2971,14007,14008],{},"UPDATE (1 рядок)",[3344,14010,13980],{},[3344,14012,13983],{},[3344,14014,13986],{},[3323,14016,14017,14022,14024,14026],{},[3344,14018,14019],{},[2971,14020,14021],{},"DELETE (1 рядок)",[3344,14023,13980],{},[3344,14025,13983],{},[3344,14027,13986],{},[3323,14029,14030,14035,14038,14041],{},[3344,14031,14032],{},[2971,14033,14034],{},"TRUNCATE (очищення)",[3344,14036,14037],{},"5–10ms",[3344,14039,14040],{},"10–20ms",[3344,14042,14043],{},"2× повільніше",[3323,14045,14046,14051,14054,14057],{},[3344,14047,14048],{},[2971,14049,14050],{},"Повний тест (10 операцій)",[3344,14052,14053],{},"15–30ms",[3344,14055,14056],{},"50–80ms",[3344,14058,13970],{},[3323,14060,14061,14066,14069,14072],{},[3344,14062,14063],{},[2971,14064,14065],{},"Тестовий клас (20 тестів)",[3344,14067,14068],{},"0.5–1s",[3344,14070,14071],{},"2–3s",[3344,14073,13970],{},[2964,14075,14076],{},[2971,14077,14078],{},"Висновки з вимірювань:",[3959,14080,14081,14091,14108],{},[3962,14082,14083,14086,14087,14090],{},[2971,14084,14085],{},"Запуск контейнера — одноразова ціна:"," 2–5 секунд на запуск PostgreSQL контейнера. Але це відбувається ",[2971,14088,14089],{},"один раз"," для всього тестового класу (або навіть для всіх тестів при singleton pattern).",[3962,14092,14093,14096,14097],{},[2971,14094,14095],{},"Окремі операції повільніші у 2–3 рази:"," Кожна SQL-операція на PostgreSQL займає 2–3ms замість 1ms на H2. Це пов'язано з:",[4203,14098,14099,14102,14105],{},[3962,14100,14101],{},"Network overhead (навіть через localhost)",[3962,14103,14104],{},"Більш складний оптимізатор PostgreSQL",[3962,14106,14107],{},"Реальні disk I/O (навіть у Docker)",[3962,14109,14110,14113],{},[2971,14111,14112],{},"Загальний час тестового класу — прийнятний:"," 20 тестів виконуються за 2–3 секунди на Testcontainers vs 0.5–1 секунду на H2. Для CI/CD це прийнятно.",[4762,14115,14116,14121],{},[2964,14117,14118],{},[2971,14119,14120],{},"Оптимізація швидкості Testcontainers:",[3959,14122,14123,14129,14137,14143],{},[3962,14124,14125,14128],{},[2971,14126,14127],{},"Singleton Container Pattern:"," Один контейнер для всіх тестових класів. Економія: 2–5 секунд на кожен клас.",[3962,14130,14131,7609,14134,14136],{},[2971,14132,14133],{},"Reusable Containers:",[2991,14135,7563],{}," — контейнер залишається запущеним між запусками тестів. Економія: 2–5 секунд на кожен запуск.",[3962,14138,14139,14142],{},[2971,14140,14141],{},"Parallel Test Execution:"," JUnit 5 підтримує паралельне виконання тестів. З окремими схемами БД для кожного потоку можна досягти лінійного прискорення.",[3962,14144,14145,14148,14149,14152],{},[2971,14146,14147],{},"Кешування Docker-образів:"," Docker кешує завантажені образи. Перший запуск завантажує ",[2991,14150,14151],{},"postgres:15-alpine"," (~80MB), наступні — миттєві.",[3307,14154],{},[2959,14156,4741],{"id":14157},"cicd-інтеграція",[2964,14159,14160],{},"Testcontainers у CI/CD вимагає наявності Docker Engine на CI-сервері. Розглянемо налаштування для популярних платформ.",[2976,14162,14164],{"id":14163},"github-actions","GitHub Actions",[2984,14166,14170],{"className":14167,"code":14168,"language":14169,"meta":5001,"style":2989},"language-yaml shiki shiki-themes light-plus dark-plus dark-plus","# .github/workflows/test.yml\nname: Integration Tests\n\non:\n  push:\n    branches: [ main, develop ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest  # Ubuntu має Docker за замовчуванням\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up JDK 17\n        uses: actions/setup-java@v4\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n          cache: 'maven'\n\n      - name: Run integration tests\n        run: mvn verify -P integration-tests\n\n      - name: Upload test reports\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: test-reports\n          path: target/surefire-reports/\n","yaml",[2991,14171,14172,14177,14188,14192,14200,14207,14226,14233,14243,14247,14254,14261,14274,14278,14285,14297,14307,14311,14322,14331,14338,14348,14358,14368,14372,14383,14393,14397,14408,14418,14427,14433,14443],{"__ignoreMap":2989},[2994,14173,14174],{"class":2996,"line":2997},[2994,14175,14176],{"class":3000},"# .github/workflows/test.yml\n",[2994,14178,14179,14181,14184],{"class":2996,"line":3004},[2994,14180,8333],{"class":5011},[2994,14182,14183],{"class":3021},": ",[2994,14185,14187],{"class":14186},"su9tN","Integration Tests\n",[2994,14189,14190],{"class":2996,"line":3058},[2994,14191,3062],{"emptyLinePlaceholder":3061},[2994,14193,14194,14197],{"class":2996,"line":3065},[2994,14195,14196],{"class":3007},"on",[2994,14198,14199],{"class":3021},":\n",[2994,14201,14202,14205],{"class":2996,"line":3079},[2994,14203,14204],{"class":5011},"  push",[2994,14206,14199],{"class":3021},[2994,14208,14209,14212,14215,14218,14220,14223],{"class":2996,"line":3091},[2994,14210,14211],{"class":5011},"    branches",[2994,14213,14214],{"class":3021},": [ ",[2994,14216,14217],{"class":14186},"main",[2994,14219,3029],{"class":3021},[2994,14221,14222],{"class":14186},"develop",[2994,14224,14225],{"class":3021}," ]\n",[2994,14227,14228,14231],{"class":2996,"line":3102},[2994,14229,14230],{"class":5011},"  pull_request",[2994,14232,14199],{"class":3021},[2994,14234,14235,14237,14239,14241],{"class":2996,"line":3125},[2994,14236,14211],{"class":5011},[2994,14238,14214],{"class":3021},[2994,14240,14217],{"class":14186},[2994,14242,14225],{"class":3021},[2994,14244,14245],{"class":2996,"line":3139},[2994,14246,3062],{"emptyLinePlaceholder":3061},[2994,14248,14249,14252],{"class":2996,"line":3148},[2994,14250,14251],{"class":5011},"jobs",[2994,14253,14199],{"class":3021},[2994,14255,14256,14259],{"class":2996,"line":3845},[2994,14257,14258],{"class":5011},"  test",[2994,14260,14199],{"class":3021},[2994,14262,14263,14266,14268,14271],{"class":2996,"line":3851},[2994,14264,14265],{"class":5011},"    runs-on",[2994,14267,14183],{"class":3021},[2994,14269,14270],{"class":14186},"ubuntu-latest",[2994,14272,14273],{"class":3000},"  # Ubuntu має Docker за замовчуванням\n",[2994,14275,14276],{"class":2996,"line":3856},[2994,14277,3062],{"emptyLinePlaceholder":3061},[2994,14279,14280,14283],{"class":2996,"line":3861},[2994,14281,14282],{"class":5011},"    steps",[2994,14284,14199],{"class":3021},[2994,14286,14287,14290,14292,14294],{"class":2996,"line":3867},[2994,14288,14289],{"class":3021},"      - ",[2994,14291,8333],{"class":5011},[2994,14293,14183],{"class":3021},[2994,14295,14296],{"class":14186},"Checkout code\n",[2994,14298,14299,14302,14304],{"class":2996,"line":3873},[2994,14300,14301],{"class":5011},"        uses",[2994,14303,14183],{"class":3021},[2994,14305,14306],{"class":14186},"actions/checkout@v4\n",[2994,14308,14309],{"class":2996,"line":3879},[2994,14310,3062],{"emptyLinePlaceholder":3061},[2994,14312,14313,14315,14317,14319],{"class":2996,"line":3885},[2994,14314,14289],{"class":3021},[2994,14316,8333],{"class":5011},[2994,14318,14183],{"class":3021},[2994,14320,14321],{"class":14186},"Set up JDK 17\n",[2994,14323,14324,14326,14328],{"class":2996,"line":3891},[2994,14325,14301],{"class":5011},[2994,14327,14183],{"class":3021},[2994,14329,14330],{"class":14186},"actions/setup-java@v4\n",[2994,14332,14333,14336],{"class":2996,"line":3897},[2994,14334,14335],{"class":5011},"        with",[2994,14337,14199],{"class":3021},[2994,14339,14340,14343,14345],{"class":2996,"line":3903},[2994,14341,14342],{"class":5011},"          java-version",[2994,14344,14183],{"class":3021},[2994,14346,14347],{"class":14186},"'17'\n",[2994,14349,14350,14353,14355],{"class":2996,"line":3908},[2994,14351,14352],{"class":5011},"          distribution",[2994,14354,14183],{"class":3021},[2994,14356,14357],{"class":14186},"'temurin'\n",[2994,14359,14360,14363,14365],{"class":2996,"line":3914},[2994,14361,14362],{"class":5011},"          cache",[2994,14364,14183],{"class":3021},[2994,14366,14367],{"class":14186},"'maven'\n",[2994,14369,14370],{"class":2996,"line":3920},[2994,14371,3062],{"emptyLinePlaceholder":3061},[2994,14373,14374,14376,14378,14380],{"class":2996,"line":3926},[2994,14375,14289],{"class":3021},[2994,14377,8333],{"class":5011},[2994,14379,14183],{"class":3021},[2994,14381,14382],{"class":14186},"Run integration tests\n",[2994,14384,14385,14388,14390],{"class":2996,"line":3931},[2994,14386,14387],{"class":5011},"        run",[2994,14389,14183],{"class":3021},[2994,14391,14392],{"class":14186},"mvn verify -P integration-tests\n",[2994,14394,14395],{"class":2996,"line":3937},[2994,14396,3062],{"emptyLinePlaceholder":3061},[2994,14398,14399,14401,14403,14405],{"class":2996,"line":3943},[2994,14400,14289],{"class":3021},[2994,14402,8333],{"class":5011},[2994,14404,14183],{"class":3021},[2994,14406,14407],{"class":14186},"Upload test reports\n",[2994,14409,14410,14413,14415],{"class":2996,"line":3949},[2994,14411,14412],{"class":5011},"        if",[2994,14414,14183],{"class":3021},[2994,14416,14417],{"class":14186},"always()\n",[2994,14419,14420,14422,14424],{"class":2996,"line":5348},[2994,14421,14301],{"class":5011},[2994,14423,14183],{"class":3021},[2994,14425,14426],{"class":14186},"actions/upload-artifact@v4\n",[2994,14428,14429,14431],{"class":2996,"line":5365},[2994,14430,14335],{"class":5011},[2994,14432,14199],{"class":3021},[2994,14434,14435,14438,14440],{"class":2996,"line":5382},[2994,14436,14437],{"class":5011},"          name",[2994,14439,14183],{"class":3021},[2994,14441,14442],{"class":14186},"test-reports\n",[2994,14444,14445,14448,14450],{"class":2996,"line":5391},[2994,14446,14447],{"class":5011},"          path",[2994,14449,14183],{"class":3021},[2994,14451,14452],{"class":14186},"target/surefire-reports/\n",[2964,14454,14455],{},[2971,14456,12598],{},[4203,14458,14459,14468,14482],{},[3962,14460,14461,3769,14464,14467],{},[2971,14462,14463],{},"Рядок 12",[2991,14465,14466],{},"runs-on: ubuntu-latest","): Ubuntu GitHub Runners мають Docker Engine за замовчуванням. Не потрібно додаткове налаштування.",[3962,14469,14470,3769,14473,7593,14476,7597,14479,14481],{},[2971,14471,14472],{},"Рядок 24",[2991,14474,14475],{},"mvn verify",[2991,14477,14478],{},"verify",[2991,14480,5099],{},", щоб запустити інтеграційні тести (якщо вони у окремому Maven profile).",[3962,14483,14484,14487],{},[2971,14485,14486],{},"Рядки 26–31",": Завантажуємо звіти тестів як артефакти — корисно для діагностики провалених тестів.",[3307,14489],{},[2976,14491,14493],{"id":14492},"gitlab-ci","GitLab CI",[2984,14495,14497],{"className":14167,"code":14496,"language":14169,"meta":5001,"style":2989},"# .gitlab-ci.yml\nstages:\n  - test\n\nintegration-tests:\n  stage: test\n  image: maven:3.9-eclipse-temurin-17  # Maven + JDK 17\n  \n  services:\n    - docker:24-dind  # Docker-in-Docker\n\n  variables:\n    DOCKER_HOST: tcp://docker:2375\n    DOCKER_TLS_CERTDIR: \"\"\n    MAVEN_OPTS: \"-Dmaven.repo.local=.m2/repository\"\n\n  script:\n    - mvn verify -P integration-tests\n\n  artifacts:\n    when: always\n    reports:\n      junit: target/surefire-reports/TEST-*.xml\n    paths:\n      - target/surefire-reports/\n\n  cache:\n    paths:\n      - .m2/repository\n",[2991,14498,14499,14504,14511,14519,14523,14530,14539,14552,14557,14564,14575,14579,14586,14596,14606,14616,14620,14627,14633,14637,14644,14654,14661,14671,14678,14684,14688,14695,14701],{"__ignoreMap":2989},[2994,14500,14501],{"class":2996,"line":2997},[2994,14502,14503],{"class":3000},"# .gitlab-ci.yml\n",[2994,14505,14506,14509],{"class":2996,"line":3004},[2994,14507,14508],{"class":5011},"stages",[2994,14510,14199],{"class":3021},[2994,14512,14513,14516],{"class":2996,"line":3058},[2994,14514,14515],{"class":3021},"  - ",[2994,14517,14518],{"class":14186},"test\n",[2994,14520,14521],{"class":2996,"line":3065},[2994,14522,3062],{"emptyLinePlaceholder":3061},[2994,14524,14525,14528],{"class":2996,"line":3079},[2994,14526,14527],{"class":5011},"integration-tests",[2994,14529,14199],{"class":3021},[2994,14531,14532,14535,14537],{"class":2996,"line":3091},[2994,14533,14534],{"class":5011},"  stage",[2994,14536,14183],{"class":3021},[2994,14538,14518],{"class":14186},[2994,14540,14541,14544,14546,14549],{"class":2996,"line":3102},[2994,14542,14543],{"class":5011},"  image",[2994,14545,14183],{"class":3021},[2994,14547,14548],{"class":14186},"maven:3.9-eclipse-temurin-17",[2994,14550,14551],{"class":3000},"  # Maven + JDK 17\n",[2994,14553,14554],{"class":2996,"line":3125},[2994,14555,14556],{"class":3021},"  \n",[2994,14558,14559,14562],{"class":2996,"line":3139},[2994,14560,14561],{"class":5011},"  services",[2994,14563,14199],{"class":3021},[2994,14565,14566,14569,14572],{"class":2996,"line":3148},[2994,14567,14568],{"class":3021},"    - ",[2994,14570,14571],{"class":14186},"docker:24-dind",[2994,14573,14574],{"class":3000},"  # Docker-in-Docker\n",[2994,14576,14577],{"class":2996,"line":3845},[2994,14578,3062],{"emptyLinePlaceholder":3061},[2994,14580,14581,14584],{"class":2996,"line":3851},[2994,14582,14583],{"class":5011},"  variables",[2994,14585,14199],{"class":3021},[2994,14587,14588,14591,14593],{"class":2996,"line":3856},[2994,14589,14590],{"class":5011},"    DOCKER_HOST",[2994,14592,14183],{"class":3021},[2994,14594,14595],{"class":14186},"tcp://docker:2375\n",[2994,14597,14598,14601,14603],{"class":2996,"line":3861},[2994,14599,14600],{"class":5011},"    DOCKER_TLS_CERTDIR",[2994,14602,14183],{"class":3021},[2994,14604,14605],{"class":3025},"\"\"\n",[2994,14607,14608,14611,14613],{"class":2996,"line":3867},[2994,14609,14610],{"class":5011},"    MAVEN_OPTS",[2994,14612,14183],{"class":3021},[2994,14614,14615],{"class":3025},"\"-Dmaven.repo.local=.m2/repository\"\n",[2994,14617,14618],{"class":2996,"line":3873},[2994,14619,3062],{"emptyLinePlaceholder":3061},[2994,14621,14622,14625],{"class":2996,"line":3879},[2994,14623,14624],{"class":5011},"  script",[2994,14626,14199],{"class":3021},[2994,14628,14629,14631],{"class":2996,"line":3885},[2994,14630,14568],{"class":3021},[2994,14632,14392],{"class":14186},[2994,14634,14635],{"class":2996,"line":3891},[2994,14636,3062],{"emptyLinePlaceholder":3061},[2994,14638,14639,14642],{"class":2996,"line":3897},[2994,14640,14641],{"class":5011},"  artifacts",[2994,14643,14199],{"class":3021},[2994,14645,14646,14649,14651],{"class":2996,"line":3903},[2994,14647,14648],{"class":5011},"    when",[2994,14650,14183],{"class":3021},[2994,14652,14653],{"class":14186},"always\n",[2994,14655,14656,14659],{"class":2996,"line":3908},[2994,14657,14658],{"class":5011},"    reports",[2994,14660,14199],{"class":3021},[2994,14662,14663,14666,14668],{"class":2996,"line":3914},[2994,14664,14665],{"class":5011},"      junit",[2994,14667,14183],{"class":3021},[2994,14669,14670],{"class":14186},"target/surefire-reports/TEST-*.xml\n",[2994,14672,14673,14676],{"class":2996,"line":3920},[2994,14674,14675],{"class":5011},"    paths",[2994,14677,14199],{"class":3021},[2994,14679,14680,14682],{"class":2996,"line":3926},[2994,14681,14289],{"class":3021},[2994,14683,14452],{"class":14186},[2994,14685,14686],{"class":2996,"line":3931},[2994,14687,3062],{"emptyLinePlaceholder":3061},[2994,14689,14690,14693],{"class":2996,"line":3937},[2994,14691,14692],{"class":5011},"  cache",[2994,14694,14199],{"class":3021},[2994,14696,14697,14699],{"class":2996,"line":3943},[2994,14698,14675],{"class":5011},[2994,14700,14199],{"class":3021},[2994,14702,14703,14705],{"class":2996,"line":3949},[2994,14704,14289],{"class":3021},[2994,14706,14707],{"class":14186},".m2/repository\n",[2964,14709,14710],{},[2971,14711,12598],{},[4203,14713,14714,14722,14728],{},[3962,14715,14716,3769,14719,14721],{},[2971,14717,14718],{},"Рядок 10",[2991,14720,14571],{},"): Docker-in-Docker service. Дозволяє запускати Docker-контейнери всередині GitLab Runner контейнера.",[3962,14723,14724,14727],{},[2971,14725,14726],{},"Рядки 13–14",": Налаштування Docker Host для Testcontainers. Без цього Testcontainers не зможе підключитися до Docker Engine.",[3962,14729,14730,3769,14733,14736],{},[2971,14731,14732],{},"Рядки 22–23",[2991,14734,14735],{},"junit: ...","): GitLab автоматично парсить JUnit XML-звіти і відображає результати у UI.",[3307,14738],{},[2976,14740,14742],{"id":14741},"jenkins","Jenkins",[2984,14744,14748],{"className":14745,"code":14746,"language":14747,"meta":2989,"style":2989},"language-groovy shiki shiki-themes light-plus dark-plus dark-plus","// Jenkinsfile\npipeline {\n    agent {\n        docker {\n            image 'maven:3.9-eclipse-temurin-17'\n            args '-v /var/run/docker.sock:/var/run/docker.sock'\n            // Монтуємо Docker socket з host-машини\n        }\n    }\n\n    stages {\n        stage('Test') {\n            steps {\n                sh 'mvn verify -P integration-tests'\n            }\n        }\n    }\n\n    post {\n        always {\n            junit 'target/surefire-reports/*.xml'\n        }\n    }\n}\n","groovy",[2991,14749,14750,14755,14760,14765,14770,14775,14780,14785,14789,14793,14797,14802,14807,14812,14817,14821,14825,14829,14833,14838,14843,14848,14852,14856],{"__ignoreMap":2989},[2994,14751,14752],{"class":2996,"line":2997},[2994,14753,14754],{},"// Jenkinsfile\n",[2994,14756,14757],{"class":2996,"line":3004},[2994,14758,14759],{},"pipeline {\n",[2994,14761,14762],{"class":2996,"line":3058},[2994,14763,14764],{},"    agent {\n",[2994,14766,14767],{"class":2996,"line":3065},[2994,14768,14769],{},"        docker {\n",[2994,14771,14772],{"class":2996,"line":3079},[2994,14773,14774],{},"            image 'maven:3.9-eclipse-temurin-17'\n",[2994,14776,14777],{"class":2996,"line":3091},[2994,14778,14779],{},"            args '-v /var/run/docker.sock:/var/run/docker.sock'\n",[2994,14781,14782],{"class":2996,"line":3102},[2994,14783,14784],{},"            // Монтуємо Docker socket з host-машини\n",[2994,14786,14787],{"class":2996,"line":3125},[2994,14788,6709],{},[2994,14790,14791],{"class":2996,"line":3139},[2994,14792,4387],{},[2994,14794,14795],{"class":2996,"line":3148},[2994,14796,3062],{"emptyLinePlaceholder":3061},[2994,14798,14799],{"class":2996,"line":3845},[2994,14800,14801],{},"    stages {\n",[2994,14803,14804],{"class":2996,"line":3851},[2994,14805,14806],{},"        stage('Test') {\n",[2994,14808,14809],{"class":2996,"line":3856},[2994,14810,14811],{},"            steps {\n",[2994,14813,14814],{"class":2996,"line":3861},[2994,14815,14816],{},"                sh 'mvn verify -P integration-tests'\n",[2994,14818,14819],{"class":2996,"line":3867},[2994,14820,7065],{},[2994,14822,14823],{"class":2996,"line":3873},[2994,14824,6709],{},[2994,14826,14827],{"class":2996,"line":3879},[2994,14828,4387],{},[2994,14830,14831],{"class":2996,"line":3885},[2994,14832,3062],{"emptyLinePlaceholder":3061},[2994,14834,14835],{"class":2996,"line":3891},[2994,14836,14837],{},"    post {\n",[2994,14839,14840],{"class":2996,"line":3897},[2994,14841,14842],{},"        always {\n",[2994,14844,14845],{"class":2996,"line":3903},[2994,14846,14847],{},"            junit 'target/surefire-reports/*.xml'\n",[2994,14849,14850],{"class":2996,"line":3908},[2994,14851,6709],{},[2994,14853,14854],{"class":2996,"line":3914},[2994,14855,4387],{},[2994,14857,14858],{"class":2996,"line":3920},[2994,14859,4183],{},[2964,14861,14862],{},[2971,14863,12598],{},[4203,14865,14866],{},[3962,14867,14868,3769,14871,14874],{},[2971,14869,14870],{},"Рядок 6",[2991,14872,14873],{},"-v /var/run/docker.sock","): Монтуємо Docker socket з host-машини у контейнер. Це дозволяє Testcontainers всередині Maven-контейнера запускати sibling-контейнери на host Docker Engine.",[5628,14876,14877,14882,14893,14901,14906],{},[2964,14878,14879],{},[2971,14880,14881],{},"Безпека Docker-in-Docker:",[2964,14883,14884,14885,14888,14889,14892],{},"Монтування ",[2991,14886,14887],{},"/var/run/docker.sock"," дає контейнеру ",[2971,14890,14891],{},"повний доступ"," до Docker Engine host-машини. Це потенційна вразливість безпеки:",[4203,14894,14895,14898],{},[3962,14896,14897],{},"Контейнер може запустити будь-який інший контейнер з привілеями root",[3962,14899,14900],{},"Контейнер може зупинити або видалити інші контейнери на host",[2964,14902,14903],{},[2971,14904,14905],{},"Рекомендації:",[3959,14907,14908,14911,14914],{},[3962,14909,14910],{},"Використовуйте окремі CI-сервери для тестів (не production)",[3962,14912,14913],{},"Обмежте доступ до Docker socket через AppArmor/SELinux",[3962,14915,14916],{},"Розгляньте альтернативи: Podman, Kaniko (rootless container runtimes)",[3307,14918],{},[2959,14920,14922],{"id":14921},"висновки","Висновки",[2964,14924,14925],{},"Testcontainers є потужним інструментом для інтеграційного тестування JDBC-репозиторіїв, що вимагають точної емуляції production СУБД. Ключові висновки:",[14927,14928,14929,14933,14936,14940,14943,14947,14950,14954,14960,14964,14967,14971],"steps",{},[2976,14930,14932],{"id":14931},"точність-vs-швидкість","Точність vs Швидкість",[2964,14934,14935],{},"H2 швидший (3× швидше), але менш точний (70–80% сумісність). Testcontainers повільніший, але дає 100% точність — реальна PostgreSQL у Docker.",[2976,14937,14939],{"id":14938},"гібридна-стратегія","Гібридна стратегія",[2964,14941,14942],{},"Використовуйте H2 для швидких тестів базової функціональності (CRUD, constraints). Використовуйте Testcontainers для PostgreSQL-специфічних функцій (ENUM, JSON, full-text search, складні транзакції).",[2976,14944,14946],{"id":14945},"lifecycle-контейнерів","Lifecycle контейнерів",[2964,14948,14949],{},"Singleton Container Pattern (один контейнер для всіх тестів) дає найкращий баланс швидкості та ізоляції. Запуск контейнера займає 2–5 секунд, але відбувається один раз.",[2976,14951,14953],{"id":14952},"ізоляція-через-truncate","Ізоляція через TRUNCATE",[2964,14955,14956,14957,14959],{},"Кожен тест очищає БД через ",[2991,14958,7631],{},". Це швидше за створення нового контейнера і гарантує незалежність тестів.",[2976,14961,14963],{"id":14962},"cicd-вимагає-docker","CI/CD вимагає Docker",[2964,14965,14966],{},"Testcontainers потребує Docker Engine на CI-сервері. GitHub Actions та GitLab CI підтримують це «з коробки». Jenkins вимагає монтування Docker socket.",[2976,14968,14970],{"id":14969},"тестування-constraints","Тестування constraints",[2964,14972,14973],{},"Інтеграційні тести з реальною БД гарантують, що FK, UNIQUE, CHECK constraints працюють коректно. Mock-тести не можуть це перевірити.",[3307,14975],{},[2959,14977,14979],{"id":14978},"порівняльна-таблиця-коли-що-використовувати","Порівняльна таблиця: Коли що використовувати",[3317,14981,14982,14997],{},[3320,14983,14984],{},[3323,14985,14986,14989,14992,14994],{},[3326,14987,14988],{},"Сценарій",[3326,14990,14991],{},"H2",[3326,14993,3768],{},[3326,14995,14996],{},"Обґрунтування",[3339,14998,14999,15015,15029,15044,15057,15069,15082,15096,15109,15123,15137],{},[3323,15000,15001,15006,15009,15012],{},[3344,15002,15003],{},[2971,15004,15005],{},"Базові CRUD-операції",[3344,15007,15008],{},"✅",[3344,15010,15011],{},"⚠️",[3344,15013,15014],{},"H2 достатньо, швидше",[3323,15016,15017,15022,15024,15026],{},[3344,15018,15019],{},[2971,15020,15021],{},"FK/UNIQUE/CHECK constraints",[3344,15023,15008],{},[3344,15025,15008],{},[3344,15027,15028],{},"Обидва підходи працюють",[3323,15030,15031,15036,15039,15041],{},[3344,15032,15033],{},[2971,15034,15035],{},"PostgreSQL ENUM",[3344,15037,15038],{},"❌",[3344,15040,15008],{},[3344,15042,15043],{},"H2 не підтримує",[3323,15045,15046,15051,15053,15055],{},[3344,15047,15048],{},[2971,15049,15050],{},"JSON/JSONB оператори",[3344,15052,15038],{},[3344,15054,15008],{},[3344,15056,15043],{},[3323,15058,15059,15063,15065,15067],{},[3344,15060,15061],{},[2971,15062,3423],{},[3344,15064,15038],{},[3344,15066,15008],{},[3344,15068,15043],{},[3323,15070,15071,15076,15078,15080],{},[3344,15072,15073],{},[2971,15074,15075],{},"Масиви (ARRAY)",[3344,15077,15038],{},[3344,15079,15008],{},[3344,15081,15043],{},[3323,15083,15084,15089,15091,15093],{},[3344,15085,15086],{},[2971,15087,15088],{},"Складні JOIN",[3344,15090,15011],{},[3344,15092,15008],{},[3344,15094,15095],{},"Різні оптимізатори",[3323,15097,15098,15102,15104,15106],{},[3344,15099,15100],{},[2971,15101,4715],{},[3344,15103,15011],{},[3344,15105,15008],{},[3344,15107,15108],{},"Різні механізми",[3323,15110,15111,15116,15118,15120],{},[3344,15112,15113],{},[2971,15114,15115],{},"Тести продуктивності",[3344,15117,15038],{},[3344,15119,15008],{},[3344,15121,15122],{},"H2 не відображає реальну продуктивність",[3323,15124,15125,15130,15132,15134],{},[3344,15126,15127],{},[2971,15128,15129],{},"Локальна розробка",[3344,15131,15008],{},[3344,15133,15008],{},[3344,15135,15136],{},"Обидва підходи зручні",[3323,15138,15139,15144,15146,15148],{},[3344,15140,15141],{},[2971,15142,15143],{},"CI/CD",[3344,15145,15008],{},[3344,15147,15011],{},[3344,15149,15150],{},"H2 простіше (не потрібен Docker)",[2964,15152,15153],{},[2971,15154,15155],{},"Рекомендована стратегія:",[2984,15157,15160],{"className":15158,"code":15159,"language":4194},[4192],"┌─────────────────────────────────────────────────────────┐\n│  Швидкі тести (H2)                                      │\n│  • CRUD-операції                                        │\n│  • Базові constraints                                   │\n│  • Прості запити                                        │\n│  • Виконуються при кожному збереженні файлу             │\n│  Час: 0.5–1s для 20 тестів                              │\n└─────────────────────────────────────────────────────────┘\n                          ↓\n┌─────────────────────────────────────────────────────────┐\n│  Повні тести (Testcontainers)                           │\n│  • PostgreSQL-специфічні функції                        │\n│  • Складні JOIN та транзакції                           │\n│  • Тести продуктивності                                 │\n│  • Виконуються перед commit та у CI/CD                  │\n│  Час: 2–3s для 20 тестів + 3s запуск контейнера         │\n└─────────────────────────────────────────────────────────┘\n",[2991,15161,15159],{"__ignoreMap":2989},[3307,15163],{},[2959,15165,15167],{"id":15166},"подальше-читання","Подальше читання",[15169,15170,15171,15185,15198,15212],"card-group",{},[15172,15173,15176,15182],"card",{"icon":15174,"title":15175},"i-heroicons-document-text","Testcontainers Documentation",[2964,15177,15178],{},[3771,15179,15181],{"href":3773,"rel":15180},[3775],"testcontainers.com",[2964,15183,15184],{},"Офіційна документація: модулі для різних СУБД, advanced patterns, troubleshooting.",[15172,15186,15188,15195],{"icon":2567,"title":15187},"Docker Documentation",[2964,15189,15190],{},[3771,15191,15194],{"href":15192,"rel":15193},"https://docs.docker.com",[3775],"docs.docker.com",[2964,15196,15197],{},"Розуміння Docker lifecycle, networking, volumes — необхідно для налагодження Testcontainers.",[15172,15199,15202,15209],{"icon":15200,"title":15201},"i-simple-icons-postgresql","PostgreSQL Documentation",[2964,15203,15204],{},[3771,15205,15208],{"href":15206,"rel":15207},"https://www.postgresql.org/docs/",[3775],"postgresql.org/docs",[2964,15210,15211],{},"Офіційна документація PostgreSQL: ENUM, JSON, full-text search, транзакційна ізоляція.",[15172,15213,15216,15219],{"icon":15214,"title":15215},"i-heroicons-rocket-launch","Continuous Delivery",[2964,15217,15218],{},"Jez Humble, David Farley, 2010",[2964,15220,15221],{},"Розділ 9 — про тестування у CI/CD pipeline, стратегії інтеграційних тестів.",[3307,15223],{},[2964,15225,15226,15229],{},[2971,15227,15228],{},"Наступна стаття серії:"," Specification Pattern для композиції складних запитів (стаття 20, вже написана).",[2964,15231,15232,15235],{},[2971,15233,15234],{},"Попередня стаття:"," Інтеграційне тестування з Embedded H2 (стаття 22).",[15237,15238,15239],"style",{},"html pre.shiki code .spJ8K, html code.shiki .spJ8K{--shiki-light:#008000;--shiki-default:#6A9955;--shiki-dark:#6A9955}html pre.shiki code .su1O8, html code.shiki .su1O8{--shiki-light:#0000FF;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .s8Opu, html code.shiki .s8Opu{--shiki-light:#795E26;--shiki-default:#DCDCAA;--shiki-dark:#DCDCAA}html pre.shiki code .sHH4Y, html code.shiki .sHH4Y{--shiki-light:#000000;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .sbdoH, html code.shiki .sbdoH{--shiki-light:#A31515;--shiki-default:#CE9178;--shiki-dark:#CE9178}html pre.shiki code .sJj4R, html code.shiki .sJj4R{--shiki-light:#098658;--shiki-default:#B5CEA8;--shiki-dark:#B5CEA8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sN1BT, html code.shiki .sN1BT{--shiki-light:#267F99;--shiki-default:#4EC9B0;--shiki-dark:#4EC9B0}html pre.shiki code .siwwj, html code.shiki .siwwj{--shiki-light:#001080;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .sCDza, html code.shiki .sCDza{--shiki-light:#AF00DB;--shiki-default:#CE92A4;--shiki-dark:#CE92A4}html pre.shiki code .s0P7L, html code.shiki .s0P7L{--shiki-light:#800000;--shiki-default:#808080;--shiki-dark:#808080}html pre.shiki code .sKtos, html code.shiki .sKtos{--shiki-light:#800000;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .sjcCO, html code.shiki .sjcCO{--shiki-light:#EE0000;--shiki-default:#D7BA7D;--shiki-dark:#D7BA7D}html pre.shiki code .su9tN, html code.shiki .su9tN{--shiki-light:#0000FF;--shiki-default:#CE9178;--shiki-dark:#CE9178}",{"title":2989,"searchDepth":3004,"depth":3004,"links":15241},[15242,15248,15252,15253,15258,15259,15262,15266,15267,15272,15280,15281],{"id":2961,"depth":3004,"text":2962,"children":15243},[15244,15245,15246,15247],{"id":2978,"depth":3058,"text":2979},{"id":3311,"depth":3058,"text":3312},{"id":3551,"depth":3058,"text":3552},{"id":3643,"depth":3058,"text":3644},{"id":3762,"depth":3004,"text":3763,"children":15249},[15250,15251],{"id":3783,"depth":3058,"text":3784},{"id":3996,"depth":3058,"text":3997},{"id":4585,"depth":3004,"text":4586},{"id":4790,"depth":3004,"text":4791,"children":15254},[15255,15256,15257],{"id":4794,"depth":3058,"text":2586},{"id":4988,"depth":3058,"text":4989},{"id":5653,"depth":3058,"text":5654},{"id":7662,"depth":3004,"text":7663},{"id":8856,"depth":3004,"text":8857,"children":15260},[15261],{"id":8867,"depth":3058,"text":8868},{"id":11069,"depth":3004,"text":11070,"children":15263},[15264,15265],{"id":11080,"depth":3058,"text":11081},{"id":12773,"depth":3058,"text":12774},{"id":13914,"depth":3004,"text":13915},{"id":14157,"depth":3004,"text":4741,"children":15268},[15269,15270,15271],{"id":14163,"depth":3058,"text":14164},{"id":14492,"depth":3058,"text":14493},{"id":14741,"depth":3058,"text":14742},{"id":14921,"depth":3004,"text":14922,"children":15273},[15274,15275,15276,15277,15278,15279],{"id":14931,"depth":3058,"text":14932},{"id":14938,"depth":3058,"text":14939},{"id":14945,"depth":3058,"text":14946},{"id":14952,"depth":3058,"text":14953},{"id":14962,"depth":3058,"text":14963},{"id":14969,"depth":3058,"text":14970},{"id":14978,"depth":3004,"text":14979},{"id":15166,"depth":3004,"text":15167},"Від емуляції до реальності: архітектура Testcontainers, lifecycle Docker-контейнерів у тестах, тестування PostgreSQL-специфічних функцій (ENUM, JSON, full-text search), паралельне виконання тестів та інтеграція з CI/CD.","md",null,{},{"title":2345,"description":15282},"RK8xAoDyibTX6QPXYOrpJhNM5IndDv-XUNnEKCX-KAQ",[15289,15291],{"title":2341,"path":2342,"stem":2343,"description":15290,"children":-1},"Від mock-об'єктів до реальної БД: налаштування Embedded H2 для інтеграційних тестів, патерн Arrange-Act-Assert, найменування тестів за Given-When-Then, Test Data Builders та тестування FK/UNIQUE/CHECK constraints.",{"title":2349,"path":2350,"stem":2351,"description":2989,"children":-1},1777909223334]