feat(project): add table row limiter to project dump - #1357
feat(project): add table row limiter to project dump#1357Soner (shyim) wants to merge 3 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
|
||
| // keptRowsQuery returns a SELECT yielding the given columns of all rows kept | ||
| // for the given affected table. | ||
| func (r *limitResolver) keptRowsQuery(table string, columns []string) string { |
There was a problem hiding this comment.
I see a little risk here: what if somebody calls --limit order=100 --limit customer=50? It seems that we'd take the 100 latest orders and the 50 latest customers as two independent queries, with nothing keeping them together - so there's no reason the customers referenced by those 100 orders would be among the 50 kept customers. This would result in orders without customer data. Even with --limit order=100 --limit customer=100, the same problem applies, since we're picking the newest records independently in each case rather than filtering one by the other.
It feels like we could add a condition here that limits the related table's results (as it already does), but also filters them first by the FK condition against it. Wdyt?
There was a problem hiding this comment.
I would like to keep this simple as possible. wdym about:
when user limits: order + order_address, we throw an error that limiting order does influence order_address therefore limiting is not allowed?
There was a problem hiding this comment.
Yeah that would make sense and we would be fair transparent to the user!
There was a problem hiding this comment.
Tomasz Turkowski (@tturkowski) updated can you take a look second time?
There was a problem hiding this comment.
Also think keeping it as simple as possible is the way to go here. Applying the limits "top down" and rejecting further limits on references sounds fine.
Just as already hinted in my other comment, it also comes with a caveat. Your PR description mentions:
New CLI flag (repeatable):
shopware-cli project dump --limit order=100 --limit product=500Details:
- Tables that are only referenced (like product from order_line_item) are not filtered.
What does this mean in practice? Can you have that example and supply both a order and product limit? I could imagine you will then fetch more products than the specified limit if the orders also referenced other products? And then the edge case of product variants / self references (topic of my other thread)
Adds a --limit flag (e.g. --limit order=100) and a dump.limit project config section to restrict how many rows of a table are dumped. Tables referencing the limited table via foreign keys, directly or transitively, are filtered automatically so the dump only contains rows belonging to the kept rows. Rows are ordered by created_at DESC by default; the order is configurable via order_by. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The row-limit conditions contain subqueries on other tables (or the table itself via the derived limit table). MySQL rejects those with ER_TABLE_NOT_LOCKED (error 1100) while the table is locked through FLUSH TABLES ... WITH READ LOCK, so locking is now skipped for tables carrying a limit filter and an info log explains why. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
18e0ef1 to
81b4a89
Compare
Malte Janz (MalteJanz)
left a comment
There was a problem hiding this comment.
Only had a rough first look and didn't had time to review everything in detail yet, but feel free to continue without my full review (don't want to be the blocker while I have limited time) 🙂
| if projectCfg.ConfigDump.Limit == nil { | ||
| projectCfg.ConfigDump.Limit = make(map[string]shop.ConfigDumpLimit) | ||
| } |
There was a problem hiding this comment.
nit: if can be moved outside / before the for loop as it doesn't use any iteration specific values
| dumper.LimitMap = make(map[string]mysqldump.TableLimit, len(projectCfg.ConfigDump.Limit)) | ||
| for table, limit := range projectCfg.ConfigDump.Limit { | ||
| dumper.LimitMap[table] = mysqldump.TableLimit{Rows: limit.Rows, OrderBy: limit.OrderBy} | ||
| } |
There was a problem hiding this comment.
Just curious: why is the type defined in two places and data copied over here?
shopware-cli/internal/shop/config.go
Lines 255 to 261 in 81b4a89
shopware-cli/internal/mysqldump/limit.go
Lines 13 to 24 in 81b4a89
| limits := make(map[string]TableLimit, len(d.LimitMap)) | ||
| for table, limit := range d.LimitMap { |
There was a problem hiding this comment.
see above, copied again here but this time with the purpose of filtering. Why not deleting entries instead? Is keeping the original data important?
| userWhere := d.WhereMap[table] | ||
| limitWhere := d.limitWhere[table] | ||
|
|
||
| switch { | ||
| case userWhere != "" && limitWhere != "": | ||
| return fmt.Sprintf("(%s) AND %s", userWhere, limitWhere) | ||
| case userWhere != "": | ||
| return userWhere | ||
| default: | ||
| return limitWhere | ||
| } |
There was a problem hiding this comment.
At this point why not make the where conditions a slice (behind the map to differentiate between DB tables) and then combine them instead of introducing another variable / field. That way its more flexible for further extension and you don't need such a messy switch case + maintain two maps 😅
| query += " WHERE " + where | ||
| } | ||
| if limit.OrderBy != "" { | ||
| query += " ORDER BY " + limit.OrderBy |
There was a problem hiding this comment.
Likely an edge case, but I'm curious how does this implementation deal with:
- you only want to dump 100 orders with their respective dependencies / references including the products
- products can have a self reference with their
parent_id+parent_version_idfor linking to the main / container product from product variants. The good thing it's only one level deep so if you would just import products alone you could order by these fields and import theparent_id = nullfirst, making sure the foreign key data is there as well - is a similar thing applied while there is this limiting logic for orders? e.g. what happens if the order line items only reference some variants, is the parent / container / main product then included as well? Is the
parent_idsilently dropped tonull(making the data incorrect)?
*Didn't had time myself yet to try out the current dump command or this PR ✌️
|
|
||
| // keptRowsQuery returns a SELECT yielding the given columns of all rows kept | ||
| // for the given affected table. | ||
| func (r *limitResolver) keptRowsQuery(table string, columns []string) string { |
There was a problem hiding this comment.
Also think keeping it as simple as possible is the way to go here. Applying the limits "top down" and rejecting further limits on references sounds fine.
Just as already hinted in my other comment, it also comes with a caveat. Your PR description mentions:
New CLI flag (repeatable):
shopware-cli project dump --limit order=100 --limit product=500Details:
- Tables that are only referenced (like product from order_line_item) are not filtered.
What does this mean in practice? Can you have that example and supply both a order and product limit? I could imagine you will then fetch more products than the specified limit if the orders also referenced other products? And then the edge case of product variants / self references (topic of my other thread)
What changed?
Adds a table row limiter to
project dump, so large tables can be reduced to a fixed number of rows while keeping the dump referentially consistent.New CLI flag (repeatable):
New
.shopware-project.ymlsection:How it works: the dumper walks the foreign-key graph of the cached table schemas and automatically filters every table that references a limited table — directly or transitively — with
WHERE ... IN (SELECT ...)conditions, so e.g. limitingorderalso limitsorder_line_item,order_delivery, andorder_delivery_positionto rows belonging to the kept orders.Details:
IN(Shopware's(id, version_id)pairs).order_id IS NULL OR order_id IN (...)).productfromorder_line_item) are not filtered.where:config entries are combined with the limit conditions ((user where) AND (limit condition)).rows < 1or a table without a primary key is an error.Why?
Dumping a production database for local development is painful when a handful of tables (orders, logs) hold millions of rows. A plain
LIMITper table would break foreign keys on import; this feature keeps only rows that belong together, producing small dumps that still import cleanly withFOREIGN_KEY_CHECKSenabled.How was this tested?
internal/mysqldump/limit_test.gocovering: FK cascade over multiple levels, nullable FK handling, customorder_by,wherecomposition, FK cycles, unknown tables, and validation errors.go test ./...andgolangci-lint runpass.Related issue or discussion
—
🤖 Generated with Claude Code