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.
Files changed (34) hide show
  1. {quickhouse-0.3.2 → quickhouse-0.3.4}/Cargo.lock +4 -4
  2. {quickhouse-0.3.2 → quickhouse-0.3.4}/Cargo.toml +1 -1
  3. {quickhouse-0.3.2 → quickhouse-0.3.4}/PKG-INFO +25 -1
  4. {quickhouse-0.3.2 → quickhouse-0.3.4}/README.md +24 -0
  5. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/config.rs +34 -0
  6. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/ddl.rs +1 -0
  7. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/bigquery.rs +1 -0
  8. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/postgres.rs +15 -7
  9. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sync.rs +422 -217
  10. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/transform.rs +137 -1
  11. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/types.rs +43 -2
  12. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-py/src/lib.rs +3 -0
  13. {quickhouse-0.3.2 → quickhouse-0.3.4}/pyproject.toml +1 -1
  14. {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/_quickhouse.pyi +33 -0
  15. {quickhouse-0.3.2 → quickhouse-0.3.4}/LICENSE +0 -0
  16. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/Cargo.toml +0 -0
  17. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/archive.rs +0 -0
  18. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decimal.rs +0 -0
  19. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decode.rs +0 -0
  20. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decode_bigquery.rs +0 -0
  21. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/decode_mysql.rs +0 -0
  22. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/error.rs +0 -0
  23. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/lib.rs +0 -0
  24. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/memory.rs +0 -0
  25. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/bigquery_proto.rs +0 -0
  26. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/clickhouse.rs +0 -0
  27. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/sink/mod.rs +0 -0
  28. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/bigquery.rs +0 -0
  29. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/mod.rs +0 -0
  30. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-core/src/source/mysql.rs +0 -0
  31. {quickhouse-0.3.2 → quickhouse-0.3.4}/crates/quickhouse-py/Cargo.toml +0 -0
  32. {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/__init__.py +0 -0
  33. {quickhouse-0.3.2 → quickhouse-0.3.4}/python/quickhouse/progress.py +0 -0
  34. {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.2"
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.2"
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.0"
3041
+ version = "1.15.1"
3042
3042
  source = "registry+https://github.com/rust-lang/crates.io-index"
3043
- checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
3043
+ checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
3044
3044
  dependencies = [
3045
3045
  "web-time",
3046
3046
  "zeroize",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/quickhouse-core", "crates/quickhouse-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.2"
6
+ version = "0.3.4"
7
7
  edition = "2021"
8
8
  license = "MIT"
9
9
  authors = ["quickhouse contributors"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickhouse
3
- Version: 0.3.2
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
  }
@@ -147,6 +147,7 @@ mod tests {
147
147
  batch_bytes: 0,
148
148
  max_memory_bytes: 0,
149
149
  partition_column: None,
150
+ read_max_rows_per_sec: None,
150
151
  type_overrides: HashMap::new(),
151
152
  rename: HashMap::new(),
152
153
  include: vec![],
@@ -945,6 +945,7 @@ mod tests {
945
945
  batch_bytes: 0,
946
946
  max_memory_bytes: 0,
947
947
  partition_column: None,
948
+ read_max_rows_per_sec: None,
948
949
  type_overrides: HashMap::new(),
949
950
  rename: HashMap::new(),
950
951
  include: vec![],
@@ -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
- client
97
- .batch_execute(&format!(
98
- "SET statement_timeout = {}",
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(&c.name().to_string());
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,