Package: pysqlsync
pysqlsync: Synchronize schema and large volumes of data over SQLpysqlsynchelps you synchronize your target database or data warehouse with a data source, making efficient use of Python language elements and database drivers (client libraries). The library package employs Python data-classes (decorated with@dataclass) to define database tables and generateCREATE,DROP,INSERT,MERGEandDELETESQL statements. Commands against the database driver are executed with the asynchronous paradigm (asyncandawait). This can result in an order of magnitude speed increase over traditional methods such as SQLAlchemy when data is inserted or upserted (merged) into, or deleted from a table.Use casesFormation. Create an initializer SQL script (e.g. ANSI, PostgreSQL or MySQL) from a set of Python@dataclassdefinitions.Discovery. Create a traversable Python object hierarchy (e.g.Namespace,Table,ColumnandConstraintobjects) from a database catalog (usinginformation_schemaorpg_catalog).Schema synchronization. Emit SQL statements to mutate the database schema from a source state to a desired target state usingCREATE,DROPandALTER.Data import. Efficiently insert data from a list of objects or tuples of simple types into a database table usingINSERT,MERGEandDELETEwith multiple arguments, and lists of tuples or collections of objects as input.Quick startFirst, define the table structure with a standard Python data-class (including dependent data types):classWorkflowState(enum.Enum):active="active"inactive="inactive"deleted="deleted"@dataclasses.dataclassclassUserTable:id:PrimaryKey[int]updated_at:datetimeworkflow_state:WorkflowStateuuid:uuid.UUIDname:strshort_name:Annotated[str,MaxLength(255)]homepage_url:Optional[str]=NoneThe data-class can be defined statically in code, or generated dynamically from input (withdataclasses.make_dataclass). Fields can be required or nullable (represented in Python asNone). All basic data types are supported, including integers (of various widths), floating-point numbers, strings (of fixed or variable length), timestamps (datetime.datetimein Python), UUIDs (uuid.UUIDin Python), enumerations (represented in Python asenum.Enum), etc.list[...]is supported as a collection type, and composite types (data-classes without a primary key) are also permitted.Next, instantiate a database engine, open a connection, create the database structure (with aCREATE TABLEstatement), and populate the database with initial data (with SQLINSERTorCOPY):engine=get_dialect("postgresql")parameters=ConnectionParameters(host="localhost",port=5432,username="levente.hunyadi",password=None,database="levente.hunyadi",)options=GeneratorOptions(enum_mode=EnumMode.RELATION,namespaces={tables:"example"})data=[UserTable(id=1,updated_at=datetime.now(),workflow_state=WorkflowState.active,uuid=uuid.uuid4(),name="Laura Twenty-Four",short_name="Laura",)]asyncwithengine.create_connection(parameters,options)asconn:awaitconn.create_objects([UserTable])awaitconn.insert_data(UserTable,data)Let's assume the database structure changes. With the help of anExplorerinstance, discover the objects in the database, and create/drop objects to match the state as captured in the specified Python module:asyncwithengine.create_connection(parameters,options)asconn:awaitengine.create_explorer(conn).synchronize(module=tables)Finally, keep the target database content synchronized with data from the source (with the equivalent of SQLMERGE):data=[UserTable(id=2,updated_at=datetime.now(),workflow_state=WorkflowState.active,uuid=uuid.uuid4(),name="Zeta Twelve",short_name="Zeta",)]asyncwithengine.create_connection(parameters,options)asconn:awaitengine.create_explorer(conn).synchronize(module=tables)awaitconn.upsert_data(UserTable,data)In order to boost efficiency, you can insert (or update) data directly from a list of tuples:field_names=["id","uuid","name","short_name","workflow_state","updated_at"]field_types=[int,uuid.UUID,str,str,str,datetime]rows=[(1,uuid.uuid4(),"Laura Twenty-Four","Laura","active",datetime.now()),(2,uuid.uuid4(),"Zeta Twelve","Zeta","inactive",datetime.now()),]asyncwithengine.create_connection(parameters,options)asconn:awaitengine.create_explorer(conn).synchronize(module=tables)table=conn.get_table(UserTable)awaitconn.upsert_rows(table,field_names=tuple(field_names),field_types=tuple(field_types),records=rows,)Structure and data synchronizationpysqlsyncsupports two modes of operation: structure synchronization and data synchronization.When performingstructure synchronization,pysqlsyncmorphs a database source state into a desired target state. The source state is typically obtained with reflection (e.g. extracting metadata frominformation_schemaorpg_catalog). The target state is defined as a set of Python data-classes either statically or dynamically (e.g. based on aJSON Schema). Comparing source and target state,pysqlsynccreates a transformation script (predominantly ANSI SQL with vendor-specific extensions such as comments), and runs the script against an asynchronous client such asasyncpgoraiomysql. This script creates new schemas, structure and enumeration types, tables, columns, constraints, etc. whenever the target state contains items that the source state lacks, and drops database objects when the opposite is true. When there are matching objects (based on qualified name), the object is mutated (e.g. the data type of a column is changed).Once database structure has been morphed into the desired state,data synchronizationhelps keep a local database state in sync with a remote database state.pysqlsyncimplements insert and upsert functions to handle lists of tuples of data. Each tuple corresponds to a table row, and each tuple member is a column entry. Data in each column may have to be transformed to be suitable for the database dialect, e.g. MySQL would have to transform aUUIDinto aBINARY(16), represented in Python asbytesbecause it has nouuidtype. These transformation functions are derived in advance such that there is minimum CPU load to process a record (one of possibly billions). Loading data from network/disk to memory may use an efficient parser implementation such astsv2py, which significantly speeds up parse time for composite types such asdatetimeorUUIDwith the help of SIMD CPU instructions.FormationFormation is the process of generating a series of SQL statements from a collection of Python data-class definitions. Several formation modes are supported.EnumerationsEnumModedetermines how Python enumeration types (subclasses ofenum.Enum) are converted into database object types. Possible options for the target of an enumeration type are:a SQLENUMtype created withCREATE TYPE ... AS ENUM ( ... )(PostgreSQL), oran inline SQLENUMdefinition, e.g.ENUM('a', 'b', 'c')(MySQL and Oracle), ora SQL data type corresponding to the enumeration value type, and aCHECKconstraint on the column to block invalid values, ora foreign/primary key relation (reference constraint), coupled with a lookup table, in which the lookup table consists of an identity column acting as the primary key and a unique column storing the enumeration values.Data-classesStructModedetermines how to convert composite types that are not mapped into SQL tables. A data-class type may be converted intoa composite SQL type withCREATE TYPE ... AS ( ... )(PostgreSQL), orthe SQLjsontype (PostgreSQL), ora text type, e.g.varchar, which holds the data as serialized JSON.ListsArrayModedetermines how to treat sequence types, such as Python lists, i.e. whether to represent them asa SQLarraytype (PostgreSQL), ora JSON array stored in a column with the SQL data typejson(PostgreSQL), ora serialized JSON string stored in a text column type, e.g.varchar.ModulesGeneratorOptionsallows you to pass a mapping between module types and SQL schema identifiers. Objects in each Python module would be transformed into SQL objects in the corresponding SQL schema (namespace).Database standard and engine supportANSI (when callingstr()on Python objects such asTable,ColumnorConstraint)PostgreSQL (viaasyncpg)Microsoft SQL Server (viapyodbc)MySQL (viaaiomysql)Python classes for database objectspysqlsyncfeatures several Python classes that correspond to database objects:Catalogcaptures a database state of possibly several namespaces.Namespacecorresponds to a database schema in engines that feature schemas (e.g. PostgreSQL).Tablerepresents a database table with a primary key, several columns and constraints.Columnobjects declare data type and nullability, and a possibly hold a default value.StructTypeandStructMemberrepresent composite types in engines that support them (e.g. PostgreSQL).EnumTypecaptures enumeration types for engines that represent them as their own type (e.g. PostgreSQL).The collection of database objects represents a current structure, which may morph into a target structure. Some of the objects may be vendor-specific, e.g. MySQL has its ownMySQLColumntype and PostgreSQL has its ownPostgreSQLTabletype.All objects are identified either with a local ID (e.g. namespaces and columns) or a qualified ID (e.g. tables). When the database engine lacks namespace support, qualified IDs map to a prefix, e.g.canvas.accounts(tableaccountsin namespacecanvas) becomescanvas__accounts(a table with no namespace).Dialectspysqlsync comes with several database dialects shipped with the library. However, it is possible to create and register new dialects that behave the same way as built-in dialects. In terms of capabilities, there are no differences between built-in and user-defined dialects.If you are about to write integration for a new database dialect, it is recommended that you take one of the existing dialects (e.g. PostgreSQL, Microsoft SQL Server or MySQL), and use it as a template. For more information, explore the folderpysqlsync/dialect.