quickhouse 0.3.2__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quickhouse-0.3.2 → quickhouse-0.3.4}/Cargo.lock +4 -4
- {quickhouse-0.3.2 → quickhouse-0.3.4}/Cargo.toml +1 -1
- {quickhouse-0.3.2 → quickhouse-0.3.4}/PKG-INFO +25 -1
- {quickhouse-0.3.2 → quickhouse-0.3.4}/README.md +24 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/config.rs +34 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/ddl.rs +1 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/bigquery.rs +1 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/postgres.rs +15 -7
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sync.rs +422 -217
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/transform.rs +137 -1
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/types.rs +43 -2
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-py/src/lib.rs +3 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/pyproject.toml +1 -1
- {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/_quickhouse.pyi +33 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/LICENSE +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/Cargo.toml +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/archive.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decimal.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decode.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decode_bigquery.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decode_mysql.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/error.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/lib.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/memory.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/bigquery_proto.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/clickhouse.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/mod.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/bigquery.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/mod.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/mysql.rs +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-py/Cargo.toml +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/__init__.py +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/progress.py +0 -0
- {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/py.typed +0 -0
|
@@ -2634,7 +2634,7 @@ dependencies = [
|
|
|
2634
2634
|
|
|
2635
2635
|
[[package]]
|
|
2636
2636
|
name = "quickhouse-core"
|
|
2637
|
-
version = "0.3.
|
|
2637
|
+
version = "0.3.4"
|
|
2638
2638
|
dependencies = [
|
|
2639
2639
|
"anyhow",
|
|
2640
2640
|
"arrow",
|
|
@@ -2670,7 +2670,7 @@ dependencies = [
|
|
|
2670
2670
|
|
|
2671
2671
|
[[package]]
|
|
2672
2672
|
name = "quickhouse-py"
|
|
2673
|
-
version = "0.3.
|
|
2673
|
+
version = "0.3.4"
|
|
2674
2674
|
dependencies = [
|
|
2675
2675
|
"pyo3",
|
|
2676
2676
|
"quickhouse-core",
|
|
@@ -3038,9 +3038,9 @@ dependencies = [
|
|
|
3038
3038
|
|
|
3039
3039
|
[[package]]
|
|
3040
3040
|
name = "rustls-pki-types"
|
|
3041
|
-
version = "1.15.
|
|
3041
|
+
version = "1.15.1"
|
|
3042
3042
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3043
|
-
checksum = "
|
|
3043
|
+
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
|
3044
3044
|
dependencies = [
|
|
3045
3045
|
"web-time",
|
|
3046
3046
|
"zeroize",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quickhouse
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -72,6 +72,16 @@ print(result) # rows_read, rows_written, bytes_written, duration_secs, new_wat
|
|
|
72
72
|
legacy quirks like MySQL zero-dates or out-of-range timestamps are coerced to
|
|
73
73
|
`NULL` with a warning instead of aborting the run.
|
|
74
74
|
|
|
75
|
+
- **It's gentle on a small production database.** Set `read_max_rows_per_sec`
|
|
76
|
+
and quickhouse paces the read to that aggregate rate across all partitions;
|
|
77
|
+
because `COPY`/streaming results only produce as fast as the client consumes,
|
|
78
|
+
the source scan itself backs off — you're throttling the database's work, not
|
|
79
|
+
just your own. Combine it with `parallelism=1` (one connection), incremental
|
|
80
|
+
mode (only new rows), and a `statement_timeout_secs`, point it at a read
|
|
81
|
+
replica, and a bulk export stops competing with your app. The Postgres
|
|
82
|
+
connection also shows up as `application_name = 'quickhouse'` in
|
|
83
|
+
`pg_stat_activity`, so a DBA can see and kill it.
|
|
84
|
+
|
|
75
85
|
- **There's nothing to stand up.** `pip install quickhouse` and you're done —
|
|
76
86
|
no JVM, no Spark cluster, no separate service. It's an ordinary Python
|
|
77
87
|
dependency that runs wherever your jobs already run: cron, Airflow, Dagster,
|
|
@@ -180,6 +190,12 @@ deduplicated on `key` — via ClickHouse's `ReplacingMergeTree`, or a `MERGE`
|
|
|
180
190
|
upsert on BigQuery (where `key` is therefore required). Re-running with no new
|
|
181
191
|
data does nothing.
|
|
182
192
|
|
|
193
|
+
Both modes stage through a per-run-unique table (`{dest}_quickhouse_tmp_<id>`)
|
|
194
|
+
that's dropped when the run finishes, including on failure. The unique name is
|
|
195
|
+
what makes rapid re-runs and whole-call retries safe on BigQuery, whose
|
|
196
|
+
streaming ingestion rejects writes into a table recently recreated under the
|
|
197
|
+
same name.
|
|
198
|
+
|
|
183
199
|
For daily syncs that need to catch late-arriving or edited rows, set
|
|
184
200
|
`lookback_seconds` to re-scan a trailing window (e.g. `3 * 86400` for the last
|
|
185
201
|
three days) — the dedup above keeps that overlap from creating duplicates.
|
|
@@ -212,6 +228,7 @@ errors are surfaced verbatim rather than wrapped in something generic.
|
|
|
212
228
|
| `parallelism` | Concurrent read streams |
|
|
213
229
|
| `batch_rows` / `batch_bytes` | Per-batch size knobs (rows, or estimated bytes) |
|
|
214
230
|
| `max_memory_bytes` | Hard ceiling on total in-flight memory; decoding blocks when hit (default 512 MiB, `0` = unbounded) |
|
|
231
|
+
| `read_max_rows_per_sec` | Cap the aggregate source read rate to be gentle on a small DB; `None` = unlimited (default). Postgres/MySQL only |
|
|
215
232
|
| `type_overrides` | Force a destination column type, e.g. `{"qty": "Decimal(18, 3)"}` |
|
|
216
233
|
| `rename`, `include`, `exclude` | Column renames and allow/deny lists |
|
|
217
234
|
| `on_progress` | Progress callback |
|
|
@@ -234,6 +251,13 @@ knowing:
|
|
|
234
251
|
front, rather than silently falling back to `Float64`.
|
|
235
252
|
- **`TIME`** columns transfer as canonical text into a `String` column
|
|
236
253
|
(ClickHouse has no time-of-day type).
|
|
254
|
+
- **MySQL `DATETIME`/`TIMESTAMP`** map to a UTC-aware timestamp (BigQuery
|
|
255
|
+
`TIMESTAMP`, ClickHouse `DateTime64(6, 'UTC')`) — the wall-clock value is read
|
|
256
|
+
as UTC, matching how a `TIMESTAMP` column expects it. To land a column as a
|
|
257
|
+
naive BigQuery `DATETIME` instead, opt out per-column with
|
|
258
|
+
`type_overrides={"col": "DATETIME"}` — that flips the actual encoding, not
|
|
259
|
+
just the declared type. (PostgreSQL keeps the distinction natively:
|
|
260
|
+
`timestamptz` → UTC-aware, `timestamp` → naive.)
|
|
237
261
|
- **Out-of-range dates** (and MySQL zero-dates like `0000-00-00`) coerce to
|
|
238
262
|
`NULL` with a warning rather than failing the transfer.
|
|
239
263
|
- **Nullable** source columns stay nullable in the destination.
|
|
@@ -40,6 +40,16 @@ print(result) # rows_read, rows_written, bytes_written, duration_secs, new_wat
|
|
|
40
40
|
legacy quirks like MySQL zero-dates or out-of-range timestamps are coerced to
|
|
41
41
|
`NULL` with a warning instead of aborting the run.
|
|
42
42
|
|
|
43
|
+
- **It's gentle on a small production database.** Set `read_max_rows_per_sec`
|
|
44
|
+
and quickhouse paces the read to that aggregate rate across all partitions;
|
|
45
|
+
because `COPY`/streaming results only produce as fast as the client consumes,
|
|
46
|
+
the source scan itself backs off — you're throttling the database's work, not
|
|
47
|
+
just your own. Combine it with `parallelism=1` (one connection), incremental
|
|
48
|
+
mode (only new rows), and a `statement_timeout_secs`, point it at a read
|
|
49
|
+
replica, and a bulk export stops competing with your app. The Postgres
|
|
50
|
+
connection also shows up as `application_name = 'quickhouse'` in
|
|
51
|
+
`pg_stat_activity`, so a DBA can see and kill it.
|
|
52
|
+
|
|
43
53
|
- **There's nothing to stand up.** `pip install quickhouse` and you're done —
|
|
44
54
|
no JVM, no Spark cluster, no separate service. It's an ordinary Python
|
|
45
55
|
dependency that runs wherever your jobs already run: cron, Airflow, Dagster,
|
|
@@ -148,6 +158,12 @@ deduplicated on `key` — via ClickHouse's `ReplacingMergeTree`, or a `MERGE`
|
|
|
148
158
|
upsert on BigQuery (where `key` is therefore required). Re-running with no new
|
|
149
159
|
data does nothing.
|
|
150
160
|
|
|
161
|
+
Both modes stage through a per-run-unique table (`{dest}_quickhouse_tmp_<id>`)
|
|
162
|
+
that's dropped when the run finishes, including on failure. The unique name is
|
|
163
|
+
what makes rapid re-runs and whole-call retries safe on BigQuery, whose
|
|
164
|
+
streaming ingestion rejects writes into a table recently recreated under the
|
|
165
|
+
same name.
|
|
166
|
+
|
|
151
167
|
For daily syncs that need to catch late-arriving or edited rows, set
|
|
152
168
|
`lookback_seconds` to re-scan a trailing window (e.g. `3 * 86400` for the last
|
|
153
169
|
three days) — the dedup above keeps that overlap from creating duplicates.
|
|
@@ -180,6 +196,7 @@ errors are surfaced verbatim rather than wrapped in something generic.
|
|
|
180
196
|
| `parallelism` | Concurrent read streams |
|
|
181
197
|
| `batch_rows` / `batch_bytes` | Per-batch size knobs (rows, or estimated bytes) |
|
|
182
198
|
| `max_memory_bytes` | Hard ceiling on total in-flight memory; decoding blocks when hit (default 512 MiB, `0` = unbounded) |
|
|
199
|
+
| `read_max_rows_per_sec` | Cap the aggregate source read rate to be gentle on a small DB; `None` = unlimited (default). Postgres/MySQL only |
|
|
183
200
|
| `type_overrides` | Force a destination column type, e.g. `{"qty": "Decimal(18, 3)"}` |
|
|
184
201
|
| `rename`, `include`, `exclude` | Column renames and allow/deny lists |
|
|
185
202
|
| `on_progress` | Progress callback |
|
|
@@ -202,6 +219,13 @@ knowing:
|
|
|
202
219
|
front, rather than silently falling back to `Float64`.
|
|
203
220
|
- **`TIME`** columns transfer as canonical text into a `String` column
|
|
204
221
|
(ClickHouse has no time-of-day type).
|
|
222
|
+
- **MySQL `DATETIME`/`TIMESTAMP`** map to a UTC-aware timestamp (BigQuery
|
|
223
|
+
`TIMESTAMP`, ClickHouse `DateTime64(6, 'UTC')`) — the wall-clock value is read
|
|
224
|
+
as UTC, matching how a `TIMESTAMP` column expects it. To land a column as a
|
|
225
|
+
naive BigQuery `DATETIME` instead, opt out per-column with
|
|
226
|
+
`type_overrides={"col": "DATETIME"}` — that flips the actual encoding, not
|
|
227
|
+
just the declared type. (PostgreSQL keeps the distinction natively:
|
|
228
|
+
`timestamptz` → UTC-aware, `timestamp` → naive.)
|
|
205
229
|
- **Out-of-range dates** (and MySQL zero-dates like `0000-00-00`) coerce to
|
|
206
230
|
`NULL` with a warning rather than failing the transfer.
|
|
207
231
|
- **Nullable** source columns stay nullable in the destination.
|
|
@@ -256,6 +256,17 @@ pub struct TransferConfig {
|
|
|
256
256
|
/// Column used to split the table into parallel range partitions.
|
|
257
257
|
/// Defaults to the first `key` column, else the sync falls back to a single stream.
|
|
258
258
|
pub partition_column: Option<String>,
|
|
259
|
+
/// Optional cap on how many source rows are pulled **per second**, summed
|
|
260
|
+
/// across all parallel partitions (a global limiter, not per-connection).
|
|
261
|
+
/// Deliberately paces the read so a small/production database isn't
|
|
262
|
+
/// hammered by a bulk export: after each batch is read, the reader sleeps
|
|
263
|
+
/// long enough to hold the aggregate rate at this ceiling, which — because
|
|
264
|
+
/// `COPY TO STDOUT` streams only as fast as the client consumes — makes the
|
|
265
|
+
/// server-side scan itself back off (TCP backpressure), not just the
|
|
266
|
+
/// client. `None` (default) reads as fast as possible. Applies to the
|
|
267
|
+
/// PostgreSQL and MySQL sources; ignored for a BigQuery source (its read
|
|
268
|
+
/// path is a managed, separately-metered API).
|
|
269
|
+
pub read_max_rows_per_sec: Option<u64>,
|
|
259
270
|
|
|
260
271
|
// ---- transforms ----
|
|
261
272
|
/// Per-column destination type overrides (column name -> the
|
|
@@ -330,6 +341,11 @@ impl TransferConfig {
|
|
|
330
341
|
"max_memory_bytes must be 0 (unbounded) or >= 65536",
|
|
331
342
|
));
|
|
332
343
|
}
|
|
344
|
+
if self.read_max_rows_per_sec == Some(0) {
|
|
345
|
+
return Err(EtlError::config(
|
|
346
|
+
"read_max_rows_per_sec must be None (unlimited) or >= 1",
|
|
347
|
+
));
|
|
348
|
+
}
|
|
333
349
|
Ok(())
|
|
334
350
|
}
|
|
335
351
|
}
|
|
@@ -367,6 +383,7 @@ mod tests {
|
|
|
367
383
|
batch_bytes: 0,
|
|
368
384
|
max_memory_bytes: 0,
|
|
369
385
|
partition_column: None,
|
|
386
|
+
read_max_rows_per_sec: None,
|
|
370
387
|
type_overrides: HashMap::new(),
|
|
371
388
|
rename: HashMap::new(),
|
|
372
389
|
include: vec![],
|
|
@@ -423,4 +440,21 @@ mod tests {
|
|
|
423
440
|
c.lookback_seconds = 60;
|
|
424
441
|
assert!(c.validate().is_ok());
|
|
425
442
|
}
|
|
443
|
+
|
|
444
|
+
#[test]
|
|
445
|
+
fn validate_rejects_zero_read_rate() {
|
|
446
|
+
let mut c = cfg(SyncMode::Full, None);
|
|
447
|
+
c.read_max_rows_per_sec = Some(0);
|
|
448
|
+
let err = c.validate().unwrap_err().to_string();
|
|
449
|
+
assert!(err.contains("read_max_rows_per_sec"), "got: {err}");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#[test]
|
|
453
|
+
fn validate_accepts_none_or_positive_read_rate() {
|
|
454
|
+
let mut c = cfg(SyncMode::Full, None);
|
|
455
|
+
c.read_max_rows_per_sec = None;
|
|
456
|
+
assert!(c.validate().is_ok());
|
|
457
|
+
c.read_max_rows_per_sec = Some(10_000);
|
|
458
|
+
assert!(c.validate().is_ok());
|
|
459
|
+
}
|
|
426
460
|
}
|
|
@@ -92,14 +92,22 @@ impl PgSource {
|
|
|
92
92
|
tracing::warn!("postgres connection error: {e}");
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
|
+
// Session setup, run as one round-trip. `application_name` makes this
|
|
96
|
+
// export visible (and killable) in `pg_stat_activity` — exactly what a
|
|
97
|
+
// DBA wants when watching load from a bulk read. When a statement
|
|
98
|
+
// timeout is configured we also cap `idle_in_transaction_session_timeout`
|
|
99
|
+
// at the same value: defense-in-depth so a stalled connection can never
|
|
100
|
+
// sit inside an open transaction holding an MVCC snapshot (which would
|
|
101
|
+
// block vacuum and bloat the source). The bulk COPY itself runs in
|
|
102
|
+
// autocommit, so statement_timeout is the primary guard; this covers the
|
|
103
|
+
// edge/future transactional paths.
|
|
104
|
+
let mut setup = String::from("SET application_name = 'quickhouse'");
|
|
95
105
|
if self.statement_timeout_secs > 0 {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self.statement_timeout_secs * 1000
|
|
100
|
-
))
|
|
101
|
-
.await?;
|
|
106
|
+
let ms = self.statement_timeout_secs * 1000;
|
|
107
|
+
setup.push_str(&format!("; SET statement_timeout = {ms}"));
|
|
108
|
+
setup.push_str(&format!("; SET idle_in_transaction_session_timeout = {ms}"));
|
|
102
109
|
}
|
|
110
|
+
client.batch_execute(&setup).await?;
|
|
103
111
|
Ok(client)
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -132,7 +140,7 @@ impl PgSource {
|
|
|
132
140
|
column: c.name().to_string(),
|
|
133
141
|
type_name: c.type_().name().to_string(),
|
|
134
142
|
})?;
|
|
135
|
-
let nullable = !not_null.contains(
|
|
143
|
+
let nullable = !not_null.contains(c.name());
|
|
136
144
|
cols.push(ColumnType {
|
|
137
145
|
name: c.name().to_string(),
|
|
138
146
|
type_id: pg_oid,
|