Closed (fixed)
Project:
Salsa Entity
Version:
7.x-1.x-dev
Component:
Code
Priority:
Normal
Category:
Feature request
Assigned:
Reporter:
Created:
20 Oct 2015 at 08:49 UTC
Updated:
22 Nov 2016 at 15:04 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
devlada commentedHere is export functionality, please review, working on import.
Comment #3
devlada commentedMore complete patch. Remaining todos are described in the code.
Comment #4
berdirthis should probably mention that the default and max is 500 (and also enforce that. salsa limits to that and your code might get confused)
what's the default? isn't this required? then it possibly should be an argument?
coding standard ;)
not sure about this default. Having one might make sense but it should possibly be a different, hardcoded list.
yeah, I don't think a default here makes sense. Those files should *not* be in the public folder as that would make accessible over the web if you can guess the names.
ah, fine, so just document it.
considering using a do/while loop.
I wasn't aware that we decided for csv, I thought we use json? this seems very fragile and it would be useless for manual extraction anyway with that custom format.
Why use array_filter()?
Have you tried something like donation pages with special characters, multiline and so on? is that working correctly?
Sorry but we don't use sorry in error messages :)
Consider using DirectoryIterator for this: http://stackoverflow.com/questions/4202175/php-script-to-loop-through-al....
You get http://php.net/manual/en/class.splfileinfo.php objects, with methods like isDot()/isReadable() and so on.
Yes, the order is important. Which is why we need a hardcoded list that is in the correct order or need to find a dynamic solution, which is not trivial.
A dynamic solution could look like this:
if you find a $type_KEY property, extract type. If $type has not yet been imported, switch to $type first (the loop needs to be a function then), track what you already mapped/imported somehow. But that's step two, first get it working with a manual order.
That's a good question.
As a start, you need to keep it in memory so that importing at the same time works. In a second step, we could write it into a while that we read again, but security wise, that's not trivial. Probably with an option --store-mappings=filename, e.g. as JSON.
The current logic of printing PHP code should become an option (--print-php-mappings). There are still use case for that.
$files = array_intersect($files, $types)
Comment #5
devlada commentedImproved some review points but still work in progress (mapping and import order logic missing). New patch is following soon.
Comment #6
devlada commentedBasic mapping logic is implemented, still working on import order / relations.
Comment #7
devlada commentedRecursive export and import (based on dependencies).
Comment #8
devlada commentedIn this patch I was working on Salsa mapping dependencies, tried to better understand Salsa tables and relations between them and the following conclusions / cases can draw:
1) In most cases, the tables have been connected using foreign keys that follows $type_KEY pattern.
2) It may also be in the format $type_IDS (for example campaign table contains person_legislator_IDS and exclude_person_legislator_IDS fields as foreign keys to person_legislator table).
3) Some fields introduces exclude_$type_IDS pattern but this is not the general rule.
3) In one case, lowercase is used for _key (if supporter_action_target and target table in a relation, then they are connected via target_key field, which is a derogation from the rule 1).
4) Foreign key can be multiple value field, in that case keys following $type_KEYS pattern.
5) In some cases, relation table may be missing, for example campaign table contains rep_KEYS field but rep table is missing. Maybe this is an abbreviation, there may be table under a different name, but anyway this is another special case.
6) Also exclude_rep_KEYS field in campaign table is not in compliance with point 3.
7) Some tables contains parent_KEY field as a reference to itself.
8) Several tables contain add_to_groups_KEYS and optionally_add_to_groups_KEYS fields as a reference to groups table.
9) action_target table contains excluded_KEYS field, it is not clear to which table this reference refers.
10) Also the following references deviate from the rule 1:
- membership_invoice_KEY
- referral_supporter_KEY
- source_donation_KEY
- current_reference_donation_KEY
- join_email_trigger_KEYS
- add_to_chapter_KEY
- old_custom_field_KEY
- table_KEY
- national_event_KEY
- donor_groups_KEYS
- use_new_groups_KEYS
If we look at these fields as $type_KEY / KEYS pattern, then most of types / tables will be missing, but we can infer that referral_supporter_KEY refers to the supporter table, source_donation_KEY to the donation table and so on… All these are practically separate cases.
11) Query table and some other contains several fields that follows $type_KEYS_Negate pattern, and come in pair with $type_KEYS fields, for example groups_KEYS ==> groups_KEYS_Negate, interest_KEYS ==> interest_KEYS_Negate, campaign_KEYS ==> campaign_KEYS_Negate, letter_KEYS ==> letter_KEYS_Negate and so on…
12) How to handle ($) fields?
- required$groups_KEYS
- event$email_trigger_KEYS
- waiting_list$email_trigger_KEYS
- upgrade_$email_trigger_KEYS
- reminder_$email_trigger_KEYS
- donor$email_trigger_KEYS
Look like all the special cases.
13) In merchant_account table we have two fields:
- failed_recurring_donation_email_trigger_KEYS
- successful_recurring_donation_email_trigger_KEYS
as reference to donation or email trigger table?
I have implemented recursive logic so that script exports and imports all dependent tables recursively before exports / imports the parent tables, this ensures that mapping file always contains child key pairs (source => target key), so that all references can be held with the new target keys.
Please do review, code can certainly be improved, but what worries me are dependencies (relations) and so many cases. Also, support for custom fields is missing, exception handling should be improved, in case something breaks, it is very likely that the mapping file will remain empty, because the file is saved on the end, and I need a confirmation that I'm going in the right direction?
Comment #9
devlada commented.
Comment #10
devlada commentedSome todos, should help in further planning, not arranged on priorities.
1) Documentation is missing, add a brief instruction how to use drush scripts in README.txt file.
2) salsa_entity_object_types() function should be replaced with salsa_mapping_dependencies(), on that way we should get to the valid list of Salsa tables, as salsa_entity_object_types() function is limited only to objects that are used by Drupal module.
3) Ensure creating subdirectories, file_prepare_directory() function looks like a good choice but in most cases because of linux filesystem permissions, to how is configured Apache, which account uses, it is necessary to manually create subdirectories. Check this out once more, find a better solution or make updates in README.txt file so that customers know how export / import directory should be configured.
4) ‘Remove previously exported files if any.’ - It may be possible to write nicer this code block.
5) Prevent any exception during export script is running (test exporting all tables, identify exception cases, why they happen, to better understand exception cases that happen on import).
6) Complete salsa_mapping_dependencies() function, individual tables contains foreign keys for which it is necessary to define the dependent table to which they relate. Also, due to different patters that foreign keys follows (as explained in https://www.drupal.org/node/2597059#comment-10515750) it is necessary to implement complex logic for maintaining references between tables after importing (‘Update references (foreign keys)’ block code in drush_salsa_entity_import() is simply not enough).
7) Custom fields per table support.
8) Prevent exceptions on import.
9) Provide frequent recording of mapping.json file, currently when exception occurs, mapping.json file remains empty because the recording is done at the end, which is very bad, probably the only proper and safe solution is to record after each row is imported, or to guarantee that no one exception can break script.
Comment #11
miro_dietiker8. About exceptions that make everything break: Should be pretty simple as long as we are talking about regular exceptions. Just a global try / catch. The only problem is if you create fatal errors. These need to be avoided. Persisting after every record is too much overhead. You might want to dump the mapping every 50 / N records?
4. Unsure about all this unlinking... I would expect that we can just overwrite and deleting could be up to the user (or as option only?)...
Otherwise if an export is just a one-off thing, we should possibly always create a new folder per run in the export folder? Dunno..
Comment #12
devlada commented1), 2), 3), 4), 5) done from comment #10.
4) No need for any unlinking now, this was necessary in previous versions of the script, currently overwriting works quite well.
Comment #13
devlada commentedPrevented an infinite loop on export.
Comment #14
berdirHm, $GLOBALS isn't really nice code..
what about about a helper function like salsa_entity_export_iterator($directory = NULL). you initialize it just like here by calling with the argument, and then call it without argument to get the directory iterator where you need it?
Ah, I see, you call yourself again here.
It might be a bit easier to follow if have the actual processing logic into a separate function that you call recursively. And just keep the option parsing and initializing into the current function.
Also, shouldn't you track $called globally? Assume the following dependency tree:
A
- B
- C
- D
- C
So C is twice in there. But I think you'd end up importing it twice?
If you have less then e.g. 500 records, you now always do two requests. First you fetch e.g. 100, then you try again for more.
But you know that if you have less than $paging then you're done. So instead of while (TRUE), write (count($entities) == $paging. Then it will continue until there are less.
You still need this check here, but only for the case where you have exactly 500 records or an error. An error should handled separatey and displayed IMHO.
You should be able to save a variable if you instead use $sequence * $paging as $offset?
If missing, display a warning and move on.
Aren't we missing the logic here to avoid importing the same dependencies multiple times?
Hm, so we hardcode this now? I was hoping it wouldn't be necessary but now that the work is done, fine ;)
Unrelated change, possibly due to auto-format?
Comment #15
berdirAh didn't see that $called was defined as static, ignore that then.
Comment #16
berdirNotes from testing:
* You can set 'required-arguments' => TRUE, apparently, then it should not display some strange notices if you forget the arguments.
* Getting notices like "Only variables should be passed by reference salsa_entity.drush.inc:73" when running mit -v, you need to store that in a variable first.
* Also getting "Undefined index: key salsa_api.inc:324 ", that might be a bug in salsa_api itself, maybe when there are no records or so.
* I think the all list is too much. There are many tables in there that we don't need. The following can certainly be removed as we don't need them for note. Also note that many of those won't be defined as entity types, then your entity_create() would fail on them:
+ distributed_event (not used by us atm)
+ database_table (that is internal metadata, not something you can import)
+ package (same)
+ custom_column and custom_column_option are special. That's about defining new custom fields. I don't know if that just works. We should try with a single one (creating them has lots of overhead, they also have unique names, breaking that might break salsa, we managed that before)
+ publish
+ tag/tag_data is special, I'm not sure if we can just save that, needs to be tested. Maybe we need to import tags differently (usually done as a special element on saving an entity: $form_state['supporter']->additional['tag'] = implode(',', $form_state['values']['tags']);. We might want to load them first and then add them to saved objects like that. We however need to database_table names to do that, so we might actually need that table, but not for importing again, just for lookup up the ID's in that table
+ merchant_account ( we don't need them and I don't think they can just be set up like this)
+ feed_archetype
+ supporter_picture and album, currently not used by us, we'd also need the files for that
+ version (internal)
+ chapter* ( I don't think we can just export/import them, not sure
So comment out all those things.
Also import should skip anything that does not exist as an entity type and display a warning, even if we define it.
* Another notice: Undefined property: stdClass::$result salsa_entity.drush.inc:112, you need to check if you actually have a result property there.
* Some more information would be great during the import. When you start a type, you could do a getCount() query and then display an info message like "Starting to export supporter (100'000 records)" (with getCounts()). If you get a count of 0 then you can also skip that type.
* For displaying messages that you want to be seen by the user, use drush_log($message, 'ok') use the default type (notice) for debug info that shouldn't be visible by default.
Comment #17
devlada commentedjust submitting new patch, more about improved points (changelog) during the day.
Comment #18
devlada commentedExport is now much stable.
Comment #19
devlada commentedComment #20
devlada commentedImprovements required in previous comments.
Comment #21
berdirYou seem to make it more complicated for yourself than it has to be. Why not use $custom_fields and document it as such?
If we have things like this commented out, we should have an explanation for it. In this case, say that they are added explicitly at the beginning.
without explanation, it might be better to remove them. Or keep them all together and have a single comment, explaining that those objects are not supported.
I find "Helper function: " and similar prefixes to be extremely useless. It's a waste of characters and width, even making your description go over 80 characters because of that. Just use the sentence that you have, that's perfect.
only use object when the type is unknown. Otherwise just use the type directly: @return \DirectoryIterator.
This code is backwards, you always create a new iterator?
You set it to FALSE but then use !is_null(). neither false nor an iterator object is null, so it will *always* go in there. Just use !$iterator.
I'm also not sure about catching the exception here. As you've seen yourself, error handling then gets more complicated. You basically have to catch and ignore but then check again. That's what exception handling is for, that you can bubble up an unexpected error and deal with it in the caller.
Some feedback from testing out the export:
* I got some exceptions on certain tables:
If those tables definitely don't work, we should not have them in our list. And if there's an exception I would expect to not see the succesfully created file message?
And last, looking at the tables we're exporting now, I noticed you export tag but not tag_data. Just the tags table alone is useless, it just contains a list of all existing tags. The tags_data table is the one that connects it to a certain thing. The problem with that is that it does go through the database table ID.
If you look at https://hq-kampa.salsalabs.com/api/getObjects.sjs?object=tag_data&limit=10, you can see that it has database_table_KEY and table_key. The first is the row in the database table. So if you have 45 there, you need to look it up in database_table, which tells you that's donation. So it's a tag for donation with KEY 12788348.
For saving, it's a lot easier. You can just put a comma separated list of tag *strings* (not ID's) on the $object->tags property. And salsa will take care of the rest.
Given that, I would recommend to deal with this complexity on export. We have a method for that on SalsaEntity, see getTags. You no longer use that, which is OK. If we do this, we need to improve that logic so we can fetch it for many objects at a time anyway.
That's the theory. Let me check how important that actually is.
Comment #22
berdirLets open a follow-up for those tags.
Comment #23
devlada commentedComment #24
devlada commentedStill needs work but improved some points from comment #21.
Comment #25
devlada commentedAdditional improvements in relation to the reviews given in the comment #21.
Dependencies array is slightly improved, cleaned the table that I think we do not need, if it proves to be, I'll add each separately. For now, we start from the tables which we have defined as salsa types. Also added a lot of comments, primarily as a reminder, will remove once I solve certain cases.
Some of the follow-up issues (my plan / priorities)
- Tags handling.
- Groups handling.
- Other dependencies / relations, handling different keys.
- On import, Email validation, preventing exceptions.
Comment #26
devlada commented- Improved dependencies.
- Implemented groups handling.
Comment #27
devlada commentedImplemented various cases (keys handling).
Comment #28
devlada commentedGetting simplexml_load_string() parser error on event->save()
simplexml_load_string(): Entity: line 1: parser error : Extra content at the end of the document salsa_api.inc:406 [warning]
simplexml_load_string():
No captcha on this form, continuing.
salsa_api.inc:406 [warning]
simplexml_load_string(): ^ salsa_api.inc:406 [warning]
Any idea how to overcome captcha issue? What this means, that captcha should be disabled in salsa backend before import or?
Comment #29
devlada commentedComment #30
devlada commentedIssue described in comment #28 is salsa_api issue. Here is patch https://www.drupal.org/node/2675508
Comment #31
devlada commentedComment #32
devlada commentedSupporter Email validation.
Comment #33
berdirAdded email things, fixed a few bugs, sorting the files for import and skip already exported files, for faster restart when it fails on export.
Comment #36
berdirCommitted, we'll do necessary improvements in follow-ups.