Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Lightweight/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ set(HEADER_FILES
SqlQuery/Select.hpp
SqlQuery/Update.hpp

CodeGen/SplitFileWriter.hpp

MigrationFold/Folder.hpp
MigrationFold/CppEmitter.hpp
MigrationFold/SqlEmitter.hpp

DataMapper/BelongsTo.hpp
DataMapper/DataMapper.hpp
DataMapper/Error.hpp
Expand Down Expand Up @@ -126,6 +132,12 @@ set(SOURCE_FILES
SqlQuery/Migrate.cpp
SqlQuery/MigrationPlan.cpp
SqlQuery/Select.cpp

CodeGen/SplitFileWriter.cpp

MigrationFold/Folder.cpp
MigrationFold/CppEmitter.cpp
MigrationFold/SqlEmitter.cpp
SqlQueryFormatter.cpp
SqlSchema.cpp
SqlStatement.cpp
Expand Down
176 changes: 176 additions & 0 deletions src/Lightweight/CodeGen/SplitFileWriter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: Apache-2.0

#include "SplitFileWriter.hpp"

#include <format>
#include <fstream>
#include <stdexcept>
#include <system_error>

namespace Lightweight::CodeGen
{

std::vector<std::vector<CodeBlock>> GroupBlocksByLineBudget(std::vector<CodeBlock> const& blocks,
std::size_t maxLinesPerFile)
{
std::vector<std::vector<CodeBlock>> chunks;
if (blocks.empty())
return chunks;

if (maxLinesPerFile == 0)
{
chunks.emplace_back(blocks);
return chunks;
}

chunks.emplace_back();
std::size_t currentLines = 0;
for (auto const& block: blocks)
{
if (!chunks.back().empty() && currentLines + block.lineCount > maxLinesPerFile)
{
chunks.emplace_back();
currentLines = 0;
}
chunks.back().push_back(block);
currentLines += block.lineCount;
}
if (chunks.back().empty())
chunks.pop_back();
return chunks;
}

namespace
{
/// @brief Ensures the parent directory exists so subsequent `ofstream` opens succeed.
void EnsureParentDirectoryExists(std::filesystem::path const& filePath)
{
auto const parent = filePath.parent_path();
if (parent.empty())
return;
std::error_code ec;
std::filesystem::create_directories(parent, ec);
if (ec)
throw std::runtime_error(std::format("Failed to create directory {}: {}", parent.string(), ec.message()));
}

/// @brief Writes one chunk file with optional header/footer.
void WriteChunkFile(std::filesystem::path const& path,
std::vector<CodeBlock> const& chunk,
std::string_view header,
std::string_view footer)
{
EnsureParentDirectoryExists(path);
std::ofstream out(path);
if (!out.is_open())
throw std::runtime_error(std::format("Failed to open output file: {}", path.string()));
if (!header.empty())
out << header;
for (auto const& block: chunk)
out << block.content;
if (!footer.empty())
out << footer;
}

/// @brief Total line count of all blocks; cached on the `CodeBlock` so this loop
/// is just a sum, not a per-call newline scan.
[[nodiscard]] std::size_t TotalLines(std::vector<CodeBlock> const& blocks)
{
std::size_t total = 0;
for (auto const& b: blocks)
total += b.lineCount;
return total;
}
} // namespace

WriteResult EmitChunked(std::filesystem::path const& outputPath,
std::vector<CodeBlock> const& blocks,
std::size_t maxLinesPerFile,
std::string_view fileHeader,
std::string_view fileFooter)
{
WriteResult result;

if (maxLinesPerFile == 0 || TotalLines(blocks) <= maxLinesPerFile)
{
WriteChunkFile(outputPath, blocks, fileHeader, fileFooter);
result.writtenFiles.push_back(outputPath);
return result;
}

auto const chunks = GroupBlocksByLineBudget(blocks, maxLinesPerFile);
auto const stem = outputPath.parent_path() / outputPath.stem();
auto const ext = outputPath.extension().string();

for (std::size_t i = 0; i < chunks.size(); ++i)
{
auto partPath = std::filesystem::path { std::format("{}_part{:02}{}", stem.string(), i + 1, ext) };
WriteChunkFile(partPath, chunks[i], fileHeader, fileFooter);
result.writtenFiles.push_back(std::move(partPath));
}

return result;
}

void EmitPluginCmake(std::filesystem::path const& outputDir, std::string_view pluginName, std::string_view sourceGlob)
{
std::error_code ec;
std::filesystem::create_directories(outputDir, ec);
if (ec)
throw std::runtime_error(std::format("Failed to create directory {}: {}", outputDir.string(), ec.message()));

auto const cmakePath = outputDir / "CMakeLists.txt";
{
std::ofstream out(cmakePath);
if (!out.is_open())
throw std::runtime_error(std::format("Failed to open output file: {}", cmakePath.string()));
out << "# SPDX-License-Identifier: Apache-2.0\n"
<< "# Auto-generated. DO NOT EDIT.\n"
<< "\n"
<< "cmake_minimum_required(VERSION 3.25)\n"
<< "\n"
<< "# Pick up every generated migration source. CONFIGURE_DEPENDS makes CMake re-glob\n"
<< "# when the generator regenerates the directory, so new sources enter the build\n"
<< "# without a manual reconfigure.\n"
<< "file(GLOB " << pluginName << "_MIGRATIONS CONFIGURE_DEPENDS\n"
<< " \"${CMAKE_CURRENT_SOURCE_DIR}/" << sourceGlob << "\"\n"
<< ")\n"
<< "\n"
<< "add_library(" << pluginName << " MODULE\n"
<< " Plugin.cpp\n"
<< " ${" << pluginName << "_MIGRATIONS}\n"
<< ")\n"
<< "\n"
<< "target_link_libraries(" << pluginName << " PRIVATE Lightweight::Lightweight)\n"
<< "\n"
<< "set_target_properties(" << pluginName << " PROPERTIES\n"
<< " LIBRARY_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/plugins\"\n"
<< " RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/plugins\"\n"
<< " PREFIX \"\"\n"
<< ")\n"
<< "\n"
<< "# Generated migration sources are large and intentionally literal; skip clang-tidy\n"
<< "# so lint thresholds (function size, cognitive complexity) don't trip on them.\n"
<< "set_target_properties(" << pluginName << " PROPERTIES CXX_CLANG_TIDY \"\")\n"
<< "set_source_files_properties(${" << pluginName << "_MIGRATIONS} PROPERTIES SKIP_LINTING TRUE)\n";
}

auto const pluginPath = outputDir / "Plugin.cpp";
{
std::ofstream out(pluginPath);
if (!out.is_open())
throw std::runtime_error(std::format("Failed to open output file: {}", pluginPath.string()));
out << "// SPDX-License-Identifier: Apache-2.0\n"
<< "// Auto-generated. DO NOT EDIT.\n"
<< "//\n"
<< "// Migration plugin entry point. Individual migrations self-register with the\n"
<< "// MigrationManager via static initialization; this file only exposes the\n"
<< "// plugin ABI that dbtool expects when dlopen'ing the shared module.\n"
<< "\n"
<< "#include <Lightweight/SqlMigration.hpp>\n"
<< "\n"
<< "LIGHTWEIGHT_MIGRATION_PLUGIN()\n";
}
}

} // namespace Lightweight::CodeGen
78 changes: 78 additions & 0 deletions src/Lightweight/CodeGen/SplitFileWriter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include "../Api.hpp"

#include <cstddef>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>

namespace Lightweight::CodeGen
{

/// @brief A pre-rendered text block plus its line count, ready to be packed into
/// one or more output files by `EmitChunked`.
///
/// The block is treated as opaque — `EmitChunked` never splits a block in two.
/// Callers compute `lineCount` once and reuse the same value across multiple
/// passes, since counting newlines on every visit would be wasteful for large
/// migrations.
struct CodeBlock
{
/// Pre-rendered text of the block. Treated as opaque by `EmitChunked` —
/// never split across files.
std::string content;

/// Number of newlines in `content`, computed once by the caller and reused
/// across packing passes.
std::size_t lineCount = 0;
};

/// @brief Greedy bin-packing of `blocks` across files of at most `maxLinesPerFile`
/// lines.
///
/// One block always lands wholly in one chunk — even when its line count exceeds
/// the budget — because builder DSL chains cannot be broken across translation
/// units. The returned outer vector has no empty inner vectors. When
/// `maxLinesPerFile == 0`, every block lands in a single chunk.
[[nodiscard]] LIGHTWEIGHT_API std::vector<std::vector<CodeBlock>> GroupBlocksByLineBudget(
std::vector<CodeBlock> const& blocks, std::size_t maxLinesPerFile);

/// @brief Result of an `EmitChunked` call.
struct WriteResult
{
/// All files actually written, in apply order. Either `[outputPath]` (single
/// file) or `[<stem>_part01.<ext>, <stem>_part02.<ext>, ...]`.
std::vector<std::filesystem::path> writtenFiles;
};

/// @brief Writes one or more output files, splitting `blocks` across `<stem>_partNN.<ext>`
/// siblings when their combined line count exceeds `maxLinesPerFile`. When the
/// total fits, a single `outputPath` is written and split is skipped.
///
/// `fileHeader` and `fileFooter` are emitted at the top/bottom of every produced
/// file (so all chunks remain self-contained translation units when used for
/// `.cpp` outputs). Pass empty strings to skip them.
///
/// @return `WriteResult` listing the paths actually written.
/// @throws `std::runtime_error` if any output file cannot be opened.
[[nodiscard]] LIGHTWEIGHT_API WriteResult EmitChunked(std::filesystem::path const& outputPath,
std::vector<CodeBlock> const& blocks,
std::size_t maxLinesPerFile,
std::string_view fileHeader = {},
std::string_view fileFooter = {});

/// @brief Writes a `CMakeLists.txt` and a `Plugin.cpp` next to the generated
/// migration sources so the output directory becomes a drop-in plugin.
///
/// Mirrors the layout of `src/tools/LupMigrationsPlugin/` so consumers can
/// `add_subdirectory()` the generated dir and pick up a self-registering plugin
/// without further glue.
LIGHTWEIGHT_API void EmitPluginCmake(std::filesystem::path const& outputDir,
std::string_view pluginName,
std::string_view sourceGlob = "lup_*.cpp");

} // namespace Lightweight::CodeGen
Loading
Loading