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.
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.
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:
- le scelte che non condividi, e perché;
- i comportamenti di GenroPy dimenticati o fraintesi;
- gli usi nelle tue applicazioni che una decisione romperebbe.
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.
| Decisione | Stato | D'accordo | Contrario | Non so | Note |
|---|---|---|---|---|---|
| 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.
- Qualunque applicazione Python dichiara un modello, apre un database e lo interroga con lo stesso linguaggio di espressioni di GenroPy (
$col,@rel.col,:param, macro). - Un'applicazione GenroPy usa Asqueel tramite un adattatore. L'adattatore traduce le dichiarazioni GenroPy (
config_db,sysFields, attributi di tabella, strutture delle griglie) in dichiarazioni Asqueel, e riporta i risultati nella forma che il codice GenroPy si aspetta.
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
- La sintassi di GenroPy è quella di default. Un modello o un'espressione scritti per GenroPy significano la stessa cosa in Asqueel, quando Asqueel li supporta.
- Nessuna grafia alternativa. Asqueel non aggiunge un secondo modo di scrivere ciò che GenroPy esprime già.
- Ogni divergenza ha un motivo esterno scritto: un inventario dell'uso reale, un difetto del legacy mostrato nel codice o un requisito del database.
- Una forma non supportata dà un errore esplicito, mai un risultato diverso.
- Prima Python. Nel dubbio fra un comportamento in Python e uno delegato all'SQL prevale quello Python. Il nome corto è l'azione Python, il suffisso
_sqlquella SQL. - snake_case per i parametri. Un parametro di GenroPy tiene il nome in forma snake (
onDelete_sql→on_delete_sql). I nomi degli elementi di GenroPy restano invariati (formulaColumn,aliasColumn,compositeColumn); gli elementi nuovi sono in snake (weak_relation,colgroup,sys_fields). - La dichiarazione è una ricetta; il modello è un albero di oggetti prodotto da un renderer, senza corrispondenza uno a uno fra nodi e oggetti. Le regole sono nella sezione «Sorgente e modello costruito».
- Le funzioni si prendono da ciò che GenroPy offre, non dalla sua implementazione; la compatibilità la dà l'adattatore.
- Ciò che non è implementato resta assente, senza codice provvisorio.
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 builder | Cosa significa per un modello | GenroPy 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 rende | La 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 grammatica | Il 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 vuotoAsqueel
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:
- costruzione: le dichiarazioni (
configure, gli hook di costruzione, l'adattatore) scrivono un sorgente, che è la ricetta; - render: il renderer trasforma il sorgente negli oggetti del modello.
Le regole valgono a ogni livello: database, schema, package, tabella, colonna, relazione, gruppo, opzione.
- A sistema avviato il sorgente non è raggiungibile: nessun oggetto lo tiene e nessuna API lo restituisce.
- Tutto ciò che serve a sistema avviato sta negli oggetti. Se un dato manca, lo copia il renderer.
- A sistema avviato esiste quindi una sola fonte di verità sul modello: gli oggetti. Nessun codice legge una seconda versione che potrebbe essere diversa.
- Gli hook che modificano il modello girano durante la costruzione, prima del render.
db.table('sales.invoice')restituisce l'oggetto tabella. Lo stesso vale per gli altri livelli.- Un proxy aggiunto per non affollare un namespace collega oggetti fra loro. Non esiste un passaggio dal sorgente agli oggetti.
- Ogni oggetto ha
attributes, in sola lettura: una copia degli attributi dell'oggetto, dichiarati o assegnati durante la costruzione. Esempi: il gruppo di una colonna dichiarata tramite il gruppo; gli attributi delle colonne create da un'opzione. Le proprietà dell'oggetto eattributesdescrivono lo stesso oggetto.
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 tblLe 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 GenroPy | Cosa fa | Uso nelle applicazioni | Proposta |
|---|---|---|---|
joinColumn | colonna virtuale con .relation(target, cnd=...): collega la riga a un'altra tabella con una condizione, senza FK | circa 12 usi in 8 repository | sostituita da virtualRelation; l'adattatore traduce |
toolColumn | colonna virtuale che mostra un link verso uno strumento esterno (per esempio un file ICS); con dtype='P' un'immagine | 1 uso | da decidere |
aliasTable | nome 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 package | pochi 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
| Elemento | Database | Target |
|---|---|---|
relation(...) | FOREIGN KEY ... ON UPDATE CASCADE più un indice sulle colonne sorgente | deve avere una chiave unica; altrimenti errore |
weak_relation(...) | nessun vincolo; indice su colonne sorgente e target | creduto 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
| Opzione | Funzioni | Origine in GenroPy |
|---|---|---|
sys_fields | chiave primaria generata; timestamp di insert e update; utente di insert e update; cancellazione logica; bozza | sysFields(id, ins, upd, user_ins, user_upd, ldel, draftField) |
auto_counter | numerazione delle righe sulla tabella o dentro una FK; numerazione dentro un master che non riusa i numeri | sysFields(counter=..., counter_<nome>=..., relidx=...). Non riguarda i contatori di protocollo, cioè i metodi counter_<campo> della tabella. |
hierarchical | struttura 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:
False: la colonna non viene aggiunta;True: la colonna viene aggiunta con gli attributi standard;- un dict: la colonna viene aggiunta e gli attributi del dict prevalgono su quelli standard.
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:
| Gruppo | Attributi (file che li usano) |
|---|---|
| Proprietà della tabella, usate quasi ovunque | name_long 5707, name_plural 5317, pkey 5219, caption_field 5047 |
| Funzioni usate davvero | rowcaption 675, lookup 561, partition_* 240, multidb 134, order_by 105, group_* 90, unifyRecordsTag 58, broadcast 57, archivable 40, audit 20 |
| Funzioni scritte come classi base | AttachmentTable 206 file, TotalizeTable 13 |
| Mai usati nelle applicazioni | lastTS, 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
| GenroPy | Asqueel | Issue |
|---|---|---|
| Spostare un nodo sotto un suo discendente non è controllato sul server: cascata senza fine | Errore | genropy/genropy#1523 |
| Padre e figli letti con i filtri di bozza e cancellazione logica: il figlio di un padre archiviato diventa radice | Lettura 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 = 1295 | Ultima 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
| Forma | Significato |
|---|---|
$col | colonna della tabella corrente |
@customer_id.name | colonna raggiunta tramite la relazione di customer_id |
@customer_id.@state_id.name | più passaggi; ogni segmento di relazione inizia con @. @customer.country.name (segmento intermedio senza @) è un errore, come in GenroPy |
:name | parametro |
:env_name | valore 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) |
#name | subquery 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
- La relazione uscente prende il nome della sua colonna:
@customer_id. Decisa - La relazione inversa prende il nome da
relation_namesulla relazione che la dichiara. Senzarelation_nameil nome segue la regola di GenroPy<schema>_<tabella>_<colonna>e la relazione è privata. Proposta
Macro
- Una macro è un costrutto del linguaggio, espanso durante la compilazione in parti dell'espressione. I path dentro i suoi argomenti si risolvono come ogni altro path. Decisa (30 settembre 2026)
- Le macro si registrano per livello (nucleo, dialetto, applicazione, package) e per contesto (colonne, where, order by, group by, having, condizione di relazione, formula, record). Una macro sconosciuta al dialetto è un errore di compilazione. Proposta
| Macro | Regole | Stato |
|---|---|---|
#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 |
#ENV | Non portato: lo sostituisce :env_name. | Decisa |
#BAG, #BAGCOLS, TSQUERY, VECQUERY, #PREF | Da 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.
configure(self, tables)dichiara.tbl.column(...)crea sempre una colonna e non ne cerca mai una. Dentroconfigurenon esiste lettura del modello.- Una dichiarazione che dipende da un'altra tabella la nomina con un nome o un path:
relation('sales.customer.id'),aliasColumn('customer_name', relation_path='@customer_id.name'). Il renderer la risolve dopo, quando tutte le tabelle sono dichiarate; un riferimento sbagliato è un errore al render. - Il modello costruito si legge dagli oggetti:
db.table('sales.invoice')restituisce l'oggetto tabella (sezione «Sorgente e modello costruito»).
Negli esempi di questa sezione invoice = db.table('sales.invoice').
column() ed eccezioni Decisa
invoice.column('total')einvoice.column('$total')restituiscono la colonnatotaldisales.invoice.invoice.column('@customer_id.name')restituisce la colonnanamedisales.customer, raggiunta con la relazione@customer_id.- Ogni forma restituisce la colonna che il compiler risolve per quella espressione.
- Una colonna offre
relatedTable()erelatedColumn(), come in GenroPy. - Un nome semplice mancante restituisce
None; un path con una relazione mancante alza un'eccezione. È il comportamento di GenroPy:invoice.column('x') is not Noneè il modo del framework per sapere se una tabella ha una colonna. - Su una tabella con chiave primaria composita
pkeysrestituisce le colonne membro. - Le eccezioni formano una gerarchia con radice
GnrSqlException(nome di GenroPy), una classe per caso:GnrSqlMissingTable,GnrSqlMissingField(relazione mancante),GnrSqlMissingColumn(colonna finale mancante),GnrSqlInvalidVirtualColumn,GnrSqlRelationError(dichiarazione di relazione). In GenroPy queste derivano daGnrExceptione sfuggono aexcept GnrSqlException.
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:
| Forma | Opera su | Esempio |
|---|---|---|
#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)).
| Espressione | Subquery 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.
- Più passaggi.
@invoices.@rows.SUM($quantity)aggrega su tutte le righe raggiungibili dal path (tutte le righe di tutte le fatture del cliente). Un aggregato annidato dà un'altra grandezza:@invoices.AVG(@rows.SUM($quantity))è la media, sulle fatture, della somma per fattura. Le due forme convivono perché l'argomento è diverso (colonna o funzione di relazione). Sui dati di prova: 1097 / 43 = 25,51 contro 1097 / 13 = 84,38. SUMsul testo è l'aggregazione di stringhe:@invoices.SUM($number)unisce i valori con il separatore del modello, altrimenti',';@invoices.SUM($number, ' | ')lo dà esplicito. L'ordine segue l'order_bydella tabella collegata, altrimenti la chiave primaria.JSON_DICTeJSON_LISTdanno un JSON, tipoX.@invoices.JSON_DICT($date, $total)dà un dizionario per chiave primaria della tabella collegata:{"<id>": {"date": ..., "total": ...}, ...}. Conkey=$numberla chiave è un'altra colonna, che deve essere unica; altrimenti è un errore.@invoices.JSON_LIST($number)dà una lista di valori;@invoices.JSON_LIST(#JSON($date, $total))una lista di oggetti. L'ordine segue l'order_bydella tabella collegata, altrimenti la chiave primaria. Sostituiscono*@rel.(a,b), che non dice cosa aggrega.- Nessun filtro nella funzione.
@rel.FUNZIONE(...)vale su tutta la relazione. Un insieme filtrato è una relazione virtuale (sezione successiva):@invoices_current_year.SUM($total).
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.
#SUM(a, b, ...)somma i numeri e concatena il testo; i valori non testuali in un#SUMdi testo diventano testo.#SUM(@invoices.SUM($total), @credit_notes.SUM($total))somma due subquery.#TEMPLATE('...')formatta una riga:$xè sempre un campo della riga,#xun argomento con nome:#TEMPLATE('totale #ttl su #n fatture', @invoices.SUM($total) AS ttl, @invoices.COUNT() AS n). Su una relazione verso molte righe serve un aggregato:@invoices.SUM(#TEMPLATE('$year-$number')). I nomi degli argomenti non possono coincidere con quelli delle macro.#JSON(campi)costruisce un oggetto dai campi della riga:#JSON($date, @customer_id.name AS customer, $total). SenzaASla chiave segue la regola dei nomi automatici ($datedàdate).
Colonne aggregabili nel modello
Una colonna dichiara di essere aggregabile con aggregate=; il valore dipende dal tipo:
| Tipo | aggregate=True | Altri valori |
|---|---|---|
numerico (N, L, R, I) | SUM | elenco di funzioni: aggregate='SUM,AVG,MIN,MAX' |
testo (A, T) | unione con ',' | il separatore: aggregate=' / ' |
| data, timestamp, booleano | testo 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
| Funzione | PostgreSQL | SQLite |
|---|---|---|
COUNT, SUM, AVG, MIN, MAX, COUNT(DISTINCT(x)) | standard | standard |
SUM sul testo | string_agg(expr, sep ORDER BY ...) | group_concat(expr, sep), ordine non garantito |
#TEMPLATE, #SUM sul testo | || | || |
#JSON | json_build_object | json_object |
JSON_LIST | json_agg | json_group_array |
JSON_DICT | json_object_agg | json_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
relation=: una relazione della tabella, automatica o virtuale; l'accoppiamento si eredita.table=: la tabella collegata, richiesta solo senzarelation=(accoppiamento senza FK).condition=: un predicato;$cole@rel.colsi riferiscono sempre alla tabella collegata.var_<nome>=: lega la variabile:var_<nome>a un'espressione della tabella di partenza ($col,@path.col, una funzione di relazione). I valori della riga di partenza entrano solo così;#THISnon si usa.order_by=,limit=: una o poche righe scelte per ordine.limit=1implicaone_one.ask=: un parametro chiesto all'utente, come in una formula.
# 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:
- un termine con una variabile legata alla tabella di partenza è accoppiamento: dice come una riga collegata appartiene alla riga di partenza;
- un termine con solo
:env_*, valori diasko costanti è filtro: dice quali righe collegate interessano; - un termine con OR che contiene una variabile legata alla tabella di partenza è accoppiamento per intero.
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
- Sola lettura: niente cluster di record,
on_delete, indici o vincoli. - Composizione:
relation=verso una relazione virtuale ne eredita tutto in AND. Le dipendenze formano un grafo; i cicli sono un errore. - Ordine di valutazione: accoppiamento, poi
condition, poiorder_byelimit. order_byconlimit: l'ordine dev'essere totale, il compiler aggiunge la chiave primaria per gli spareggi. Derivare da una relazione conlimitè vietato, oppure eredita solo accoppiamento e condizione e rifà ordine e limite. Si compila comeLEFT JOIN LATERAL (... ORDER BY ... LIMIT 1) ON truesu PostgreSQL, o come subquery correlata nell'ON, valida anche su SQLite.one_onesenzalimitè una garanzia sui dati, non sullo schema: serve un vincolo (indice unico parziale, vincolo di esclusione per gli intervalli), altrimenti il join moltiplica le righe.- Tipo di join: la navigazione è sempre LEFT. Un INNER è un semi-join e si scrive come filtro (
@rel.COUNT() > 0); il compiler può usare INNER da solo quando la relazione è totale (FK NOT NULL). La condizione va nell'ON del LEFT JOIN, non nel WHERE. - Path della tabella di partenza nelle variabili: solo verso una riga; il compiler mette quel join prima di quello della relazione. Un path verso molte righe richiede un aggregato.
- NULL: una FK intermedia nulla o un aggregato su un insieme vuoto danno NULL e il confronto esclude ogni riga; un altro comportamento richiede un COALESCE esplicito.
- Parametri
:env_*: rendono il risultato dipendente dal momento della valutazione e fanno parte della sua identità per cache o materializzazione. - Nomi: un nome usato sia da
asksia da una variabile legata alla tabella è un errore di dichiarazione.
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
| GenroPy | In 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
| Forma | GenroPy | Asqueel | Motivo |
|---|---|---|---|
* | colonne fisiche (X solo con bagFields), poi le virtuali con static=True | uguale | — |
*prefix_ | restituisce tutte le colonne dopo una regressione del 2020 | errore | genropy/genropy#1504 |
*name, *@rel, *@rel.prefix_ | espande le colonne della relazione, può moltiplicare le righe | errore | nessun uso nei progetti indicizzati; genropy/genropy#623 |
*@rel.(a,b) | moltiplica le righe e le riaggrega in Python | errore | lo sostituisce @rel.JSON_LIST(#JSON($a, $b)) |
path senza AS | _customer_id_name | uguale | client ed export calcolano lo stesso nome |
colonna pkey nei risultati | aggiunta di default | non la aggiunge il nucleo; la aggiunge l'adattatore | un 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
| Dichiarazione | Database |
|---|---|
relation | FOREIGN KEY ... ON UPDATE CASCADE più indice sulle colonne sorgente |
weak_relation | indice sulle colonne sorgente e, se non già coperte, sulle colonne target |
compositeColumn(..., unique=True) | UNIQUE sulle colonne membro |
relation su una composita | FOREIGN KEY composita più indice |
colgroup | niente |
| colonne virtuali | niente |
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:
- una richiesta di raggruppamento dichiarativa e serializzabile (Break, aggregati da un elenco chiuso, filtri, pivot), compilata dallo stesso resolver;
- Break su una tabella gerarchica che producono i nodi intermedi con i loro totali, a qualsiasi profondità;
- regole di risalita dei totali calcolate in Python sopra un
GROUP BYSQL piatto (somma, conteggio, minimo, massimo, media pesata, unione dei valori distinti); - drill-down per condizione invece che per elenco di chiavi; concatenamento, dove la condizione di un gruppo filtra un nuovo raggruppamento;
- metadati del modello per l'interfaccia: relazioni, tipi, caption leggibili, campi gerarchici.
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 GenroPy | Compito dell'adattatore |
|---|---|
config_db(pkg), config_db_<pkg> | Chiamarli e produrre la dichiarazione Asqueel |
Argomenti di sysFields(...), attributi di tabella | Tradurli 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, _onDbChange | Riprodurre l'ordine; l'ordine fa parte del contratto |
tempEnv, connessioni nominate, store | Tenere tempEnv come modificatore di contesto; il cambio di store non è nel nucleo |
| Strutture delle grouped view | Tradurle 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'è.
| GenroPy | Asqueel | Motivo | Builder |
|---|---|---|---|
config_db, attributi scritti in una Bag | configure, elementi della grammatica | una dichiarazione controllata e documentata | elementi tipizzati, costruzione in due fasi |
| Attributi sconosciuti accettati in silenzio | Errore; le estensioni usano x_ | refusi delle applicazioni mai letti | firme chiuse |
Comportamenti della tabella come argomenti di sysFields, attributi, classi base | elementi di tbl.options() | un blocco visibile, una voce di documentazione per opzione | regole di contenimento, introspezione |
relation() logica, mode='foreignkey' aggiunge la FK | relation() è la FK; weak_relation() logica | l'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 trovato | parametro solo dove vale |
group='name.NNN', group_<name>= | colgroup | la codifica mescola gruppo, ordine e visibilità; il contatore non avanza | gruppo come elemento con errori controllati |
indexed su una relazione ignorato | sempre indicizzata salvo copertura delle prime colonne | GenroPy controlla l'appartenenza alla pkey, non la posizione | — |
| Relazione verso target non unico accettata | relation: errore; weak_relation: creduto unico | duplicazione silenziosa delle righe | — |
Elemento constraint | tolto; CHECK con gli oggetti nativi | l'UNIQUE su più colonne è una compositeColumn | — |
*prefix_, *@rel, *@rel.(a,b), *name | errore | #1504, #623; sostituito da JSON_LIST con #JSON | — |
#THIS.@rel.col non espanso in sql_formula | espanso | #1505 | — |
#PERIOD con BETWEEN | intervallo semiaperto | ultimo giorno perso sui timestamp | — |
| Difetti delle tabelle gerarchiche | cicli, filtri, posizioni, LIKE corretti | #1523–#1526 | — |
joinColumn | virtualRelation | un collegamento con condizione è una relazione, non una colonna | elemento con parametri propri |
| Subtable come filtro con nome | subtable come oggetto del modello che eredita dalla tabella | si interroga e si collega come una tabella | elemento della grammatica della tabella |
Colonna pkey in ogni risultato | aggiunta dall'adattatore | i risultati del nucleo contengono ciò che si è chiesto | — |
| Grouped view: niente gerarchia, aggregati liberi, liste di chiavi | Break gerarchici, aggregati chiusi, condizioni | verificato in browser su test_invoice_pg | — |
Decisioni aperte
- Quali attributi di tabella di GenroPy diventano elementi di
optionse quali restano attributi della tabella. - Come un package registra le proprie opzioni.
- I parametri di
sys_fields,auto_counterehierarchical. - Le altre funzioni di
sysFields: concorrenza ottimistica, audit, diagnostica, protezione di riga, campi invalidi, unione di record, record di sistema, aggiornamenti per versione. - Tabelle gerarchiche: valori nulli nel path,
/nei valori,copyFromParent,_parent_h_*. - La forma di sottoalbero, antenati e radice, insieme alle funzioni di relazione e a
virtualRelation. virtualRelation: inversa con termini di filtro, prefisso dei parametri diask, presenza in*, valore del collegamento come colonna.- Funzioni di relazione: regola di troncamento dei nomi; nome delle chiavi di
#JSONper i path (_customer_id_nameoname). - Subtable: nome, colonne proprie, scrittura, forma di package.
bagItemColumn: SQL di estrazione per dialetto e tipo del risultato.toolColumn,aliasTable, colonne localizzate.- Raggruppamento: la forma della richiesta e del risultato.
- Quali hook di costruzione del modello supporta Asqueel (
onBuildingDbobj,custom_type_<dtype>usato da 75 package,onApplicationInited, ...). - Generazione delle chiavi primarie e nuovi record (
pkeyValue,newPkeyValue,newRecord). #PERIOD: se il nome nudopresta valido accanto a:p(genropy/genropy#1509).- Il ciclo di scrittura completo nel nucleo.
- Lettura del modello costruito: se il metodo si chiama
get_columninvece dicolumn, concolumnaccettato dall'adattatore e deprecato. - 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