Hi Team, I am running ETL workload using daft dist...
# daft-dev
a
Hi Team, I am running ETL workload using daft distributed mode on ray cluster. The ETL jobs picks up modified records in last 24 hours from mysql source, applies minor transforms like type casting, adding partition column (date_part), and finally does delta merge on primary keys. Since delta merge is not natively supported in daft, I am using the data sink workaround (https://github.com/Eventual-Inc/Daft/issues/4047#issuecomment-3094861350). I am seeing ~5% data loss in columns only with datatype DECIMAL(38, 20). I typecast it to
daft.DataType.decimal128(38, 20)
after reading from mysql. If distributed delta merge using deltalake fails on a worker due to delta contention, I am doing retries (I am not using dynamodb as lock provider). Is there a known issue with datatype decimal(38, 20) and how to avoid it? Please let me know if additional details are required.
r
What are you seeing specifically for data loss e.g. missing rows or NULLs for some DECIMAL columns?
To clarify - are you seeing the loss on write or on read?
a
I am seeing NULL values in sink. I haven't isolated the problem yet (read, transform, delta merge - at which stage). I will get back with these details. My pip package list:
PIP_PACKAGES=(
"ray[default]==2.54.0"
"daft==0.7.4"
"deltalake==1.4.2"
"pyarrow==23.0.1"
"pymysql"
"boto3"
"sqlglot"
"sqlalchemy"
)
AWS compute instances - 26 nodes of mostly m6g.xlarge if that help. Mysql source is InnoDB. Size of table in S3 - 183 GB
j
My guess is that some parts of this casting might be resulting in nulls @Robert Howell
What's the datatype coming from mysql @Abhijeet Patil ?
a
This is how its defined in mysql source (sharing a sample column): ``customercgstamount_lcy` decimal(38,20) DEFAULT '0.00000000000000000000',`
r
Hey @Abhijeet Patil, I can get back to you tomorrow morning. Thanks for the additional info!
❤️ 2
Alright, current theory is decimal overflow + arrow safe casting causing a NULL fallback. The workaround would be to cast to string first then decimal128.
I used Claude Code to research possible causes by auditing Daft, but need your help to see if these track to how your pipeline is setup.
Trigger A: Existing Delta Table Has a Different Scale (Highest Likelihood)
Your delta merge reads the existing Delta table as DECIMAL(38, S) — written previously by Spark (which frequently infers DECIMAL(38, 18) from data), by a prior Daft version, or via a different connector. The new MySQL data arrives as DECIMAL(38, 20).
Daft's supertype computation (supertype.rs:204):
Some(DataType::Decimal128(max(p1, p2), max(s1, s2)))
// DECIMAL(38, 18) + DECIMAL(38, 20) → DECIMAL(38, 20)
This forces old rows to be upscaled from S=18 to S=20. Any row with |value| > i128::MAX / 10² (≈ 1.7×10^36 internal, ≈ 1.7×10^18 actual) will overflow → NULL. Since DECIMAL(38, 18) allows values up to 10^20, roughly 98% of the range (10^18 to 10^20) is the danger zone.
Trigger B: PyArrow Scale Inference at MySQL Read Time
Python MySQL connectors (mysql-connector-python, PyMySQL) return decimal.Decimal objects with trailing zeros stripped. For example, a MySQL DECIMAL(38, 20) value 1000000000000000000.00000000000000000000 comes back as Decimal('1000000000000000000') (scale=0).
PyArrow infers the type from the actual values per partition. If a Ray partition happens to contain only integer-valued decimals (scale=0 after stripping), PyArrow creates DECIMAL(19, 0) for that partition. When Daft reconciles with other partitions (via supertype) or you call .cast(DataType.decimal128(38, 20)), the upscaling is fallible.
Overflow threshold for scale=0 → scale=20: |value| > 10^18 (which equals the precision cap of DECIMAL(38, 20) itself, meaning ALL values near the maximum are at risk).
a
Thanks @Robert Howell. I will check this and get back. Yes, the existing delta tables on which I am trying to merge is written by spark on aws emr.
I think the problem is in read part. I am using sqlalchemy and that might infering wrong data types (it tries to guess based on few rows in the batch I think). I tried switching to connectorx in my local dev env, seems working. I will rollout in production and update this conversation with my findings. Screenshots of my cursor with claude 4.6 opus high:
🙌 1
❤️ 1
j
Wow very impressive work debugging this — do you point cursor at the daft source code?
❤️ 1
r
@Abhijeet Patil an explicit schema is often nice to avoid schema inference issues.
💯 2
a
@jay Yes. My cursor IDE has daft repo at label v0.7.5. I find these skills useful : /daft-docs-navigation /daft-distributed-scaling
j
@YK FYI 😛 we have a pretty interesting case of skills debugging powerusage
👀 1
🔥 1
a
I was able to get rid of this using connectorx. connectorx infers schema from mysql table metadata. Also connectorx seems faster than sqlalchemy due to zero data copy. But with connectorx I ended up with another problem if tinyint(1) (https://github.com/sfu-db/connector-x/blob/d6985c53719a4d8334565bd693d12b8372306bf9/connectorx/src/transports/mysql_arrow.rs#L44) . Spark connector to mysql provides us the option "tinyint1isbit" and depending on that we can choose if we want tinyint(1) as boolean or integer. Problem with connectorx is it defaults to boolean. As a workaround, I added "CAST(col AS SIGNED)" in my select query. With all these fixes, I am able to successfully migrate our data capture ETL workload from spark to daft. 🙂