Преглед на файлове

tidy up sql statement generation for TransactionView

Daniel Sheffield преди 2 години
родител
ревизия
230eff1a2b
променени са 4 файла, в които са добавени 153 реда и са изтрити 144 реда
  1. 67 87
      app/data/PriceView.py
  2. 42 57
      app/data/TransactionView.py
  3. 6 0
      app/data/__init__.py
  4. 38 0
      app/data/util.py

+ 67 - 87
app/data/PriceView.py

@@ -4,18 +4,66 @@
 # All rights reserved
 #
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from collections import (
+    OrderedDict,
+)
+from typing import Tuple
 from psycopg.sql import (
     Identifier,
     SQL,
     Literal,
-    Placeholder,
-    Composed,
-)
-from collections import (
-    OrderedDict,
+    Composable,
 )
+from .util import get_select, get_from
 
-def get_where(unit, product=None, category=None, group=None, organic=None, limit='90 days'):
+def get_selectors(
+    unit: str,
+    product: str,
+    window: SQL
+) -> OrderedDict[Tuple[str, Composable]]:
+    return  OrderedDict([
+        ('id', Identifier('transactions', 'id')),
+        ('ts_raw', SQL("""(transactions.ts AT TIME ZONE 'UTC')::timestamp without time zone""")),
+        ('%d/%m/%y %_I%P', SQL("""(transactions.ts AT TIME ZONE 'UTC')::timestamp without time zone""")),
+        ('code', Identifier('stores', 'code')),
+        ('$/unit', SQL("""TRUNC(
+    price / quantity / convert_unit(units.name, {unit}, {product}), 4
+)""").format(unit=Literal(unit), product=Literal(product))),
+        ('last', SQL(f"""TRUNC(last_value(
+    price / quantity / convert_unit(units.name, {{unit}}, {{product}})
+) OVER {window}, 4)
+""").format(unit=Literal(unit), product=Literal(product))),
+        ('avg', SQL(f"""TRUNC(sum(price) OVER {window} / sum(
+    quantity * convert_unit(units.name, {{unit}}, {{product}})
+) OVER {window}, 4)
+""").format(unit=Literal(unit), product=Literal(product))),
+        ('min', SQL(f"""TRUNC(min(
+    price / quantity / convert_unit(units.name, {{unit}}, {{product}})
+) OVER {window}, 4)
+""").format(unit=Literal(unit), product=Literal(product))),
+        ('max', SQL(f"""TRUNC(max(
+    price / quantity / convert_unit(units.name, {{unit}}, {{product}})
+) OVER {window}, 4)
+""").format(unit=Literal(unit), product=Literal(product))),
+        ('price', SQL("""TRUNC(price, 4)""")),
+        ('quantity', SQL("""TRUNC(
+    quantity * convert_unit(units.name, {unit}, {product}), 4
+)""").format(unit=Literal(unit), product=Literal(product))),
+        ('product', Identifier('products', 'name')),
+        ('category', Identifier('categories', 'name')),
+        ('group', Identifier('groups', 'name')),
+        ('organic', Identifier('organic')),
+    ])
+
+JOINS = OrderedDict([
+    ('units', ('id', 'unit_id')),
+    ('stores', ('id', 'store_id')),
+    ('products', ('id', 'product_id')),
+    ('categories', ('id', 'category_id')),
+    ('groups', ('id', 'group_id')),
+])
+
+def get_where(product=None, category=None, group=None, organic=None, limit='90 days'):
     where = [ ]
     if product is not None:
         where.append(SQL(' ').join([
@@ -48,9 +96,6 @@ def get_where(unit, product=None, category=None, group=None, organic=None, limit
                 interval=SQL("{literal}::interval").format(literal=Literal(limit))
             )
         )
-    #where.append(SQL(
-    #    'convert_unit(units.name, {unit}, {product}) IS NOT NULL'
-    #).format(unit=Literal(unit), product=Literal(product)))
     return SQL('').join([
         SQL("WHERE"
             "\n      "),
@@ -58,93 +103,28 @@ def get_where(unit, product=None, category=None, group=None, organic=None, limit
     ])
 
 def get_historic_prices_statement(unit, sort=None, product=None, category=None, group=None, organic=None, limit='90 days'):
-    partition = f"(PARTITION BY {'organic,' if organic is not None else ''} product_id ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)"
+    window = f"""(
+PARTITION BY {'organic,' if organic is not None else ''} product_id
+ORDER BY ts
+ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
+)"""
     organic_sort = f"{'organic,' if organic is not None else ''}"
     sort_sql = SQL('').join([
         SQL('{sort} {direction},').format(
             sort=Identifier(f'{sort}'),
-            direction = SQL('DESC' if sort == 'ts' else 'ASC')
+            direction=SQL('DESC' if sort == 'ts' else 'ASC')
         ),
     ]) if sort is not None else SQL('')
 
-    select = OrderedDict([
-        ('id', Identifier('transactions','id')),
-        ('ts_raw', SQL("""(transactions.ts AT TIME ZONE 'UTC')::timestamp without time zone""")),
-        ('%d/%m/%y %_I%P', SQL("""(transactions.ts AT TIME ZONE 'UTC')::timestamp without time zone""")),
-        ('code', Identifier('stores', 'code')),
-        ('$/unit', SQL("""TRUNC(
-    price / quantity / convert_unit(units.name, {unit}, {product}), 4
-)""").format(unit=Literal(unit), product=Literal(product))),
-        ('last', SQL(f"""TRUNC(last_value(
-    price / quantity / convert_unit(units.name, {{unit}}, {{product}})
-) OVER {partition}, 4)
-""").format(unit=Literal(unit), product=Literal(product))),
-        ('avg', SQL(f"""TRUNC(sum(price) OVER {partition} / sum(
-    quantity * convert_unit(units.name, {{unit}}, {{product}})
-) OVER {partition}, 4)
-""").format(unit=Literal(unit), product=Literal(product))),
-        ('min', SQL(f"""TRUNC(min(
-    price / quantity / convert_unit(units.name, {{unit}}, {{product}})
-) OVER {partition}, 4)
-""").format(unit=Literal(unit), product=Literal(product))),
-        ('max', SQL(f"""TRUNC(max(
-    price / quantity / convert_unit(units.name, {{unit}}, {{product}})
-) OVER {partition}, 4)
-""").format(unit=Literal(unit), product=Literal(product))),
-        ('price', SQL("""TRUNC(price, 4)""")),
-        ('quantity', SQL("""TRUNC(
-    quantity * convert_unit(units.name, {unit}, {product}), 4
-)""").format(unit=Literal(unit), product=Literal(product))),
-        ('product', Identifier('products','name')),
-        ('category', Identifier('categories', 'name')),
-        ('group', Identifier('groups', 'name')),
-        ('organic', Identifier('organic')),
-    ])
     statement = SQL('\n').join([
-        SQL('').join([
-            SQL("SELECT"
-                "\n  "),
-            SQL(','
-                "\n  ").join([
-                SQL(' ').join([v, SQL('AS'), Identifier(f'{k}')]) for k, v in select.items()
-            ])
-        ]),
-        SQL('').join([
-            SQL("FROM"
-                "\n       "),
-            SQL("\n  JOIN ").join([
-                SQL("transactions"),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('units'),
-                    key=Identifier('id'),
-                    index=Identifier('unit_id'),
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('stores'),
-                    key=Identifier('id'),
-                    index=Identifier('store_id')
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('products'),
-                    key=Identifier('id'),
-                    index=Identifier('product_id')
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('categories'),
-                    key=Identifier('id'),
-                    index=Identifier('category_id')
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('groups'),
-                    key=Identifier('id'),
-                    index=Identifier('group_id')
-                ),
-            ]),
-        ]),
-        get_where(unit, product=product, category=category, group=group, organic=organic, limit=limit),
-        SQL('ORDER BY {organic_sort} {sort} code, product, category, "group", "$/unit" ASC, ts DESC').format(
+        get_select(get_selectors(unit, product, window)),
+        get_from("transactions", JOINS),
+        get_where(product=product, category=category, group=group, organic=organic, limit=limit),
+        SQL("""
+ORDER BY {organic_sort} {sort} code, product, category, "group", "$/unit" ASC, ts DESC
+""").format(
             sort=sort_sql,
             organic_sort=SQL(organic_sort),
-        ),
+       ),
     ])
     return statement

+ 42 - 57
app/data/TransactionView.py

@@ -4,16 +4,16 @@
 # All rights reserved
 #
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from collections import (
+    OrderedDict,
+)
 from psycopg.sql import (
+    Composable,
     Identifier,
     SQL,
     Literal,
-    Placeholder,
-    Composed,
-)
-from collections import (
-    OrderedDict,
 )
+from .util import get_select, get_from
 
 def get_table_statement(alias):
     tables = {
@@ -32,25 +32,38 @@ def get_table_statement(alias):
 def get_transactions_statement():
     return SQL("SELECT * FROM transaction_view")
 
-select = OrderedDict([
-    ('id', Identifier('transactions','id')),
+SELECT = OrderedDict([
+    ('id', Identifier('transactions', 'id')),
     ('ts', Identifier('transactions', 'ts')),
     ('store', Identifier('stores', 'name')),
     ('code', Identifier('stores', 'code')),
-    ('description', Identifier('transactions','description')),
+    ('description', Identifier('transactions', 'description')),
     ('volume', Identifier('quantity')),
     ('unit', Identifier('units', 'name')),
     ('price', Identifier('price')),
     ('$/unit', SQL("""TRUNC(price/quantity,4)""")),
-    ('total', SQL("""sum(transactions.price) OVER (PARTITION BY transactions.ts::date ORDER BY transactions.id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)""")),
-    ('group', Identifier('groups','name')),
-    ('category', Identifier('categories','name')),
-    ('product', Identifier('products','name')),
+    ('total', SQL("""sum(transactions.price)
+OVER (
+    PARTITION BY transactions.ts::date
+    ORDER BY transactions.id
+    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+)""")),
+    ('group', Identifier('groups', 'name')),
+    ('category', Identifier('categories', 'name')),
+    ('product', Identifier('products', 'name')),
     ('organic', Identifier('organic')),
 ])
 
+JOINS = OrderedDict([
+    ('units', ('id', 'unit_id')),
+    ('stores', ('id', 'store_id')),
+    ('products', ('id', 'product_id')),
+    ('categories', ('id', 'category_id')),
+    ('groups', ('id', 'group_id')),
+])
+
 def get_where(date, store, full_name=False, exact_time=False):
-    where = [ ]
+    where = []
     if store is not None:
         where.append(SQL(' ').join([
             Identifier('stores', 'name' if full_name else 'code'),
@@ -73,51 +86,23 @@ def get_where(date, store, full_name=False, exact_time=False):
         SQL("\n  AND ").join(where),
     ])
 
+def get_sort() -> Composable:
+    return SQL('ORDER BY {_id} DESC').format(
+        _id=Identifier('transactions', 'id')
+    )
+
 def get_session_transactions_statement(date, store, full_name=False, exact_time=False):
     statement = SQL('\n').join([
-        SQL('').join([
-            SQL("SELECT"
-                "\n  "),
-            SQL(','
-                "\n  ").join([
-                SQL(' ').join([v, SQL('AS'), Identifier(f'{k}')]) for k, v in select.items()
-            ])
-        ]),
-        SQL('').join([
-            SQL("FROM"
-                "\n       "),
-            SQL("\n  JOIN ").join([
-                SQL("transactions"),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('units'),
-                    key=Identifier('id'),
-                    index=Identifier('unit_id'),
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('stores'),
-                    key=Identifier('id'),
-                    index=Identifier('store_id')
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('products'),
-                    key=Identifier('id'),
-                    index=Identifier('product_id')
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('categories'),
-                    key=Identifier('id'),
-                    index=Identifier('category_id')
-                ),
-                SQL("{table} ON {table}.{key} = {index}").format(
-                    table=Identifier('groups'),
-                    key=Identifier('id'),
-                    index=Identifier('group_id')
-                ),
-            ]),
-        ]),
-        get_where(date if exact_time else date.replace(
-            hour=0, minute=0, second=0, microsecond=0
-        ), store, full_name=full_name, exact_time=exact_time),
-        SQL('ORDER BY {_id} DESC').format(_id=Identifier('transactions','id')),
+        get_select(SELECT),
+        get_from("transactions", JOINS),
+        get_where(
+            date if exact_time else date.replace(
+                hour=0, minute=0, second=0, microsecond=0
+            ),
+            store,
+            full_name=full_name,
+            exact_time=exact_time
+        ),
+        get_sort(),
     ])
     return statement

+ 6 - 0
app/data/__init__.py

@@ -0,0 +1,6 @@
+#
+# Copyright (c) Daniel Sheffield 2023
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY

+ 38 - 0
app/data/util.py

@@ -0,0 +1,38 @@
+#
+# Copyright (c) Daniel Sheffield 2023
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from typing import Tuple
+from psycopg.sql import (
+    Identifier,
+    SQL,
+    Composable,
+)
+
+def get_select(alias_to_sql: dict[str,Composable]) -> Composable:
+    select = SQL(""",
+    """).join([
+        SQL(' ').join([
+            v, SQL('AS'), Identifier(k)
+        ]) for k, v in alias_to_sql.items()
+    ])
+    return SQL("""
+    """).join([SQL("SELECT"), *select])
+
+def get_from(
+    base: str,
+    table_to_join_on: dict[Tuple[str, Tuple[str,str]]]
+) -> Composable:
+    joins = [
+        SQL("{table} ON {table_column} = {other_column}").format(
+            table=Identifier(table),
+            table_column=Identifier(table, table_column),
+            other_column=Identifier(other_column)
+        ) for table, (table_column, other_column) in table_to_join_on.items()
+    ]
+    return SQL('').join([SQL("""FROM {base}
+JOIN """).format(base=Identifier(base)),
+        SQL("""
+JOIN """).join(joins)])