Hi Adam,
thank you for your reply.

We usually have few indices, functions and triggers. But the most used 
database object is view. We used them for data synchronizing from some 
third party databases. These databases have complex schema which we want 
simplify. So we are building low-level DB API via views. Then we create 
Django models for these views. Then is easy to use ORM for data access or 
sync operations. 

I can show you some example from our code for one third party database:

sql_items = [
    datetime_function_sql_item,
    is_seo_valid_function_sql_item,
    quality_view_sql_item,
    article_category_view_sql_item,
    base_article_view_sql_item,
    article_view_sql_item,
    article_b2b_view_sql_item,
    master_article_view_sql_item,
    reclamation_mat_view_sql_item,
    purchase_mat_view_sql_item,
    purchase_id_index_sql_item,
    purchase_date_of_purchase_index_sql_item,
    sale_mat_view_sql_item,
    sale_id_index_sql_item,
    sale_date_of_sale_index_sql_item,
    incentive_sale_mat_view_sql_item,
    incentive_item_mat_view_sql_item,
    product_view_sql_item,
    color_master_view_sql_item,
    product_special_offers_view_sql_item,
    series_view_sql_item,
    stock_products_view_sql_item,
    pl_master_product_view_sql_item,
    pl_master_product_quality_view_sql_item,
    pl_product_view_sql_item,
    pl_product_variant_view_sql_item,
    pl_stock_products_sold_view_sql_item,
]

#
#  module containing `datetime_function_sql_item` and 
`is_seo_valid_function_sql_item` follows

#

from migrate_sql.config import SQLItem

"""
This third party app stores datetime as DATE column and INT column with seconds 
passed that day.
This function converts these columns into PostgreSQL datetime type.
"""
datetime_function_sql_item = SQLItem(
    name='datetime',
    sql='''
        CREATE OR REPLACE FUNCTION datetime(day date, seconds numeric) returns 
timestamp without time zone
        LANGUAGE plpgsql AS $$
          BEGIN
            RETURN day + (floor(seconds / 3600) || ':' || floor(seconds % 3600 
/ 60) || ':' || seconds % 60)::TIME;
          END;
        $$;
    ''',
    reverse_sql='DROP FUNCTION IF EXISTS datetime(day date, seconds numeric)',
    replace=True,
)


is_seo_valid_function_sql_item = SQLItem(
    name='is_seo_valid',
    sql='''
        CREATE OR REPLACE FUNCTION is_seo_valid(seo text) returns boolean
        LANGUAGE plpgsql AS $$
          BEGIN
            RETURN seo ~ '^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$';
          END;
        $$;
    ''',
    reverse_sql='DROP FUNCTION IF EXISTS is_seo_valid(seo text)',
    replace=True,
)


#
#  module containing `article_view_sql_item` follows

#

