1010 * `isNotNull` are unaffected and allowed.
1111 * - `raw-interpolation`: `${table.column}` inside a raw `sql` template. A raw expression is read
1212 * or compared as stored, so it sees ciphertext; select the column itself, or use the helper.
13- * - `raw-column-name`: the physical column name inside a `sql` template or `sql.raw(...)` body,
14- * which reads the stored form the same way. Tests and the sealing backfill read it on purpose.
13+ * - `raw-column-name`: the physical column name inside a `sql` template or `sql.raw(...)` body, a
14+ * `sql.identifier(...)` of it, or a `* FROM <table>` wildcard over its table, all of which read
15+ * the stored form the same way. Tests and the sealing backfill read it on purpose.
1516 *
1617 * Table aliases count as the table: renamed imports from `@sim/db` / `@sim/db/schema` and
1718 * `alias(table, ...)` bindings.
@@ -61,6 +62,7 @@ const COMPARISON_OPERATORS = [
6162]
6263const SQL_TEMPLATE = / \b s q l (?: < [ ^ > \n ] * > ) ? ` ( (?: [ ^ ` \\ ] | \\ .) * ) ` / g
6364const SQL_RAW = / \b s q l \. r a w \( \s * ( [ ' " ` ] ) ( (?: (? ! \1) [ \s \S ] ) * ) \1/ g
65+ const SQL_IDENTIFIER = / \b s q l \. i d e n t i f i e r \( \s * ( [ ' " ` ] ) ( \w + ) \1/ g
6466
6567type Rule = 'comparison' | 'raw-interpolation' | 'raw-column-name'
6668
@@ -76,6 +78,8 @@ interface SealedColumn {
7678 table : string
7779 /** The column's property (`accessToken`). */
7880 property : string
81+ /** The physical table name (`account`). */
82+ tableName : string
7983 /** The physical column name (`access_token`). */
8084 columnName : string
8185 /** How code references it, for reports (`account.accessToken`). */
@@ -90,6 +94,7 @@ function lineOf(source: string, index: number): number {
9094export function readSealedColumns ( schemaSource : string ) : SealedColumn [ ] {
9195 const tables = [ ...schemaSource . matchAll ( TABLE_DECLARATION ) ] . map ( ( match ) => ( {
9296 exportName : match [ 1 ] ,
97+ tableName : match [ 2 ] ,
9398 index : match . index ?? 0 ,
9499 } ) )
95100 const columns : SealedColumn [ ] = [ ]
@@ -99,6 +104,7 @@ export function readSealedColumns(schemaSource: string): SealedColumn[] {
99104 if ( ! table ) continue
100105 columns . push ( {
101106 table : table . exportName ,
107+ tableName : table . tableName ,
102108 property,
103109 columnName,
104110 reference : `${ table . exportName } .${ property } ` ,
@@ -140,16 +146,22 @@ export function findViolations(file: string, source: string, columns: SealedColu
140146 }
141147 }
142148 if ( STORED_FORM_READERS . test ( file ) ) return violations
143- const names = new RegExp ( `\\b(?:${ columns . map ( ( c ) => escapeRegExp ( c . columnName ) ) . join ( '|' ) } )\\b` )
149+ const columnNames = new Set ( columns . map ( ( column ) => column . columnName ) )
150+ const names = new RegExp ( `\\b(?:${ [ ...columnNames ] . map ( escapeRegExp ) . join ( '|' ) } )\\b` )
151+ const tables = [ ...new Set ( columns . map ( ( column ) => column . tableName ) ) ] . map ( escapeRegExp ) . join ( '|' )
152+ const wildcard = new RegExp ( `\\*\\s*from\\s+(?:"?public"?\\.)?"?(?:${ tables } )"?(?![\\w"])` , 'i' )
144153 for ( const pattern of [ SQL_TEMPLATE , SQL_RAW ] ) {
145154 for ( const match of source . matchAll ( pattern ) ) {
146155 const capture = match [ match . length - 1 ]
147156 const bodyOffset = ( match . index ?? 0 ) + match [ 0 ] . lastIndexOf ( capture )
148157 const body = capture . replace ( / \$ \{ [ ^ } ] * \} / g, ( value ) => ' ' . repeat ( value . length ) )
149- const hit = names . exec ( body )
158+ const hit = names . exec ( body ) ?? wildcard . exec ( body )
150159 if ( hit ) report ( 'raw-column-name' , bodyOffset + hit . index , hit [ 0 ] )
151160 }
152161 }
162+ for ( const match of source . matchAll ( SQL_IDENTIFIER ) ) {
163+ if ( columnNames . has ( match [ 2 ] ) ) report ( 'raw-column-name' , match . index ?? 0 , match [ 0 ] )
164+ }
153165 return violations
154166}
155167
0 commit comments