Skip to content

Commit 9e264da

Browse files
committed
feat: prune OR/range, pool PostgreSQL, 2PC stress, honest docs
Prune shard_key OR-of-equalities and RANGE inequalities/BETWEEN. Placeholders still scatter. ThreadSafeMultiRemoteExecutor pools PostgreSQL as well as MySQL. engine_stress_test uses DistributedTransactionManager. README matches current Session/ShardMap and tool 2PC behavior.
1 parent 7e0a05f commit 9e264da

10 files changed

Lines changed: 558 additions & 22 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Trust the `Makefile` over prose. Extension recipes live in `CLAUDE.md`. `docs/su
77
- Parser: header-only templates in `include/sql_parser/` except `src/sql_parser/{arena,parser}.cpp`
88
- Engine: headers in `include/sql_engine/` (`operators/`, `functions/`, `rules/`); compiled files are the explicit `ENGINE_SRCS` list
99
- High-level API: `Session<D>` (`include/sql_engine/session.h`) — parse → plan → optimize → distribute → execute
10-
- Production remote path: `ThreadSafeMultiRemoteExecutor`, not the single-connection executors
10+
- Production remote path: `ThreadSafeMultiRemoteExecutor` (pooled MySQL **and** PostgreSQL), not the single-connection executors
1111
- All shard routing (SELECT prune and DML) goes through `ShardMap`. Do not add a private hash in the planner.
1212
- Backend URL / shard-spec parsing: `tool_config_parser` — do not add another copy in tools
1313
- Do not edit `third_party/`

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ echo "SELECT 1 + 2, UPPER('hello'), COALESCE(NULL, 42)" | ./sqlengine
9393
# Against a MySQL backend
9494
./sqlengine --backend "mysql://root:pass@127.0.0.1:3306/mydb?name=primary"
9595

96-
# Sharded across two backends
96+
# Sharded across two backends (2PC is on; optional --txn-log PATH)
9797
./sqlengine \
9898
--backend "mysql://root:pass@host1:3306/db?name=shard1" \
9999
--backend "mysql://root:pass@host2:3306/db?name=shard2" \
@@ -171,20 +171,20 @@ ResultSet rs = executor.execute(plan);
171171
#include "sql_engine/session.h"
172172
#include "sql_engine/thread_safe_executor.h"
173173
#include "sql_engine/shard_map.h"
174-
#include "sql_engine/local_txn.h"
174+
#include "sql_engine/distributed_txn.h"
175175

176-
// Backends (connection-pooled, thread-safe)
176+
// Backends (connection-pooled, thread-safe; MySQL or PostgreSQL)
177177
ThreadSafeMultiRemoteExecutor executor;
178178
executor.add_backend({.name = "shard1", .host = "h1", .port = 3306, ...});
179179
executor.add_backend({.name = "shard2", .host = "h2", .port = 3306, ...});
180180

181181
// Sharding policy: "users" is sharded on "id" across shard1, shard2
182182
ShardMap shards;
183-
shards.add_sharded_table("users", "id", {"shard1", "shard2"});
183+
shards.add_table({"users", "id", {{"shard1"}, {"shard2"}}});
184184

185-
// Catalog, transactions, session
185+
// Catalog + 2PC (required for atomic multi-shard DML)
186186
InMemoryCatalog catalog; /* ... add_table(...) ... */
187-
LocalTransactionManager txn;
187+
DistributedTransactionManager txn(executor);
188188
Session<Dialect::MySQL> session(catalog, txn);
189189
session.set_remote_executor(&executor);
190190
session.set_shard_map(&shards);
@@ -354,11 +354,11 @@ auto report = recovery.recover();
354354
355355
### Distributed execution
356356
357-
- **Shard routing** — shard-key lookups go to one backend; scatter queries go to all
358-
- **Distributed aggregation** — per-shard partial aggregates + coordinator merge (COUNT+SUM+MIN+MAX + AVG from SUM/COUNT)
359-
- **Distributed sort** — per-shard sort + coordinator merge
360-
- **Cross-shard joins** — hash-join coordinator; materialized subquery cache
361-
- **Cross-shard DML** — scatter INSERT/UPDATE/DELETE when no shard key; single-shard when key present
357+
- **Shard routing** — equality / `IN` / `OR` of equalities prune via `ShardMap`; RANGE also prunes `<`/`>`/`BETWEEN`. Placeholders scatter.
358+
- **Distributed aggregation** — per-shard partial aggregates + coordinator merge (`COUNT`/`SUM`/`MIN`/`MAX`/`AVG`). `COUNT(DISTINCT)` gathers then aggregates locally.
359+
- **Distributed sort** — per-shard sort + coordinator merge when keys are table columns
360+
- **Joins** — co-located same-key joins push down; otherwise gather both sides and join locally
361+
- **Cross-shard DML** — routed by `ShardMap`; missing/non-literal shard key and multi-table DML on shards fail closed
362362
- **Cross-shard INSERT ... SELECT** — source materialized, rows routed by destination shard key
363363
364364
### Transactions
@@ -372,10 +372,10 @@ auto report = recovery.recover();
372372
373373
### Backends & connectivity
374374
375-
- **MySQL** — libmysqlclient with pooled and single-connection paths, UTF-8, configurable timeouts
376-
- **PostgreSQL** — libpq with statement_timeout, UTC-normalized TIMESTAMPTZ handling
375+
- **MySQL** — libmysqlclient with pooled (`ThreadSafeMultiRemoteExecutor`) and single-connection paths
376+
- **PostgreSQL** — libpq pooled on the same executor, plus a single-connection path; `statement_timeout` and UTC TIMESTAMPTZ
377377
- **SSL/TLS** — `ssl_mode`, `ssl_ca`, `ssl_cert`, `ssl_key` configurable per backend for both dialects
378-
- **Connection pool** — thread-safe with health checks, reconnection, RAII `ConnectionGuard`
378+
- **Connection pool** — thread-safe per dialect, RAII checkout, poison-on-error
379379
- **MySQL wire-protocol server** — `mysql_server` speaks the MySQL protocol; backends are ParserSQL engines
380380
381381
### Thread-safety
@@ -388,7 +388,7 @@ auto report = recovery.recover();
388388
389389
| Tool | Build | Purpose |
390390
|---|---|---|
391-
| `sqlengine` | `make build-sqlengine` | Interactive SQL CLI; stdin, one-shot, or REPL; optional backends and sharding |
391+
| `sqlengine` | `make build-sqlengine` | Interactive SQL CLI; 2PC when `--backend` is set; optional `--txn-log` |
392392
| `mysql_server` | `make mysql-server` | MySQL wire-protocol server fronted by the ParserSQL engine |
393393
| `corpus_test` | `make build-corpus-test` | Read SQL from stdin/files, parse each, report OK/PARTIAL/ERROR |
394394
| `engine_stress_test` | `make engine-stress` | Direct-API engine stress test |

include/sql_engine/distributed_planner.h

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <cstring>
1919
#include <cstdio>
2020
#include <cstdlib>
21+
#include <climits>
2122
#include <vector>
2223
#include <unordered_map>
2324
#include <functional>
@@ -401,6 +402,62 @@ class DistributedPlanner {
401402
}
402403
}
403404
}
405+
if (is_compare_op(op) &&
406+
shards_.routing_strategy(table_name) == RoutingStrategy::RANGE) {
407+
const sql_parser::AstNode* left_node = expr->first_child;
408+
const sql_parser::AstNode* right_node = left_node ? left_node->next_sibling : nullptr;
409+
if (left_node && right_node) {
410+
const sql_parser::AstNode* col = nullptr;
411+
const sql_parser::AstNode* lit = nullptr;
412+
bool key_on_left = false;
413+
if (is_shard_key_ref(left_node, shard_key) && is_literal(right_node)) {
414+
col = left_node; lit = right_node; key_on_left = true;
415+
} else if (is_shard_key_ref(right_node, shard_key) && is_literal(left_node)) {
416+
col = right_node; lit = left_node; key_on_left = false;
417+
}
418+
if (col && lit) {
419+
int64_t v = literal_to_int(lit);
420+
int64_t lo = INT64_MIN, hi = INT64_MAX;
421+
char c0 = op.ptr[0];
422+
bool has_eq = op.len == 2 && op.ptr[1] == '=';
423+
if (c0 == '<' && key_on_left) {
424+
hi = has_eq ? v : (v == INT64_MIN ? INT64_MIN : v - 1);
425+
} else if (c0 == '>' && key_on_left) {
426+
lo = has_eq ? v : (v == INT64_MAX ? INT64_MAX : v + 1);
427+
} else if (c0 == '<' && !key_on_left) {
428+
lo = has_eq ? v : (v == INT64_MAX ? INT64_MAX : v + 1);
429+
} else if (c0 == '>' && !key_on_left) {
430+
hi = has_eq ? v : (v == INT64_MIN ? INT64_MIN : v - 1);
431+
}
432+
shards_.collect_int_range_shards(table_name, lo, hi, target_indices);
433+
return;
434+
}
435+
}
436+
}
437+
if (op.len == 2 &&
438+
(op.ptr[0] == 'O' || op.ptr[0] == 'o') &&
439+
(op.ptr[1] == 'R' || op.ptr[1] == 'r')) {
440+
const sql_parser::AstNode* left_node = expr->first_child;
441+
const sql_parser::AstNode* right_node = left_node ? left_node->next_sibling : nullptr;
442+
std::vector<size_t> left_targets, right_targets;
443+
extract_shard_targets(left_node, shard_key, table_name, num_shards, left_targets);
444+
extract_shard_targets(right_node, shard_key, table_name, num_shards, right_targets);
445+
if (left_targets.empty() || right_targets.empty()) return;
446+
std::vector<bool> seen(num_shards, false);
447+
for (auto i : left_targets) {
448+
if (i < num_shards && !seen[i]) {
449+
seen[i] = true;
450+
target_indices.push_back(i);
451+
}
452+
}
453+
for (auto i : right_targets) {
454+
if (i < num_shards && !seen[i]) {
455+
seen[i] = true;
456+
target_indices.push_back(i);
457+
}
458+
}
459+
return;
460+
}
404461
// Recurse into AND branches
405462
if (op.len == 3 &&
406463
(op.ptr[0] == 'A' || op.ptr[0] == 'a') &&
@@ -446,6 +503,29 @@ class DistributedPlanner {
446503
}
447504
}
448505
}
506+
507+
if (expr->type == sql_parser::NodeType::NODE_BETWEEN &&
508+
shards_.routing_strategy(table_name) == RoutingStrategy::RANGE) {
509+
const sql_parser::AstNode* col = expr->first_child;
510+
const sql_parser::AstNode* lo = col ? col->next_sibling : nullptr;
511+
const sql_parser::AstNode* hi = lo ? lo->next_sibling : nullptr;
512+
if (col && is_shard_key_ref(col, shard_key) && is_literal(lo) && is_literal(hi)) {
513+
shards_.collect_int_range_shards(table_name,
514+
literal_to_int(lo), literal_to_int(hi),
515+
target_indices);
516+
}
517+
}
518+
}
519+
520+
static bool is_compare_op(sql_parser::StringRef op) {
521+
if (op.len == 1) return op.ptr[0] == '<' || op.ptr[0] == '>';
522+
if (op.len == 2) return (op.ptr[0] == '<' || op.ptr[0] == '>') && op.ptr[1] == '=';
523+
return false;
524+
}
525+
526+
static int64_t literal_to_int(const sql_parser::AstNode* lit) {
527+
if (!lit || !lit->value().ptr) return 0;
528+
return std::strtoll(lit->value().ptr, nullptr, 10);
449529
}
450530

451531
bool is_shard_key_ref(const sql_parser::AstNode* node, sql_parser::StringRef shard_key) const {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#ifndef SQL_ENGINE_PG_CONNECTION_POOL_H
2+
#define SQL_ENGINE_PG_CONNECTION_POOL_H
3+
4+
#include "sql_engine/backend_config.h"
5+
#include <libpq-fe.h>
6+
#include <mutex>
7+
#include <unordered_map>
8+
#include <vector>
9+
#include <string>
10+
#include <stdexcept>
11+
#include <memory>
12+
13+
#ifndef SQL_ENGINE_PG_STATEMENT_TIMEOUT_MS
14+
#define SQL_ENGINE_PG_STATEMENT_TIMEOUT_MS 30000
15+
#endif
16+
17+
namespace sql_engine {
18+
19+
class PgConnectionPool {
20+
public:
21+
PgConnectionPool() = default;
22+
23+
~PgConnectionPool() {
24+
for (auto& kv : backends_) {
25+
auto& be = *kv.second;
26+
std::lock_guard<std::mutex> lk(be.mu);
27+
for (PGconn* c : be.idle) {
28+
if (c) PQfinish(c);
29+
}
30+
}
31+
}
32+
33+
void add_backend(const BackendConfig& config) {
34+
auto be = std::make_unique<Backend>();
35+
be->config = config;
36+
backends_[config.name] = std::move(be);
37+
}
38+
39+
bool has_backend(const std::string& name) const {
40+
return backends_.find(name) != backends_.end();
41+
}
42+
43+
PGconn* checkout(const std::string& backend) {
44+
Backend& be = get_backend(backend);
45+
{
46+
std::lock_guard<std::mutex> lk(be.mu);
47+
if (!be.idle.empty()) {
48+
PGconn* c = be.idle.back();
49+
be.idle.pop_back();
50+
if (c && PQstatus(c) == CONNECTION_OK) return c;
51+
if (c) PQfinish(c);
52+
}
53+
}
54+
return create_connection(be);
55+
}
56+
57+
void checkin(const std::string& backend, PGconn* conn) {
58+
if (!conn) return;
59+
Backend& be = get_backend(backend);
60+
std::lock_guard<std::mutex> lk(be.mu);
61+
be.idle.push_back(conn);
62+
}
63+
64+
private:
65+
struct Backend {
66+
BackendConfig config;
67+
std::mutex mu;
68+
std::vector<PGconn*> idle;
69+
};
70+
71+
std::unordered_map<std::string, std::unique_ptr<Backend>> backends_;
72+
73+
Backend& get_backend(const std::string& name) {
74+
auto it = backends_.find(name);
75+
if (it == backends_.end()) {
76+
throw std::runtime_error("PgConnectionPool: unknown backend: " + name);
77+
}
78+
return *it->second;
79+
}
80+
81+
static PGconn* create_connection(Backend& be) {
82+
const BackendConfig& cfg = be.config;
83+
std::string conninfo = "host=" + cfg.host
84+
+ " port=" + std::to_string(cfg.port)
85+
+ " user=" + cfg.user
86+
+ " password=" + cfg.password
87+
+ " dbname=" + cfg.database
88+
+ " connect_timeout=5"
89+
+ " options='-c statement_timeout="
90+
+ std::to_string(SQL_ENGINE_PG_STATEMENT_TIMEOUT_MS) + "'";
91+
if (!cfg.ssl_mode.empty()) conninfo += " sslmode=" + cfg.ssl_mode;
92+
if (!cfg.ssl_ca.empty()) conninfo += " sslrootcert=" + cfg.ssl_ca;
93+
if (!cfg.ssl_cert.empty()) conninfo += " sslcert=" + cfg.ssl_cert;
94+
if (!cfg.ssl_key.empty()) conninfo += " sslkey=" + cfg.ssl_key;
95+
96+
PGconn* c = PQconnectdb(conninfo.c_str());
97+
if (PQstatus(c) != CONNECTION_OK) {
98+
std::string err = PQerrorMessage(c);
99+
PQfinish(c);
100+
throw std::runtime_error("PgConnectionPool connect failed for " + cfg.name + ": " + err);
101+
}
102+
return c;
103+
}
104+
};
105+
106+
} // namespace sql_engine
107+
108+
#endif

include/sql_engine/shard_map.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "sql_parser/common.h"
55
#include <cstdint>
6+
#include <climits>
67
#include <cstring>
78
#include <string>
89
#include <vector>
@@ -155,6 +156,32 @@ class ShardMap {
155156
return 0;
156157
}
157158

159+
RoutingStrategy routing_strategy(sql_parser::StringRef table_name) const {
160+
const TableShardConfig* cfg = lookup(table_name);
161+
return cfg ? cfg->strategy : RoutingStrategy::HASH;
162+
}
163+
164+
// RANGE only. Inclusive [lo, hi]. HASH/LIST yield no indices (caller scatters).
165+
void collect_int_range_shards(sql_parser::StringRef table_name,
166+
int64_t lo, int64_t hi,
167+
std::vector<size_t>& out) const {
168+
const TableShardConfig* cfg = lookup(table_name);
169+
if (!cfg || cfg->strategy != RoutingStrategy::RANGE || cfg->ranges.empty())
170+
return;
171+
if (lo > hi) return;
172+
size_t n = cfg->shards.size();
173+
const auto& ranges = cfg->ranges;
174+
for (size_t i = 0; i < ranges.size(); ++i) {
175+
int64_t seg_hi = (i + 1 == ranges.size())
176+
? INT64_MAX : ranges[i].upper_inclusive;
177+
int64_t seg_lo = (i == 0) ? INT64_MIN
178+
: (cfg->ranges[i - 1].upper_inclusive == INT64_MAX
179+
? INT64_MAX : cfg->ranges[i - 1].upper_inclusive + 1);
180+
if (seg_lo <= hi && seg_hi >= lo)
181+
out.push_back(clamp_index(ranges[i].shard_index, n));
182+
}
183+
}
184+
158185
bool same_routing(sql_parser::StringRef a, sql_parser::StringRef b) const {
159186
const TableShardConfig* ca = lookup(a);
160187
const TableShardConfig* cb = lookup(b);

0 commit comments

Comments
 (0)