Revisione di progetto · per il team GenroPy

Asqueel e GenroPy SQL

Asqueel è il porting del layer SQL di GenroPy in una libreria indipendente. Questo documento descrive Asqueel come sarà quando le decisioni qui sotto saranno implementate, confronta ogni parte con il layer SQL di GenroPy e chiede al team di mettere in discussione le scelte.

Data 4 ottobre 2026 GenroPy origin/develop a 5e7f02e8d774 Fonti proposta 0001 e analisi 34–39

Come rivedere

Ciò che segue descrive decisioni di progetto, prese una per volta e ciascuna confrontata con GenroPy prima di essere scritta. Non descrive codice esistente.

Decisa concordata e registrata; le obiezioni servono ancora Direzione l'obiettivo è deciso, i dettagli no Proposta scritta nei piani, non ancora confermata Aperta non decisa; il tuo parere la orienta

Sotto ogni decisione trovi tre pulsanti (D'accordo, Contrario, Non so) e un campo per la motivazione. In fondo a ogni sezione c'è un campo per ciò che manca. Indica in particolare:

In questa fase non proporre soluzioni alternative. Scrivi cosa non funzionerebbe, cosa resterebbe fuori e cosa diventerebbe scomodo. Raccolti i pareri, ci sarà una sintesi. Per i punti controversi si potranno allora proporre alternative. Ogni alternativa andrà valutata sul resto del disegno: una soluzione che sembra logica in un punto può avere effetti su altre parti.

Il parere si salva da solo. Tutti i lettori vedono i pareri di tutti; ognuno modifica solo i propri. I numeri sull'uso reale vengono dall'indice Sourcerer dei repository del team; ogni fatto sul legacy ha file e riga nei documenti di analisi citati in fondo.

Riepilogo dei pareri

Una riga per decisione. «Discorde» vuol dire almeno un parere d'accordo e almeno uno contrario.

DecisioneStatoD'accordoContrarioNon soNote
Nessun parere ancora. Il primo che lasci comparirà qui.

Cos'è Asqueel

Asqueel conserva ciò che rende prezioso il layer database di GenroPy, cioè il modello come unico luogo dove vivono tabelle, relazioni, colonne calcolate e le loro descrizioni, e lo rende usabile fuori da GenroPy.

Una dichiarazione del modello o l'adattatore GenroPy costruiscono un albero sorgente; i renderer producono oggetti del modello, struttura di migrazione e riferimento della grammatica; compiler, dialect e driver eseguono le query sul database. Applicazione Python Dichiarazione del modello configure(self, tables) Applicazione GenroPy config_db, sysFields attributi di tabella, griglie Adattatore GenroPy Albero sorgente genro-builders Oggetti del modello tabelle, colonne, relazioni Struttura di migrazione asqueel-migration Riferimento grammatica documentazione generata renderer compiler dialect driver PostgreSQL SQLite

Restano fuori dal nucleo di Asqueel, e appartengono all'adattatore o all'applicazione: l'interfaccia utente, la composizione dei package GenroPy e le notifiche dell'applicazione web.

Principi Decisa

Perché contano i builder

GenroPy dichiara un modello chiamando metodi che scrivono attributi in una Bag. Asqueel lo dichiara con una grammatica costruita con genro-builders. Gran parte delle differenze di questo documento discende da ciò che una grammatica dà e gli attributi liberi no. È la base di ogni riquadro «Perché cambia».

Capacità dei builderCosa significa per un modelloGenroPy oggi
Ogni elemento è un metodo con firma tipizzata (@element)Ogni dichiarazione ha un insieme noto di parametri. Una keyword sconosciuta è un errore di validazione; le estensioni libere usano il prefisso x_.Gli attributi sconosciuti di tabella e colonna vengono salvati in silenzio. Refusi come rowCaption, captionField, partitioned_progetto_id esistono nelle applicazioni e nessuno li legge.
Regole di contenimento (parent_tags, sub_tags con cardinalità)«Al massimo un options per tabella», «una funzione solo dentro options», «una relazione solo su una colonna» sono regole della grammatica, controllate alla costruzione del modello.Controllate per convenzione, o per niente.
Due fasi: si costruisce un albero sorgente, poi lo si rendeLa dichiarazione è una ricetta. I renderer trasformano la stessa sorgente in oggetti del modello, nella struttura di migrazione e nella documentazione. Un renderer può leggere prima un nodo e produrne più oggetti.Dichiarazione e modello compilato nascono in un solo passaggio; helper come sysFields scrivono colonne mentre la tabella si dichiara.
Introspezione della grammaticaIl riferimento della grammatica (docs/grammar.md) è generato da firme e docstring, e un test lo confronta byte per byte. Ogni opzione ha una voce documentata con parametri e default.Quasi tutti gli attributi di tabella non hanno documentazione (l'analisi 38 elenca circa 80 nomi e famiglie, quasi tutti non documentati).
Instradamento dei figli (SqlBuilder.set_child)Chi scrive usa tbl.column(...) sulla tabella; il builder lo mette nel contenitore interno giusto. Verificato con un prototipo.Stessa esperienza per chi scrive, meccanismo diverso.

Un esempio: un refuso nel nome di un attributo.

GenroPy

tbl.column('stato', grouo='_')
# accettato: l'attributo grouo viene salvato
# e nessuno lo legge; group resta vuoto

Asqueel

tbl.column('stato', grouo='_')
# errore di validazione alla costruzione:
# column() non ha il parametro 'grouo'

Sorgente e modello costruito Decisa

Ogni modello passa per due fasi:

Le regole valgono a ogni livello: database, schema, package, tabella, colonna, relazione, gruppo, opzione.

Perché cambia. In GenroPy il sorgente db.model.src resta accessibile accanto agli oggetti db.model.obj (gnrsqlmodel/model.py:78, :187). Il codice applicativo legge il sorgente ad avvio finito (gnrapp.py:802, gnrsql/schema.py:58); gli hook di costruzione lo modificano (gnrdbo.py:567, :620, :1674); SqlTable passa per il proxy .model (gnrsqltable/table.py:123, columns.py:203).

Dichiarare un modello

GenroPy

class Table(object):
    def config_db(self, pkg):
        tbl = pkg.table('invoice', pkey='id',
                        name_long='!![en]Invoice',
                        caption_field='number')
        self.sysFields(tbl, counter='customer_id')
        tbl.column('number', size=':10')
        tbl.column('customer_id', size='22').relation(
            'sales.customer.id', mode='foreignkey',
            relation_name='invoices', onDelete='raise')
        tbl.column('total', dtype='N', size='12,2',
                   group='amounts.010')
        tbl.aliasColumn('customer_name',
                        '@customer_id.name')

Asqueel

class InvoiceModel:
    def configure(self, tables):
        tbl = tables.table('invoice', pkey='id',
                           name_long='Invoice',
                           caption_field='number')
        opts = tbl.options()
        opts.sys_fields()
        opts.auto_counter('customer_id')
        tbl.column('number', size=':10')
        tbl.column('customer_id', size='22').relation(
            'sales.customer.id',
            relation_name='invoices', on_delete='raise')
        amounts = tbl.colgroup('amounts', name_long='Amounts')
        amounts.column('total', dtype='N', size='12,2')
        tbl.aliasColumn('customer_name',
                        relation_path='@customer_id.name')
        return tbl

Le colonne si dichiarano sulla tabella Decisa

Come in GenroPy: tbl.column, tbl.formulaColumn, tbl.aliasColumn, tbl.index. Un nome usato sia da una colonna fisica sia da una virtuale è un errore; l'_override di GenroPy arriva con la composizione dei package. Elementi di colonna: column, formulaColumn, aliasColumn, subQueryColumn, pyColumn, compositeColumn, bagItemColumn.

Valori dentro una colonna Bag Direzione

bagItemColumn(nome, bagcolumn='$metadata', itempath='status', dtype=...) legge un valore da dentro una colonna di tipo Bag. Nelle applicazioni è molto usata: 77 moduli in 23 repository, circa 200 dichiarazioni. In GenroPy l'estrazione usa XPath sulla forma XML della Bag. Da progettare: l'SQL che estrae il valore, per dialetto, e il tipo del risultato.

Subtable Direzione

In GenroPy una subtable è un sottoinsieme con nome di una tabella: tbl.subtable('industriale', condition='$conto_industriale IS TRUE'). Si interroga con query(subtable='industriale'), anche combinando i nomi (a&b, a|b, !a); la tabella può avere un default_subtable. Esiste anche una forma di package, pkg.subtable(nome, maintable='pkg.tab'), che copia colonne e relazioni della tabella principale e distingue le righe con la colonna __subtable. Nelle applicazioni la forma di tabella è in almeno 25 moduli di 10 repository.

In asqueel una subtable diventa un oggetto del modello di prima classe. Si dichiara nella grammatica della tabella, eredita la configurazione della tabella principale (colonne, relazioni, opzioni) e aggiunge sempre la propria condizione. Si interroga come una tabella e può essere target di una relazione.

Da decidere: il nome con cui si interroga; se una subtable aggiunge colonne virtuali proprie; cosa fa un insert in una subtable (controlla la condizione o imposta un valore); se la forma di package diventa un caso della stessa grammatica.

Altri elementi di colonna Aperta

Elemento GenroPyCosa faUso nelle applicazioniProposta
joinColumncolonna virtuale con .relation(target, cnd=...): collega la riga a un'altra tabella con una condizione, senza FKcirca 12 usi in 8 repositorysostituita da virtualRelation; l'adattatore traduce
toolColumncolonna virtuale che mostra un link verso uno strumento esterno (per esempio un file ICS); con dtype='P' un'immagine1 usoda decidere
aliasTablenome breve per un path di relazione ('@invoice_id.@customer_id')nessun uso attivo (2 commentati)da decidere
colonne localizzate (localized=True)una colonna con un valore per lingua del database, gestita dal packagepochi usi (teamset)da decidere

Un solo metodo, configure Decisa

Una tabella si dichiara in configure(self, tables), che restituisce la tabella. Non esistono config_db né metodi per ruolo. L'adattatore GenroPy chiama i config_db del legacy e li traduce.

Gruppi di colonne Decisa

tbl.colgroup(nome, name_long=..., col_*=...) dichiara un gruppo una volta. Una colonna vi entra se dichiarata tramite il gruppo, oppure con colgroup='nome' sulla tabella. L'ordine è quello di dichiarazione; non ci sono numeri di posizione.

Perché cambia. In GenroPy group='amounts.010' mescola raggruppamento, ordine e visibilità nell'interfaccia in una stringa, e il contatore del colgroup legacy non avanza mai (ogni colonna riceve .001). Come elemento della grammatica un gruppo ha la sua firma, default per le sue colonne (col_*) e tre errori controllati: gruppo non dichiarato, gruppo in conflitto, gruppo dichiarato due volte.

I gruppi raggruppano e basta: la visibilità nell'interfaccia e la posizione dei nodi di relazione nell'albero dei campi non fanno parte del nucleo. colgroup accetta solo name_long e i default col_*; gli attributi d'interfaccia, se servono, passano per x_ui. Un metodo chiamato su un gruppo si comporta come sulla tabella (un indice dichiarato tramite un gruppo prende il nome della tabella).

Relazioni Decisa

ElementoDatabaseTarget
relation(...)FOREIGN KEY ... ON UPDATE CASCADE più un indice sulle colonne sorgentedeve avere una chiave unica; altrimenti errore
weak_relation(...)nessun vincolo; indice su colonne sorgente e targetcreduto unico; insensitive=True confronta senza maiuscole

I parametri tengono i nomi di GenroPy in snake_case: related_column, relation_name, one_name, many_name, one_one, on_delete / on_update (azioni Python sui record collegati), on_delete_sql / on_update_sql (azioni SQL della FK), deferred, deferrable, initially_deferred. La relazione uscente prende il nome della sua colonna (@customer_id).

Ogni FK nasce con ON UPDATE CASCADE, come in GenroPy dal 27 novembre 2018 (commit 8f5dd4e950, «onUpdate_sql='cascade' is default»). Lo stesso commit introduce changePrimaryKeyValue(pkey, newpkey): la cascata serve a cambiare il valore della chiave primaria di un record, e quello dei codici referenziati, con le FK che seguono il nuovo valore invece di bloccare l'update. on_update_sql=None la toglie su una singola relazione.

Perché cambia. In GenroPy un relation() semplice è logico e mode='foreignkey' aggiunge il vincolo. Negli otto repository applicativi più grandi 3555 relazioni su 4134 (86%) usano mode='foreignkey', quindi il caso comune diventa quello breve. Due elementi al posto di una stringa mode danno a ciascun tipo il suo insieme chiuso di parametri: insensitive esiste solo dove può funzionare, perché una FK non confronta senza maiuscole. Target non unici, relazioni filtrate e «una riga per ordine» li copre virtualRelation (sezione «Relazioni virtuali»).

Chiavi composite e vincoli Decisa

Un solo concetto copre ogni chiave su più colonne, come in GenroPy: compositeColumn(nome, columns='a,b', unique=True). Il suo valore è la chiave serializzata ('["P1", 2024]'); * la seleziona; unique=True crea l'UNIQUE su più colonne; .relation(...) su una composita punta a un'altra composita. pkey nomina sempre una colonna, fisica o composita. L'elemento constraint esce dal modello: i CHECK tornano con gli altri oggetti nativi del database (trigger, funzioni, view).

Ogni relazione crea un indice sulle colonne sorgente, salvo che siano già le prime colonne della chiave primaria, di un UNIQUE o di un indice. Non c'è un'opzione per spegnerlo. Ogni FK nasce con ON UPDATE CASCADE, come in GenroPy.

Una chiave primaria composita è pkey='<nome della compositeColumn>'; nel modello costruito pkeys restituisce le colonne membro (sezione «Leggere il modello costruito»). Il valore serializzato è l'identità di un record con chiave composita: è il valore pkey di risultati e record, e GenroPy lo rilegge con parseSerializedKey.

Opzioni della tabella: options

GenroPy aggiunge colonne di sistema e comportamenti con gli argomenti di sysFields(...), con attributi di tabella e con classi base. Asqueel raccoglie questi comportamenti in un blocco visibile, tbl.options(), dove ogni opzione è un elemento. Le funzioni si prendono da ciò che GenroPy offre, non da come lo implementa.

Le prime tre opzioni Decisa

OpzioneFunzioniOrigine in GenroPy
sys_fieldschiave primaria generata; timestamp di insert e update; utente di insert e update; cancellazione logica; bozzasysFields(id, ins, upd, user_ins, user_upd, ldel, draftField)
auto_counternumerazione delle righe sulla tabella o dentro una FK; numerazione dentro un master che non riusa i numerisysFields(counter=..., counter_<nome>=..., relidx=...). Non riguarda i contatori di protocollo, cioè i metodi counter_<campo> della tabella.
hierarchicalstruttura ad albero (sezione successiva)sysFields(hierarchical=...) e sotto-opzioni

Regole: al massimo un options per tabella; una funzione fuori da options è un errore; auto_counter può comparire una volta per FK; le dipendenze fra funzioni le controlla il validatore (per esempio hierarchical richiede una chiave primaria a una sola colonna).

Perché cambia. Chi scrive il modello dichiara solo colonne applicative. Le colonne di sistema le aggiunge un'opzione: si chiede la funzione, e l'opzione crea le colonne di cui ha bisogno. La loro configurazione passa per l'opzione, non per tbl.column. Per questo sys_fields resta in options anche se aggiunge colonne. Poiché ogni opzione è un elemento della grammatica, un refuso è un errore invece di un attributo ignorato, e il riferimento generato documenta ogni opzione. In GenroPy la stessa conoscenza è sparsa fra gli argomenti di sysFields (1617 chiamate, il 58% senza argomenti) e attributi di tabella letti in molti punti.

Parametri delle opzioni che aggiungono colonne Proposta

Ogni parametro di un'opzione che aggiunge una colonna accetta tre valori:

opts.sys_fields(ins={'name_long': 'Creato il'}, ldel=False)

I nomi dei parametri sono quelli di GenroPy (ins, upd, ldel, user_ins, ...). Il dict non cambia il nome della colonna (__ins_ts) né gli attributi che ne fanno la funzione (onInserting='setTSNow'). Non esiste una forma con kwargs a prefisso. In GenroPy questi parametri accettano solo vero o falso e le colonne hanno attributi fissi (gnrdbo.py:324).

Cosa diventa un'opzione Aperta

Misura su 5720 file di modello applicativi in 66 repository:

GruppoAttributi (file che li usano)
Proprietà della tabella, usate quasi ovunquename_long 5707, name_plural 5317, pkey 5219, caption_field 5047
Funzioni usate davverorowcaption 675, lookup 561, partition_* 240, multidb 134, order_by 105, group_* 90, unifyRecordsTag 58, broadcast 57, archivable 40, audit 20
Funzioni scritte come classi baseAttachmentTable 206 file, TotalizeTable 13
Mai usati nelle applicazionilastTS, noChangeMerge, noTestForMerge, sqlschema, comment

Le altre funzioni di sysFields (concorrenza ottimistica, audit, diagnostica, protezione di riga, campi invalidi, unione di record, record di sistema, aggiornamenti per versione) si discuteranno una per una. Anche i parametri di ogni opzione sono aperti.

Opzioni registrate da un package Aperta

Dieci package GenroPy in sette repository aggiungono comportamento a tabelle di altri package nello stesso modo: la tabella dichiara un attributo del package (multidb, wkfl, survey, e2l, tmsh_bookable, multi_tenant, ...) e il package lo legge e aggiunge un mixin alla tabella. Asqueel deve permettere a un package di registrare le proprie opzioni, con firma e documentazione. I default di package per tutte le tabelle sono rari (politica dei dbstore in sei package) e non sono un primo bisogno. Le classi Table di package nei main.py (199 package) non ridefiniscono mai gli hook di scrittura né sysFields.

Esempio in GenroPy: la tabella dichiara pkg.table('customer', ..., multidb=True). Il package multidb, nel suo hook onBuildingDbobj, legge l'attributo multidb di ogni tabella nel sorgente e aggiunge alla tabella le colonne e le relazioni che servono (projects/gnrcore/packages/multidb/main.py:70). L'attributo non ha firma né documentazione: un refuso viene ignorato.

Obiettivo in Asqueel: la tabella chiede la funzione del package come un elemento di options, con firma e documentazione, come sys_fields. Domanda aperta: con quale meccanismo un package registra un proprio elemento di options.

Tabelle gerarchiche

Modello: quello di GenroPy Decisa

Una relazione parent_id più path materializzati, fisici, della chiave primaria (hierarchical_pkey) e dei campi dichiarati (hierarchical_<campo>). Spostare un nodo o rinominare un campo del path riscrive il sottoalbero un figlio alla volta, con i trigger Python. Le tabelle gerarchiche sono molto statiche, quindi le letture contano più del costo di una riscrittura rara. L'ordine fra fratelli è auto_counter('parent_id'); la chiave di ordinamento _h_count tiene i 2 caratteri in base 36 per livello di GenroPy.

GenroPy

self.sysFields(tbl, hierarchical='description',
               counter=True, hierarchical_root_id=True)

Asqueel

opts = tbl.options()
opts.sys_fields()
opts.hierarchical('description')
opts.auto_counter('parent_id')

Correzioni Decisa

GenroPyAsqueelIssue
Spostare un nodo sotto un suo discendente non è controllato sul server: cascata senza fineErroregenropy/genropy#1523
Padre e figli letti con i filtri di bozza e cancellazione logica: il figlio di un padre archiviato diventa radiceLettura senza filtri#1524
Un nodo spostato tiene la vecchia posizione fra i fratelli; _h_count diventa vuoto oltre 1295 fratelli: usa 2 caratteri in base 36 per livello, e 36² − 1 = 1295Ultima posizione sotto il nuovo padre; errore esplicito al 1296° fratello#1525
LIKE sui path senza escape (gli id contengono _); nessun indice utilizzabile da LIKE 'x%'LIKE con escape; indice text_pattern_ops su PostgreSQL#1526

Il nucleo offrirà anche sottoalbero, antenati e radice, con «nodo incluso» o «nodo escluso» esplicito. Oggi le applicazioni scrivono il filtro del sottoalbero a mano in tre forme diverse. La forma si deciderà insieme alle funzioni di relazione e a virtualRelation.

Punti aperti Aperta

Valore nullo in un campo del path; / dentro un valore; copyFromParent applicato solo all'insert e allo spostamento; _parent_h_* calcolato invece che fisico. Da progettare anche: hierarchical_linked_to, nodi virtuali, hdepth (da uno a tre usi ciascuno).

Scrivere espressioni

Il linguaggio delle espressioni è quello di GenroPy. Asqueel non aggiunge grafie alternative; una forma non supportata dà errore e non produce mai un risultato diverso. Un solo parser e un solo resolver sono condivisi da query, formule, policy di riga e column() del modello costruito.

Sintassi Decisa

FormaSignificato
$colcolonna della tabella corrente
@customer_id.namecolonna raggiunta tramite la relazione di customer_id
@customer_id.@state_id.namepiù passaggi; ogni segmento di relazione inizia con @. @customer.country.name (segmento intermedio senza @) è un errore, come in GenroPy
:nameparametro
:env_namevalore dall'ambiente
#THIS.<path>colonna della riga esterna in una subquery correlata
#NOME(argomenti)macro, in maiuscolo, registrata per livello e contesto; comprende le funzioni di riga (sotto)
#namesubquery con nome di una formula, in minuscolo e senza parentesi

#THIS è una radice di path, come $ e @, non una macro: non si registra, ed è ammesso solo in un contesto correlato. L'SQL nativo del dialetto (COALESCE, CASE, ||, ...) passa nell'espressione così com'è, come in GenroPy. Proposta

Nomi delle relazioni

Macro

MacroRegoleStato
#THIS.<path>Colonna o path della riga esterna in una subquery correlata; la risolve la query esterna e i suoi join appartengono alla query esterna. Funziona nelle condizioni select / exists e dentro sql_formula, dove GenroPy non espande #THIS.@rel.col (genropy/genropy#1505). Fuori da un contesto correlato è un errore.Decisa
#IN_RANGE(valore, inizio, fine)Vero quando inizio <= valore <= fine. Un estremo NULL lascia aperto quel lato; due estremi NULL danno sempre vero (semantica di GenroPy). Gli argomenti sono espressioni qualsiasi: colonna, path, parametro, :env_name, #THIS.<path>. Si espande in ogni contesto, group by, having e query di record comprese.Decisa
#PERIOD($campo, :p)Filtra $campo sul periodo descritto dal valore di p ('today', 'today;today+7', 'questa settimana', '2024', ...). Il periodo lo decodifica genro_toolbox.dates.parse_period(text, workdate, locale) (genro-toolbox 0.15.0) con workdate e locale del contesto; un valore non riconosciuto alza PeriodError. Il parser riproduce 2796 casi di GenroPy; le 432 divergenze approvate sono elencate con il motivo in genro-toolbox. L'SQL è sempre semiaperto: $campo >= :p_from AND $campo < :p_to_next, con p_to_next il giorno dopo la fine. Un valore letterale va tra apici: #PERIOD($date, '2024'). Un parametro mancante è un errore.Decisa; :p o p da rivedere con #1509
#PERIOD($date, $col)Non ammesso: il testo del periodo si decodifica in Python durante la compilazione, prima che esista una riga. Neanche GenroPy espande questa forma. Si usa #IN_RANGE($date, $inizio, $fine) quando la riga contiene le due date, oppure un parametro (:p, :env_p).Decisa
#ENVNon portato: lo sostituisce :env_name.Decisa
#BAG, #BAGCOLS, TSQUERY, VECQUERY, #PREFDa progettare.Aperta

Perché cambia. #THIS.@rel.col in sql_formula funziona come ogni altro path, mentre GenroPy non lo espande. #PERIOD usa un intervallo semiaperto: su una colonna data il risultato è quello di GenroPy, su un timestamp l'ultimo giorno è incluso, cosa che il BETWEEN di GenroPy perdeva.

Leggere il modello costruito

Dichiarare e leggere sono due API distinte.

Negli esempi di questa sezione invoice = db.table('sales.invoice').

column() ed eccezioni Decisa

Nome del metodo di lettura Aperta

Da valutare: il metodo di lettura si chiama get_column invece di column. Il nome diverso separa la lettura dalla dichiarazione tbl.column di configure. L'adattatore GenroPy accetta ancora column, con un avviso di deprecazione.

Funzioni di relazione Proposta

Una funzione applicata a un path di relazione dà un valore per ogni riga della tabella principale: @invoices.COUNT(), @invoices.SUM($total), @invoices.@rows.COUNT(DISTINCT($product_id)). Ogni espressione si compila in una subquery correlata sulla tabella collegata. La granularità è giusta per costruzione: asqueel non compila mai un join che moltiplica le righe della tabella principale e non riaggrega mai righe in Python.

La regola

Una funzione opera su una riga o su un insieme di righe, e la forma dice quale:

FormaOpera suEsempio
#FUNZIONE(argomenti)la riga; i path negli argomenti raggiungono le righe collegate verso una riga#SUM($name, ' ', @customer_id.name)
@molti.FUNZIONE(argomenti)l'insieme delle righe di una relazione verso molte righe@invoices.SUM($total)

Le funzioni di asqueel si scrivono in maiuscolo. Una funzione in minuscolo (@invoices.sum(...)) è un errore che indica la forma giusta. Una funzione su una relazione verso una riga (@customer_id.SUM(...)) è un errore: si usano i path dentro la macro. Una relazione verso molte righe dentro un path richiede sempre una funzione di aggregazione con argomenti espliciti: cosa si aggrega e come si legge dall'espressione. Un path che la attraversa senza funzione (@invoices.total) è un errore.

Funzioni di aggregazione

Forma: <path di relazione>.<FUNZIONE>(<argomenti>). Funzioni: COUNT, SUM, AVG, MIN, MAX, JSON_DICT, JSON_LIST. Il modificatore DISTINCT(...) vale per l'argomento; COUNT() non ha argomenti. Gli argomenti seguono la grammatica generale dei campi sulla tabella collegata: $col, un path, un'espressione (@rows.SUM($quantity * $unit_price)).

EspressioneSubquery compilata
@invoices.COUNT()SELECT COUNT(*) FROM invoice t WHERE t.customer_id = t0.id
@invoices.SUM($total)SELECT SUM(t.total) FROM invoice t WHERE t.customer_id = t0.id
@invoices.@rows.SUM($quantity)SELECT SUM(r.quantity) FROM invoice_row r JOIN invoice i ON r.invoice_id = i.id WHERE i.customer_id = t0.id

Più espressioni su relazioni diverse convivono nella stessa SELECT, ognuna con la propria granularità. Nessun DISTINCT sulla query esterna, nessuna rielaborazione in Python.

Dove si usano

Ovunque può stare un campo: colonne, where (scritto a mano o costruito da un editor di query), order_by, group_by, having, sql_formula di una formula, condizioni delle relazioni. Esempi: where='@invoices.SUM($total) > :soglia', order_by='@invoices.COUNT() DESC'. Un solo resolver serve tutti questi punti.

Nome del risultato

Senza AS il nome viene dalla struttura dell'espressione, nella convenzione di _invoices_total: @invoices.COUNT() → _invoices_count; @invoices.SUM($total) → _invoices_sum_total; @invoices.AVG(@rows.SUM($quantity)) → _invoices_avg_rows_sum_quantity. Con #TEMPLATE(...) o una stringa libera AS è obbligatorio. Aperto: la regola di troncamento oltre il limite degli identificatori (63 caratteri su PostgreSQL).

Funzioni di riga: #SUM, #TEMPLATE, #JSON

Una funzione di riga calcola un valore dai campi di una riga. Le funzioni di riga sono macro (sezione «Macro»): # e nome maiuscolo, registrate per livello e contesto. Ogni argomento segue la grammatica generale, quindi un path verso una riga collegata o un aggregato sono argomenti validi: prima si calcolano gli aggregati, poi la funzione di riga li combina.

Colonne aggregabili nel modello

Una colonna dichiara di essere aggregabile con aggregate=; il valore dipende dal tipo:

Tipoaggregate=TrueAltri valori
numerico (N, L, R, I)SUMelenco di funzioni: aggregate='SUM,AVG,MIN,MAX'
testo (A, T)unione con ','il separatore: aggregate=' / '
data, timestamp, booleanotesto in formato ISO, unito con ','il separatore; aggregate='MIN,MAX' usa le funzioni SQL native

Su una singola relazione .aggregations('quantity:SUM,AVG', 'unit_price:SUM'), concatenato a relation(), restringe o estende le funzioni di quella relazione e prevale sulla colonna. La dichiarazione è facoltativa e serve alle interfacce: i metadati del modello elencano gli aggregati disponibili sotto ogni relazione verso molte righe, con il loro tipo.

Dialetti

FunzionePostgreSQLSQLite
COUNT, SUM, AVG, MIN, MAX, COUNT(DISTINCT(x))standardstandard
SUM sul testostring_agg(expr, sep ORDER BY ...)group_concat(expr, sep), ordine non garantito
#TEMPLATE, #SUM sul testo||||
#JSONjson_build_objectjson_object
JSON_LISTjson_aggjson_group_array
JSON_DICTjson_object_aggjson_group_object; le chiavi duplicate non danno errore, il controllo lo fa asqueel in compilazione

Relazioni virtuali Proposta

virtualRelation dichiara una relazione derivata: non fisica, in sola lettura. Copre le relazioni filtrate, i collegamenti senza FK, una riga scelta per ordine e limite. È l'unico modo di legare una condizione a una relazione. Si naviga come ogni relazione (@last_invoice.date) e le funzioni di relazione valgono anche su di essa (@invoices_current_year.SUM($total)).

tbl.virtualRelation(name, relation=None, table=None, condition=None,
                    order_by=None, limit=None, one_one=False, ask=None,
                    var_<nome>=<espressione della tabella>, **attrs)

Parametri

# nella tabella customer
tbl.virtualRelation('invoices_current_year', relation='@invoices',
                    condition='$year = :env_current_year')
tbl.virtualRelation('discount_tiers', table='invc.discount_tier',
                    condition='$customer_type_code = :var_type',
                    var_type='$customer_type_code')
tbl.virtualRelation('invoices_over_credit', relation='@invoices',
                    condition='$total > :var_credit_limit',
                    var_credit_limit='@category_id.credit_limit')
tbl.virtualRelation('invoices_above_prev_avg', relation='@invoices_current_year',
                    condition='$total > :var_prev_avg',
                    var_prev_avg='@invoices_prev_year.AVG($total)')
tbl.virtualRelation('last_invoice', relation='@invoices',
                    order_by='$date DESC', limit=1)

Accoppiamento e filtro

Il compiler divide condition nei suoi termini in AND e li classifica dalle variabili che contengono:

Con table= e senza relation= serve almeno un termine di accoppiamento; altrimenti la condizione descrive un prodotto cartesiano filtrato. Una relazione ristretta da un filtro resta una relazione: la differenza fra accoppiamento e filtro sta nei termini, non nella natura. Conta per la composizione (i filtri si sommano in AND, le relazioni si concatenano come path) e per l'inversa.

Regole

Inversa

Si genera su richiesta dai soli termini di accoppiamento, scambiando i ruoli. Con termini di filtro l'inversa è una funzione parziale. Aperto: vietarla, oppure registrarla senza il filtro.

Il nome

virtualRelation vale in ogni caso (relation=, table=, one_one, limit) e dice l'invariante: una relazione non fisica, in sola lettura, derivata. È coerente con gli altri costrutti virtuali (formulaColumn, aliasColumn). manyRelation descrive solo la cardinalità di default; «view» promette proiezioni e colonne calcolate che il costrutto non ha: restringe le righe, le colonne restano quelle della tabella collegata.

Cosa sostituisce in GenroPy

GenroPyIn asqueel
joinColumn(nome).relation(target, cnd=...) (circa 12 usi in 8 repository)virtualRelation(..., table=target, condition=..., var_...=...)
relation(..., cnd= / join_on= / between=)virtualRelation(..., relation=..., condition=...)
joinConditions, setJoinCondition (condizioni date dalla pagina al momento della query)una relazione virtuale con :env_* o ask
formulaColumn(select=...) che esiste solo per un aggregato filtrato@relazione_virtuale.SUM(...)

L'adattatore traduce le forme di GenroPy. Aperto, oltre all'inversa: un prefisso distinto per i parametri di ask; la presenza in * (static=True delle joinColumn); se la relazione espone come colonna il valore del collegamento (l'id della riga collegata).

Selezionare colonne

FormaGenroPyAsqueelMotivo
*colonne fisiche (X solo con bagFields), poi le virtuali con static=Trueuguale—
*prefix_restituisce tutte le colonne dopo una regressione del 2020erroregenropy/genropy#1504
*name, *@rel, *@rel.prefix_espande le colonne della relazione, può moltiplicare le righeerrorenessun uso nei progetti indicizzati; genropy/genropy#623
*@rel.(a,b)moltiplica le righe e le riaggrega in Pythonerrorelo sostituisce @rel.JSON_LIST(#JSON($a, $b))
path senza AS_customer_id_nameugualeclient ed export calcolano lo stesso nome
colonna pkey nei risultatiaggiunta di defaultnon la aggiunge il nucleo; la aggiunge l'adattatoreun risultato del nucleo contiene ciò che si è chiesto
count()—stessi numeri di GenroPy; errore con limit o offset—

Colonna pkey: in GenroPy ogni query aggiunge alle colonne richieste $pkey AS pkey (gnrsqldata/compiler.py:950), e il codice applicativo legge row['pkey']. In Asqueel il nucleo restituisce solo le colonne richieste. Per il codice GenroPy la colonna la aggiunge l'adattatore, con le esclusioni di GenroPy (sezione «L'adattatore GenroPy»).

Effetti sulle migrazioni Decisa

DichiarazioneDatabase
relationFOREIGN KEY ... ON UPDATE CASCADE più indice sulle colonne sorgente
weak_relationindice sulle colonne sorgente e, se non già coperte, sulle colonne target
compositeColumn(..., unique=True)UNIQUE sulle colonne membro
relation su una compositaFOREIGN KEY composita più indice
colgroupniente
colonne virtualiniente

Importando un database che ha vincoli CHECK si importa il modello senza di essi, con un avviso che ne riporta i nomi. Le migrazioni sono additive, quindi i vincoli restano nel database.

Raggruppamento Direzione

La grouped view di GenroPy permette a qualunque utente di raggruppare qualunque vista senza codice: si trascinano i campi, si sceglie Break o un aggregato, si vede una lista, un albero o una pivot, si clicca un gruppo per vederne le righe, si salva la vista. Le applicazioni la usano anche da codice (strutture in 173 moduli di 24 repository). Il nucleo di Asqueel fornirà ciò che serve, per qualunque interfaccia:

Perché cambia. In GenroPy un Break su hierarchical_description dà un nodo per path completo («Building Materials/Insulation/Batts») e nessun totale intermedio, perché chi costruisce l'albero non spezza mai sul / e la cella non porta l'attributo gerarchico. Il server legacy inserisce nell'SQL il nome dell'aggregato mandato dal client; un elenco chiuso lo evita. _pkeylist concatena tutte le chiavi di un gruppo e cresce con i dati; una condizione no.

Chiavi primarie Aperta

Solo analisi. In GenroPy newPkeyValue(record) chiama pkeyValue(record), che prova sei regole in ordine: chiave composita, max + 1 numerico, UUID senza record, pkey_columns, __syscode, UUID testuale di 22 caratteri; altrimenti restituisce None. Quattro applicazioni ridefiniscono newPkeyValue (un codice da un contatore, una chiave fissa per una tabella con un solo record, _ sostituito per gli import bancari, un codice casuale ripetuto finché è unico).

Nuovi identificativi. La chiave testuale di 22 caratteri viene da genro_toolbox.uid.get_uuid(). Il valore è la lettera Z, poi 9 caratteri di timestamp, poi 12 caratteri casuali. Il timestamp conta i microsecondi dal 2025-01-01 UTC e copre circa 429 anni. La parte casuale viene da 9 byte di os.urandom (72 bit). L'alfabeto è base62 in ordine ASCII (0-9, A-Z, a-z). Due chiavi generate in microsecondi diversi si ordinano quindi come il momento di creazione. Nello stesso microsecondo l'ordine è casuale.

In GenroPy getUuid() codifica in base64 un UUID3 calcolato da un UUID1 e dall'id del thread, tronca a 22 caratteri e sostituisce - con _. Il risultato non ha ordine. Il formato nuovo resta di 22 caratteri e dentro [A-Za-z0-9_], quindi le colonne e i controlli esistenti lo accettano. L'ordine porta un vantaggio sull'indice della chiave primaria: i nuovi record finiscono in fondo all'indice. Il vantaggio c'è solo con collation C del database.

Avvicinamento di GenroPy. È in corso un lavoro per adottare in GenroPy i nuovi identificativi: getUuid() restituirà il valore di genro_toolbox.uid.get_uuid(), con genro-toolbox fra le dipendenze (issue genropy/genropy#1552). È ancora da decidere se diventa il comportamento di default o un flag in <experimental> di instanceconfig. Le chiavi generate prima restano valide: le vecchie e le nuove convivono nella stessa colonna.

Difetti da evitare: max + 1 senza lock; insertMany che dà a tutti i record la stessa chiave numerica; collisioni di pkey_columns con valori None o contenenti il separatore; un None silenzioso quando nessuna regola si applica.

Scritture e azioni Python

Prima Python Decisa: quando un comportamento può stare in Python o in SQL, prevale quello Python; i nomi seguono, il nome corto è l'azione Python e il suffisso _sql quella SQL (on_delete contro on_delete_sql). Il ciclo di scrittura completo (trigger di campo, contatori, cascate sui record collegati, protezioni) è ancora da progettare; i compiti dell'adattatore qui sotto elencano il protocollo GenroPy che dovrà riprodurre.

L'adattatore GenroPy

L'adattatore è il modo in cui il codice GenroPy esistente continua a funzionare. È un livello separato: composizione dei package, regole applicative e notifiche non entrano nel driver PostgreSQL o SQLite.

Lato GenroPyCompito dell'adattatore
config_db(pkg), config_db_<pkg>Chiamarli e produrre la dichiarazione Asqueel
Argomenti di sysFields(...), attributi di tabellaTradurli in elementi di options
relation(mode=...), group='name.NNN'Tradurli in relation / weak_relation e in colgroup
joinColumn(...).relation(..., cnd=...), subtable(...)Tradurli in virtualRelation e in subtable di asqueel
Risultati letti come row['pkey']Aggiungere $pkey AS pkey con le esclusioni di GenroPy (nessuna pkey, count, distinct, group_by, subquery delle formule, aggregati)
Protocollo di scrittura: checkPkey, protect_*, trigger di campo, trigger dei package, contatori, dbo_on*, updateRelated / deleteRelated, _onDbChangeRiprodurre l'ordine; l'ordine fa parte del contratto
tempEnv, connessioni nominate, storeTenere tempEnv come modificatore di contesto; il cambio di store non è nel nucleo
Strutture delle grouped viewTradurle in richieste di raggruppamento e ritorno (_grp_count_sum, _pkeylist, _thgroup_pkey)

Tutte le differenze da GenroPy

Ogni divergenza richiede un motivo scritto: uso misurato, difetto mostrato nel codice o requisito del database. L'ultima colonna indica la capacità dei builder coinvolta, quando c'è.

GenroPyAsqueelMotivoBuilder
config_db, attributi scritti in una Bagconfigure, elementi della grammaticauna dichiarazione controllata e documentataelementi tipizzati, costruzione in due fasi
Attributi sconosciuti accettati in silenzioErrore; le estensioni usano x_refusi delle applicazioni mai lettifirme chiuse
Comportamenti della tabella come argomenti di sysFields, attributi, classi baseelementi di tbl.options()un blocco visibile, una voce di documentazione per opzioneregole di contenimento, introspezione
relation() logica, mode='foreignkey' aggiunge la FKrelation() è la FK; weak_relation() logical'86% delle relazioni usa mode='foreignkey'un elemento per tipo, parametri propri
mode='insensitive'weak_relation(insensitive=True)una FK non confronta senza maiuscole; nessun uso trovatoparametro solo dove vale
group='name.NNN', group_<name>=colgroupla codifica mescola gruppo, ordine e visibilità; il contatore non avanzagruppo come elemento con errori controllati
indexed su una relazione ignoratosempre indicizzata salvo copertura delle prime colonneGenroPy controlla l'appartenenza alla pkey, non la posizione—
Relazione verso target non unico accettatarelation: errore; weak_relation: creduto unicoduplicazione silenziosa delle righe—
Elemento constrainttolto; CHECK con gli oggetti nativil'UNIQUE su più colonne è una compositeColumn—
*prefix_, *@rel, *@rel.(a,b), *nameerrore#1504, #623; sostituito da JSON_LIST con #JSON—
#THIS.@rel.col non espanso in sql_formulaespanso#1505—
#PERIOD con BETWEENintervallo semiapertoultimo giorno perso sui timestamp—
Difetti delle tabelle gerarchichecicli, filtri, posizioni, LIKE corretti#1523–#1526—
joinColumnvirtualRelationun collegamento con condizione è una relazione, non una colonnaelemento con parametri propri
Subtable come filtro con nomesubtable come oggetto del modello che eredita dalla tabellasi interroga e si collega come una tabellaelemento della grammatica della tabella
Colonna pkey in ogni risultatoaggiunta dall'adattatorei risultati del nucleo contengono ciò che si è chiesto—
Grouped view: niente gerarchia, aggregati liberi, liste di chiaviBreak gerarchici, aggregati chiusi, condizioniverificato in browser su test_invoice_pg—

Decisioni aperte

  1. Quali attributi di tabella di GenroPy diventano elementi di options e quali restano attributi della tabella.
  2. Come un package registra le proprie opzioni.
  3. I parametri di sys_fields, auto_counter e hierarchical.
  4. Le altre funzioni di sysFields: concorrenza ottimistica, audit, diagnostica, protezione di riga, campi invalidi, unione di record, record di sistema, aggiornamenti per versione.
  5. Tabelle gerarchiche: valori nulli nel path, / nei valori, copyFromParent, _parent_h_*.
  6. La forma di sottoalbero, antenati e radice, insieme alle funzioni di relazione e a virtualRelation.
  7. virtualRelation: inversa con termini di filtro, prefisso dei parametri di ask, presenza in *, valore del collegamento come colonna.
  8. Funzioni di relazione: regola di troncamento dei nomi; nome delle chiavi di #JSON per i path (_customer_id_name o name).
  9. Subtable: nome, colonne proprie, scrittura, forma di package.
  10. bagItemColumn: SQL di estrazione per dialetto e tipo del risultato.
  11. toolColumn, aliasTable, colonne localizzate.
  12. Raggruppamento: la forma della richiesta e del risultato.
  13. Quali hook di costruzione del modello supporta Asqueel (onBuildingDbobj, custom_type_<dtype> usato da 75 package, onApplicationInited, ...).
  14. Generazione delle chiavi primarie e nuovi record (pkeyValue, newPkeyValue, newRecord).
  15. #PERIOD: se il nome nudo p resta valido accanto a :p (genropy/genropy#1509).
  16. Il ciclo di scrittura completo nel nucleo.
  17. Lettura del modello costruito: se il metodo si chiama get_column invece di column, con column accettato dall'adattatore e deprecato.
  18. Nomi delle relazioni inverse senza relation_name (regola <schema>_<tabella>_<colonna>, relazione privata) e registrazione delle macro per livello e contesto: entrambe proposte nel piano 32, non ancora confermate.

Domande generali

La tua mappa

I tuoi pareri o quelli di tutti, sezione per sezione come nell'indice. Ogni riga porta al punto da compilare. d'accordo contrario non so senza risposta segnalazione scritta