article_view_sql_item = SQLItem(
    name=Article._meta.db_table,
    sql='''
        CREATE OR REPLACE VIEW {view_name} AS
        SELECT
          sq.*,
          CASE
            WHEN sq.buying_price <> 0 THEN round((sq.price / sq.buying_price - 
1) * 100, 2)
            ELSE 0
          END                                                                   
          AS margin,
          GREATEST(
            LEAST(
              NULLIF(sq.price_for_installment_calculation, 0),  -- zero turns 
off this price
              sq.eshop_price
            ),
            0
          )                                                                     
          AS installment_price
        FROM (
          SELECT
            DISTINCT ON (a.id)
            a.id                      AS id,
            a.name                    AS "name",
            a.code                    AS code,
            COALESCE(a.master, FALSE) AS master,
            a.created                 AS created,
            a.fk_id_article_producer  AS fk_id_article_producer,
            a.fk_id_master_product    AS fk_id_master_product,
            a.fk_id_color_rgb         AS fk_id_color_rgb,
            a.fk_id_vat               AS fk_id_vat,
            a.cf_article_name         AS cf_article_name,
            a.cf_article_short_name   AS cf_article_short_name,
            a.cf_seo_name             AS cf_seo_name,
            a.cf_article_type         AS cf_article_type,
            q.cf_article_quality      AS cf_article_quality,
            COALESCE(a.active2, FALSE)                                          
          AS active2,
            COALESCE(a.cf_www_visible, FALSE)                                   
          AS cf_www_visible,
            COALESCE(a.cf_bo_for_sell, FALSE)                                   
          AS cf_bo_for_sell,
            COALESCE(a.cf_buy_up_recommended, FALSE)                            
          AS cf_buy_up_recommended,
            COALESCE(a.cf_clearance_sale, FALSE)                                
          AS cf_clearance_sale,
            COALESCE(a.cf_article_short_name, a.name)                           
          AS short_name,
            COALESCE(a.cf_article_name, a.name)                                 
          AS long_name,
            COALESCE(q.uniform_id, 1)                                           
          AS quality_uniform_id,
            COALESCE(repurchase_prices.repurchase_price, 0)                     
          AS repurchase_price,
            COALESCE(
              CASE
                WHEN a.is_spare_part THEN spare_parts_buying_prices.buying_price
                WHEN q.is_new THEN buying_prices.buying_price
                ELSE repurchase_prices.repurchase_price
              END,
              0
            )                                                                   
          AS buying_price,
            COALESCE(pd_store.price_with_vat, 0)                                
          AS price,
            COALESCE(pd_eshop.price_with_vat, pd_store.price_with_vat, 0)       
          AS eshop_price,
            COALESCE(pd_common.price_with_vat, 0)                               
          AS common_price,
            COALESCE(pd_installment.price_with_vat, 0)                          
          AS price_for_installment_calculation,
            q.is_vat_version                                                    
          AS is_vat_version,
            a_vat_0.id                                                          
          AS fk_id_article_zero_vat_version,
            CASE WHEN q.is_new THEN COALESCE(a.cf_bo_cheque, 0) ELSE 0 END      
          AS cheque_value,
            CASE WHEN q.is_new THEN COALESCE(a.cf_cheque_explicit, FALSE) ELSE 
FALSE END  AS gold_cheque
          FROM {base_article_view} a
            LEFT JOIN article_variant av ON av.fk_id_article = a.id
            LEFT JOIN {quality_view} q ON q.cf_article_quality = 
COALESCE(a.cf_article_quality, '{new_quality_code}')
            LEFT JOIN
              (
                SELECT
                  DISTINCT ON (c.fk_id_article_variant)
                  c.fk_id_article_variant       AS fk_id_article_variant,
                  round(bp.price::NUMERIC, 2)   AS repurchase_price
                FROM price bp
                  JOIN conditions c ON bp.fk_id_conditions = c.id
                  JOIN chain ch ON c.fk_id_chain = ch.id
                WHERE ch.code='some-id' AND bp.valid_from <= now()
                ORDER BY c.fk_id_article_variant, bp.valid_from DESC
              ) AS repurchase_prices ON repurchase_prices.fk_id_article_variant 
= av.id
            LEFT JOIN
              (
                SELECT
                  DISTINCT ON (asi.fk_id_article_variant)
                  asi.fk_id_article_variant                               AS 
fk_id_article_variant,
                  ceil(asi.last_base_buying_price::NUMERIC * 1.21)        AS 
buying_price
                FROM article_store_info asi
                  JOIN store s on asi.fk_id_store = s.id
                WHERE s.code in ('some-id', 'some-id')
                ORDER BY asi.fk_id_article_variant, asi.date2 DESC, s.code
              ) AS buying_prices ON buying_prices.fk_id_article_variant = av.id
            LEFT JOIN
              (
                SELECT
                  DISTINCT ON (asi.fk_id_article_variant)
                  asi.fk_id_article_variant                               AS 
fk_id_article_variant,
                  ceil(asi.last_base_buying_price::NUMERIC * 1.21)        AS 
buying_price
                FROM article_store_info asi
                  JOIN store s on asi.fk_id_store = s.id
                WHERE s.code = 'some-id'
                ORDER BY asi.fk_id_article_variant, asi.date2 DESC
              ) AS spare_parts_buying_prices ON 
spare_parts_buying_prices.fk_id_article_variant = av.id
            LEFT JOIN price pd_eshop ON
              pd_eshop.fk_id_article_variant = av.id AND
              pd_eshop.fk_id_chain = 'some-id' AND
              pd_eshop.fk_id_price_zone = 'some-id' AND
              pd_eshop.valid_from < extract(EPOCH FROM now()) * 1000 AND
              pd_eshop.valid_to > extract(EPOCH FROM now()) * 1000
            LEFT JOIN price pd_store ON
              pd_store.fk_id_article_variant = av.id AND
              pd_store.fk_id_chain = 'some-id' AND
              pd_store.fk_id_price_zone = 'some-id' AND
              pd_store.valid_from < extract(EPOCH FROM now()) * 1000 AND
              pd_store.valid_to > extract(EPOCH FROM now()) * 1000
            LEFT JOIN price pd_common ON
              pd_common.fk_id_article_variant = av.id AND
              pd_common.fk_id_chain = 'some-id' AND
              pd_common.fk_id_price_zone = 'some-id' AND
              pd_common.valid_from < extract(EPOCH FROM now()) * 1000 AND
              pd_common.valid_to > extract(EPOCH FROM now()) * 1000
            LEFT JOIN price pd_installment ON   -- used for Cetelem payments
              pd_installment.fk_id_article_variant = av.id AND
              pd_installment.fk_id_chain = 'some-id' AND
              pd_installment.fk_id_price_zone = 'some-id' AND
              pd_installment.valid_from < extract(EPOCH FROM now()) * 1000 AND
              pd_installment.valid_to > extract(EPOCH FROM now()) * 1000
            LEFT JOIN {quality_view} q_vat_0
              ON q_vat_0.uniform_id = q.uniform_id AND NOT 
q_vat_0.is_vat_version AND q.is_vat_version
            LEFT JOIN article a_vat_0  -- #26502
              ON a_vat_0.id <> a.id
                AND a_vat_0.fk_id_vat = 'some-id'  -- zero VAT
                AND a.fk_id_vat = 'some-id'  -- 21 VAT
                AND a.fk_id_master_product = a_vat_0.fk_id_master_product
                AND a.fk_id_color_rgb = a_vat_0.fk_id_color_rgb
                AND q_vat_0.cf_article_quality = 
COALESCE(a_vat_0.cf_article_quality, '{new_quality_code}')
          WHERE
            a.fk_id_vat IS NOT NULL
            AND a.fk_id_article_producer IS NOT NULL
            AND (
              a.is_spare_part  -- spare part exception #27658
              OR a.master  -- masters exception (do not have cf_bo_for_sell, 
cf_www_visible and active2 filled)
              OR (
                NOT a.is_spare_part  -- article is not spare part (must have 
active2)
                AND a.active2 IS TRUE
              )
            )
          ORDER BY
            a.id,
            q.cf_article_quality,
            a_vat_0.id
        ) sq
    '''.format(
        view_name=Article._meta.db_table,
        base_article_view=base_article_view_sql_item.name,
        quality_view=quality_view_sql_item.name,
        new_quality_code=NEW_QUALITY_CODE,
    ),
    reverse_sql='DROP VIEW IF EXISTS 
{view_name}'.format(view_name=Article._meta.db_table),
    replace=True,
    dependencies=(
        ('my_django_app', quality_view_sql_item.name),
        ('my_django_app', base_article_view_sql_item.name),
    ),
)


