1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#pragma once
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include "common/api.h"
#include "common/arrow/arrow.h"
#include "common/types/value/value.h"
#include "query_summary.h"
namespace lbug {
namespace processor {
class PhysicalPlan;
struct ResultSetDescriptor;
} // namespace processor
} // namespace lbug
namespace lbug {
namespace common {
class LogicalType;
}
namespace parser {
class Statement;
}
namespace binder {
class Expression;
class ParameterExpression;
} // namespace binder
namespace planner {
class LogicalPlan;
}
namespace main {
class CachedPreparedStatementManager;
// Prepared statement cached in client context and NEVER serialized to client side.
struct CachedPreparedStatement {
bool useInternalCatalogEntry = false;
std::shared_ptr<parser::Statement> parsedStatement;
std::unique_ptr<planner::LogicalPlan> logicalPlan;
std::vector<std::shared_ptr<binder::Expression>> columns;
std::vector<std::string> columnNames;
// Cached physical plan for fast re-execution. When canReuseCachedPlanWith returns true,
// this operator-tree template is cloned and its sink state is refreshed for each call,
// avoiding the PlanMapper::mapOperator recursion entirely.
std::unique_ptr<processor::PhysicalPlan> physicalPlanCache;
// Cached ResultSetDescriptors of every pipeline-head sink, in preorder position order.
// Operator::copy() does not propagate descriptors, and multi-pipeline plans (e.g.
// aggregates) clone into several tasks that each need a descriptor to build their
// ResultSet, so we snapshot them all from the freshly mapped tree and re-attach them onto
// each cloned instance.
std::vector<std::unique_ptr<processor::ResultSetDescriptor>> sinkResultSetDescriptors;
// Every typed parameter bound for this statement. Scanned for parameters whose values
// were frozen into a plan at plan-build time (ParameterExpression::wasBakedIntoPlan) —
// e.g. SKIP/LIMIT numbers baked to uint64_t by the mapper, or evaluated numbers baked
// into operators by optimizers. The scan is operator-agnostic, so future operators that
// bake parameter values (through a marking helper) are covered without touching this
// code (see https://github.com/LadybugDB/ladybug/issues/985).
std::vector<std::shared_ptr<binder::ParameterExpression>> boundParameters;
// True when a bound parameter was baked into a plan during prepare (bind/optimize), or
// during a previous physical mapping. Such statements always rebind/replan on
// re-execution and never populate or serve the physical-plan cache, since a cached
// plan would keep serving the first execution's frozen values.
bool hasBakedParameters = false;
// Catalog::getPlanVersion() the logical plan was bound against. If the catalog changed
// (e.g. ALTER TABLE ADD) or a checkpoint renumbered column IDs since prepare, the cached
// logical/physical plan is stale and must be rebound (see
// https://github.com/LadybugDB/ladybug/issues/1158).
uint64_t planVersion = 0;
// Pool of cloned physical plans ready for reuse, keyed by executing thread.
// Cloning the cached template per execution costs hundreds of small allocations
// (operators, evaluators, print infos, schemas); pooled plans are reset via
// prepareForReuse() and reused instead. Capped per thread: deeper reentrancy
// (nested same-statement execution) falls back to fresh clones. Guarded by
// physicalPlanPoolMutex; entries are dropped with the statement.
// Thread affinity is a perf hint only: plans are interchangeable across threads.
// A stale entry from an exited thread is therefore safe to hand to a new thread
// whose std::thread::id happens to compare equal, and empty per-thread vectors
// are pruned on checkout so short-lived-thread churn cannot grow the map
// unboundedly (see checkoutPooledPhysicalPlan / recyclePooledPhysicalPlan).
// Total retained plans are bounded by MAX_POOLED_THREADS * per-thread limit.
// The per-thread limit is runtime-configurable via the
// `physical_plan_pool_per_thread` setting (DBConfig); 0 disables pooling.
// Defaults below mirror the setting's default; the pool code reads the live
// setting rather than these constants.
static constexpr size_t PHYSICAL_PLAN_POOL_PER_THREAD_DEFAULT = 5;
static constexpr size_t PHYSICAL_PLAN_POOL_MAX_THREADS = 32;
std::mutex physicalPlanPoolMutex;
std::unordered_map<std::thread::id, std::vector<std::unique_ptr<processor::PhysicalPlan>>>
physicalPlanPool;
CachedPreparedStatement();
~CachedPreparedStatement();
std::vector<std::string> getColumnNames() const;
std::vector<common::LogicalType> getColumnTypes() const;
};
/**
* @brief A prepared statement is a parameterized query which can avoid planning the same query for
* repeated execution.
*/
class PreparedStatement {
friend class Connection;
friend class ClientContext;
public:
LBUG_API ~PreparedStatement();
/**
* @return the query is prepared successfully or not.
*/
LBUG_API bool isSuccess() const;
/**
* @return the error message if the query is not prepared successfully.
*/
LBUG_API std::string getErrorMessage() const;
/**
* @return the prepared statement is read-only or not.
*/
LBUG_API bool isReadOnly() const;
/**
* @return the column names of the query result, populated at prepare time
* (parse + bind + plan) without executing the query.
*/
LBUG_API std::vector<std::string> getColumnNames() const;
/**
* @return the column data types of the query result, populated at prepare time
* without executing the query.
*/
LBUG_API std::vector<common::LogicalType> getColumnTypes() const;
/**
* @brief Returns the arrow schema of the query result, derived from the bound
* and planned statement without executing the query.
* @return datatypes of the columns as an arrow schema
*
* It is the caller's responsibility to call the release function to release the underlying
* data. If converting to another arrow type, this is usually handled automatically.
*/
LBUG_API std::unique_ptr<ArrowSchema> getArrowSchema() const;
const std::unordered_set<std::string>& getUnknownParameters() const {
return unknownParameters;
}
bool canReuseCachedPlanWith(
const std::unordered_map<std::string, std::unique_ptr<common::Value>>& inputParams) const;
std::unordered_set<std::string> getKnownParameters();
void updateParameter(const std::string& name, common::Value* value);
void addParameter(const std::string& name, common::Value* value);
LBUG_API void setParameter(const std::string& name, common::Value value);
std::string getName() const { return cachedPreparedStatementName; }
common::StatementType getStatementType() const;
static std::unique_ptr<PreparedStatement> getPreparedStatementWithError(
const std::string& errorMessage);
private:
bool success = true;
bool readOnly = true;
std::string errMsg;
PreparedSummary preparedSummary;
std::string cachedPreparedStatementName;
// Weak back-reference to the manager this statement was registered with (via
// ClientContext::prepareWithParams). The destructor uses it to unregister itself so the
// cached plan is freed promptly. Weak so that a statement destroyed after its connection
// (possible through the C API) does not access a freed manager.
std::weak_ptr<CachedPreparedStatementManager> ownerManager;
std::unordered_set<std::string> unknownParameters;
// Set once an execution has bound the statement with values for its unknown parameters.
bool unknownParametersBound = false;
std::unordered_map<std::string, std::shared_ptr<common::Value>> parameterMap;
};
} // namespace main
} // namespace lbug