You can see that SQL can be very complex. And if it is changing very often 
than is easier to manage it in one place with definition instead of 
volatile migrations. In your case you must go through all your migrations 
to find the final definition of some object. And with so many objects it 
could be complex. Next if we remove (clean) migrations in our project than 
we don't have easy way how to re-generate fake-able intial migrations from 
our code base. I know that cleaning migrations is a edge case but I think 
it would be nice if you could re-generate the whole schema also with low 
level SQL objects into migrations.

Next I think that some simple views could be generated from Django ORM 
instead of writing full SQL. But I am afraid of that ORM can't cover all 
situations for example sophisticated SQL functions. Because the database 
differences are too big. So from my perspective it is fine to have high 
abstraction ORM classes (Model, Index) but it would be even better to have 
low abstraction classes for functions, triggers, views and indices. Than 
you could easily manage the whole application database layer using same 
programming API in one place with final Python/SQL definition.

SQLItem objects could have definitions for multiple backends. So the 
re-usable Django apps could prepare custom SQLItems for all supported 
vendor backends.

Thanks,
Petr


Dne středa 11. března 2020 11:20:56 UTC+1 Adam Johnson napsal(a):
>
> Hi Petr
>
> I too often end up managing some database objects like triggers, normally 
> in order to support database migrations. I have always been happy using 
> RawSQL migration operations though.
>
> What types of database objects are you mostly using? And how?
>
> I think django-migrate-sql is a neat idea, but since it requires full 
> authorship of the SQL it doesn't provide the most ORM-like experience. It's 
> also not backend agnostic, which is something preferable for anything we 
> add to Django. I'd be more interested in implementing classes that 
> represent the underlying object type, like Models represent tables, and 
> translating changes to them into migration operations.
>
> Thanks,
>
> Adam
>
> On Wed, 11 Mar 2020 at 09:04, Petr Přikryl <nic...@gmail.com <javascript:>> 
> wrote:
>
>> In our apps we have a lot of database objects which are hard to manage in 
>> classic Django migrations. Next, we clean our migrations time to time to 
>> speed up deployment process. And it would be awesome if Django would 
>> have system for raw SQL "models" and handle migrations and dependencies 
>> automatically in makemigrations and migrate commands like 
>> django-migrate-sql-deux https://pypi.org/project/django-migrate-sql-deux/. 
>> What do you think?
>>
>> -- 
>> You received this message because you are subscribed to the Google Groups 
>> "Django developers (Contributions to Django itself)" group.
>> To unsubscribe from this group and stop receiving emails from it, send an 
>> email to django-d...@googlegroups.com <javascript:>.
>> To view this discussion on the web visit 
>> https://groups.google.com/d/msgid/django-developers/c078a0ae-9643-4d5c-b1de-f82cec2a7f33%40googlegroups.com
>>  
>> <https://groups.google.com/d/msgid/django-developers/c078a0ae-9643-4d5c-b1de-f82cec2a7f33%40googlegroups.com?utm_medium=email&utm_source=footer>
>> .
>>
>
>
> -- 
> Adam
>

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers  (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-developers+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-developers/cf7f509c-d62b-4c43-9191-65185b9d3068%40googlegroups.com.

Reply via email to